mirror of
https://github.com/immich-app/immich.git
synced 2025-02-18 01:24:26 -05:00
Merge branch 'main' into main
This commit is contained in:
commit
8b3a27e2b7
148 changed files with 2779 additions and 2494 deletions
2
.github/workflows/cli.yml
vendored
2
.github/workflows/cli.yml
vendored
|
@ -88,7 +88,7 @@ jobs:
|
|||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
uses: docker/build-push-action@v6.12.0
|
||||
with:
|
||||
file: cli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
|
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
|
@ -174,7 +174,7 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
uses: docker/build-push-action@v6.12.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
|
@ -265,7 +265,7 @@ jobs:
|
|||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.11.0
|
||||
uses: docker/build-push-action@v6.12.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
|
|
|
@ -1 +1 @@
|
|||
22.13.0
|
||||
22.13.1
|
||||
|
|
10
cli/package-lock.json
generated
10
cli/package-lock.json
generated
|
@ -24,7 +24,7 @@
|
|||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
|
@ -59,7 +59,7 @@
|
|||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
|
@ -1397,9 +1397,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
|
@ -67,6 +67,6 @@
|
|||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
22.13.0
|
||||
22.13.1
|
||||
|
|
|
@ -160,6 +160,35 @@ For example, say you have existing transcodes with the policy "Videos higher tha
|
|||
|
||||
No. Our design principle is that the original assets should always be untouched.
|
||||
|
||||
### How can I mount a CIFS/Samba volume within Docker?
|
||||
|
||||
If you aren't able to or prefer not to mount Samba on the host (such as Windows environment), you can mount the volume within Docker.
|
||||
Below is an example in the `docker-compose.yml`.
|
||||
|
||||
Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`,
|
||||
corrolates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like.
|
||||
For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`.
|
||||
|
||||
```diff
|
||||
...
|
||||
services:
|
||||
immich-server:
|
||||
...
|
||||
volumes:
|
||||
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
+ - originals:/usr/src/app/originals
|
||||
...
|
||||
volumes:
|
||||
model-cache:
|
||||
+ originals:
|
||||
+ driver_opts:
|
||||
+ type: cifs
|
||||
+ o: 'iocharset=utf8,username=USERNAMEHERE,password=PASSWORDHERE,rw' # change to `ro` if read only desired
|
||||
+ device: '//localipaddress/sharename'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Albums
|
||||
|
|
|
@ -68,7 +68,7 @@ After bringing down the containers with `docker compose down` and back up with `
|
|||
:::note
|
||||
To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8082` to the microservices container's ports.
|
||||
Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects.
|
||||
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](../install/environment-variables/#general).
|
||||
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/docs/install/environment-variables/#general).
|
||||
:::
|
||||
|
||||
### Usage
|
||||
|
|
|
@ -49,5 +49,3 @@ The `thumbs/` folder contains both the small thumbnails displayed in the timelin
|
|||
|
||||
The storage metrics of the Immich server will track available storage at `UPLOAD_LOCATION`, so the administrator must set up some sort of monitoring to ensure the storage does not run out of space. The `profile/` folder is much smaller, usually less than 1 MB.
|
||||
:::
|
||||
|
||||
Thanks to [Jrasm91](https://github.com/immich-app/immich/discussions/2110#discussioncomment-5477767) for writing the guide.
|
||||
|
|
|
@ -159,10 +159,10 @@ Redis (Sentinel) URL example JSON before encoding:
|
|||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Name of the textual CLIP model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Name of the visual CLIP model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Name of the recognition portion of the facial recognition model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Name of the detection portion of the facial recognition model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
||||
|
|
|
@ -55,6 +55,6 @@
|
|||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,9 +73,9 @@ function HomepageHeader() {
|
|||
/>
|
||||
|
||||
<div>
|
||||
<p className="font-bold text-2xl md:text-5xl ">Download mobile app</p>
|
||||
<p className="font-bold text-2xl md:text-5xl ">Download the mobile app</p>
|
||||
<p className="text-lg">
|
||||
Download Immich app and start backing up your photos and videos securely to your own server
|
||||
Download the Immich app and start backing up your photos and videos securely to your own server
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
|
||||
|
|
|
@ -1 +1 @@
|
|||
22.13.0
|
||||
22.13.1
|
||||
|
|
12
e2e/package-lock.json
generated
12
e2e/package-lock.json
generated
|
@ -15,7 +15,7 @@
|
|||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
|
@ -64,7 +64,7 @@
|
|||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
|
@ -99,7 +99,7 @@
|
|||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
|
@ -1658,9 +1658,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
|
@ -53,6 +53,6 @@
|
|||
"vitest": "^2.0.5"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -142,6 +142,10 @@ describe('/albums', () => {
|
|||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ isFavorite: false })],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
shared: true,
|
||||
albumUsers: expect.any(Array),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -299,6 +303,10 @@ describe('/albums', () => {
|
|||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -330,6 +338,10 @@ describe('/albums', () => {
|
|||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -344,6 +356,10 @@ describe('/albums', () => {
|
|||
assets: [],
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -200,7 +200,7 @@ describe('/people', () => {
|
|||
expect(body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'New Person',
|
||||
birthDate: '1990-01-01',
|
||||
birthDate: '1990-01-01T00:00:00.000Z',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -244,7 +244,7 @@ describe('/people', () => {
|
|||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ birthDate: '1990-01-01' });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ birthDate: '1990-01-01' });
|
||||
expect(body).toMatchObject({ birthDate: '1990-01-01T00:00:00.000Z' });
|
||||
});
|
||||
|
||||
it('should clear a date of birth', async () => {
|
||||
|
|
|
@ -119,93 +119,57 @@ describe('/stacks', () => {
|
|||
const stacksAfter = await searchStacks({}, { headers: asBearerAuth(user1.accessToken) });
|
||||
expect(stacksAfter.length).toBe(stacksBefore.length);
|
||||
});
|
||||
|
||||
// it('should require a valid parent id', async () => {
|
||||
// const { status, body } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
// .send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
|
||||
|
||||
// expect(status).toBe(400);
|
||||
// expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
|
||||
// });
|
||||
});
|
||||
|
||||
// it('should require access to the parent', async () => {
|
||||
// const { status, body } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
// .send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
|
||||
describe('GET /assets/:id', () => {
|
||||
it('should include stack details for the primary asset', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// expect(status).toBe(400);
|
||||
// expect(body).toEqual(errorDto.noPermission);
|
||||
// });
|
||||
await utils.createStack(user1.accessToken, [asset1.id, asset2.id]);
|
||||
|
||||
// it('should add stack children', async () => {
|
||||
// const { status } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||
// .send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
|
||||
const { status, body } = await request(app)
|
||||
.get(`/assets/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
// expect(status).toBe(204);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
id: asset1.id,
|
||||
stack: {
|
||||
id: expect.any(String),
|
||||
assetCount: 2,
|
||||
primaryAssetId: asset1.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||
// expect(asset.stack).not.toBeUndefined();
|
||||
// expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
|
||||
// });
|
||||
it('should include stack details for a non-primary asset', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// it('should remove stack children', async () => {
|
||||
// const { status } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||
// .send({ removeParent: true, ids: [stackAssets[1].id] });
|
||||
await utils.createStack(user1.accessToken, [asset1.id, asset2.id]);
|
||||
|
||||
// expect(status).toBe(204);
|
||||
const { status, body } = await request(app)
|
||||
.get(`/assets/${asset2.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||
// expect(asset.stack).not.toBeUndefined();
|
||||
// expect(asset.stack).toEqual(
|
||||
// expect.arrayContaining([
|
||||
// expect.objectContaining({ id: stackAssets[2].id }),
|
||||
// expect.objectContaining({ id: stackAssets[3].id }),
|
||||
// ]),
|
||||
// );
|
||||
// });
|
||||
|
||||
// it('should remove all stack children', async () => {
|
||||
// const { status } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||
// .send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
|
||||
|
||||
// expect(status).toBe(204);
|
||||
|
||||
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||
// expect(asset.stack).toBeUndefined();
|
||||
// });
|
||||
|
||||
// it('should merge stack children', async () => {
|
||||
// // create stack after previous test removed stack children
|
||||
// await updateAssets(
|
||||
// { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
|
||||
// { headers: asBearerAuth(stackUser.accessToken) },
|
||||
// );
|
||||
|
||||
// const { status } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||
// .send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
|
||||
|
||||
// expect(status).toBe(204);
|
||||
|
||||
// const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||
// expect(asset.stack).not.toBeUndefined();
|
||||
// expect(asset.stack).toEqual(
|
||||
// expect.arrayContaining([
|
||||
// expect.objectContaining({ id: stackAssets[0].id }),
|
||||
// expect.objectContaining({ id: stackAssets[1].id }),
|
||||
// expect.objectContaining({ id: stackAssets[2].id }),
|
||||
// ]),
|
||||
// );
|
||||
// });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
id: asset2.id,
|
||||
stack: {
|
||||
id: expect.any(String),
|
||||
assetCount: 2,
|
||||
primaryAssetId: asset1.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"about": "حول",
|
||||
"about": "من نحن",
|
||||
"account": "الحساب",
|
||||
"account_settings": "إعدادات الحساب",
|
||||
"acknowledge": "أُدرك ذلك",
|
||||
|
|
|
@ -525,6 +525,7 @@
|
|||
"deduplicate_all": "Desduplica-ho tot",
|
||||
"deduplication_criteria_1": "Mida d'imatge en bytes",
|
||||
"deduplication_criteria_2": "Quantitat de dades EXIF",
|
||||
"deduplication_info": "Informació de deduplicació",
|
||||
"deduplication_info_description": "Per preseleccionar recursos automàticament i eliminar els duplicats de manera massiva, ens fixem en:",
|
||||
"default_locale": "Localització predeterminada",
|
||||
"default_locale_description": "Format de dates i números segons la configuració del navegador",
|
||||
|
|
13
i18n/cv.json
13
i18n/cv.json
|
@ -50,8 +50,16 @@
|
|||
"map_gps_settings_description": "Карттӑпа GPS (каялла геоюмлани) ӗнерленисене йӗркелесе тӑрӑр",
|
||||
"map_settings": "Карттӑ"
|
||||
},
|
||||
"albums": "Албумсем",
|
||||
"albums_count": "{count, plural, one {{count, number} албум} other {{count, number} албумсем}}",
|
||||
"all": "Пурте",
|
||||
"all_albums": "Пурте албумсем",
|
||||
"explore": "Тишкер",
|
||||
"explorer": "Тишкерӳҫӗ",
|
||||
"favorite": "Юратнӑ",
|
||||
"favorite_or_unfavorite_photo": "Юратнӑ е юратман сӑнӳкерчӗк",
|
||||
"favorites": "Юратнисем",
|
||||
"feature_photo_updated": "Уйрӑм сӑнӳкерчӗк ҫӗнетнӗ",
|
||||
"manage_sharing_with_partners": "Партнерсемпе пайланассине йӗркелесе пырӑр",
|
||||
"map": "Карттӑ",
|
||||
"map_marker_for_images": "{city}, {country} ҫинче ӳкернӗ ӳкерчӗксем валли карттӑ маркерӗ",
|
||||
|
@ -60,10 +68,15 @@
|
|||
"no_explore_results_message": "Хӑвӑр коллекципе киленмешкӗн сӑнӳкерчӗксем ытларах тийӗр.",
|
||||
"open_in_openstreetmap": "OpenStreetMap-па уҫ",
|
||||
"partner_sharing": "Партнер пайланӑвӗ",
|
||||
"people": "Ҫынсем",
|
||||
"photos": "Сӑнӳкерчӗксем",
|
||||
"photos_and_videos": "Сӑнӳкерчӗксем тете Видеосем",
|
||||
"photos_count": "{count, plural, one {{count, number} Сӑнӳкерчӗк} other {{count, number} Сӑнӳкерчӗксем}}",
|
||||
"photos_from_previous_years": "Иртнӗ ҫулсенчи сӑнӳкерчӗксем",
|
||||
"place": "Тӗл",
|
||||
"places": "Тӗлсем",
|
||||
"play": "Выля",
|
||||
"play_memories": "Асаилӳсем выля",
|
||||
"search_your_photos": "Сӑнӳкерчӗксене шырӑр",
|
||||
"select_photos": "Сӑнӳкерчӗксем суйлӑр",
|
||||
"sharing": "Пайлани",
|
||||
|
|
10
i18n/da.json
10
i18n/da.json
|
@ -523,6 +523,10 @@
|
|||
"date_range": "Datointerval",
|
||||
"day": "Dag",
|
||||
"deduplicate_all": "Dedupliker alle",
|
||||
"deduplication_criteria_1": "Billedstørrelse i bytes",
|
||||
"deduplication_criteria_2": "Antal EXIF-data",
|
||||
"deduplication_info": "Deduplikerings info",
|
||||
"deduplication_info_description": "For automatisk at forudvælge emner og fjerne dubletter i bulk ser vi på:",
|
||||
"default_locale": "Standardlokalitet",
|
||||
"default_locale_description": "Formatér datoer og tal",
|
||||
"delete": "Slet",
|
||||
|
@ -644,6 +648,7 @@
|
|||
"unable_to_add_partners": "Ikke i stand til at tilføje partnere",
|
||||
"unable_to_add_remove_archive": "Kan Ikke {archived, select, true {fjerne aktiv fra} other {tilføje aktiv til}} Arkiv",
|
||||
"unable_to_add_remove_favorites": "Kan ikke {favorite, select, true {tilføje aktiv til} other {fjerne aktiv fra}} favoritter",
|
||||
"unable_to_archive_unarchive": "Ude af stand til at {arkiveret, vælg, sand {arkiv} andet {arkiv}}",
|
||||
"unable_to_change_album_user_role": "Ikke i stand til at ændre albumbrugerens rolle",
|
||||
"unable_to_change_date": "Ikke i stand til at ændre dato",
|
||||
"unable_to_change_favorite": "Kan ikke ændre favorit for aktiv",
|
||||
|
@ -730,6 +735,7 @@
|
|||
"expired": "Udløbet",
|
||||
"expires_date": "Udløber {date}",
|
||||
"explore": "Udforsk",
|
||||
"explorer": "Udforske",
|
||||
"export": "Eksportér",
|
||||
"export_as_json": "Eksportér som JSON",
|
||||
"extension": "Udvidelse",
|
||||
|
@ -917,6 +923,7 @@
|
|||
"offline_paths_description": "Disse resultater kan være på grund af manuel sletning af filer, som ikke er en del af et eksternt bibliotek.",
|
||||
"ok": "Ok",
|
||||
"oldest_first": "Ældste først",
|
||||
"onboarding": "Onboarding",
|
||||
"onboarding_privacy_description": "Følgende (valgfrie) funktioner er afhængige af eksterne tjenester, og kan til enhver tid deaktiveres i administrationsindstillingerne.",
|
||||
"onboarding_theme_description": "Vælg et farvetema til din forekomst. Du kan ændre dette senere i dine indstillinger.",
|
||||
"onboarding_welcome_description": "Lad os få din instans sat op med nogle almindelige indstillinger.",
|
||||
|
@ -1249,6 +1256,7 @@
|
|||
"to_change_password": "Skift adgangskode",
|
||||
"to_favorite": "Gør til favorit",
|
||||
"to_login": "Login",
|
||||
"to_parent": "Gå op",
|
||||
"to_trash": "Papirkurv",
|
||||
"toggle_settings": "Slå indstillinger til eller fra",
|
||||
"toggle_theme": "Slå mørkt tema til eller fra",
|
||||
|
@ -1334,7 +1342,7 @@
|
|||
"warning": "Advarsel",
|
||||
"week": "Uge",
|
||||
"welcome": "Velkommen",
|
||||
"welcome_to_immich": "Velkommen til immich",
|
||||
"welcome_to_immich": "Velkommen til Immich",
|
||||
"year": "År",
|
||||
"years_ago": "{years, plural, one {# år} other {# år}} siden",
|
||||
"yes": "Ja",
|
||||
|
|
28
i18n/de.json
28
i18n/de.json
|
@ -34,7 +34,7 @@
|
|||
"authentication_settings_description": "Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen verwalten",
|
||||
"authentication_settings_disable_all": "Bist du sicher, dass du alle Anmeldemethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.",
|
||||
"authentication_settings_reenable": "Nutze einen <link>Server-Befehl</link> zur Reaktivierung.",
|
||||
"background_task_job": "Hintergrund-Aufgaben",
|
||||
"background_task_job": "Hintergrundaufgaben",
|
||||
"backup_database": "Datenbank sichern",
|
||||
"backup_database_enable_description": "Sicherung der Datenbank aktivieren",
|
||||
"backup_keep_last_amount": "Anzahl der aufzubewahrenden früheren Sicherungen",
|
||||
|
@ -83,9 +83,9 @@
|
|||
"job_concurrency": "{job} (Anzahl gleichzeitiger Prozesse)",
|
||||
"job_created": "Aufgabe erstellt",
|
||||
"job_not_concurrency_safe": "Diese Aufgabe ist nicht parallelisierungssicher.",
|
||||
"job_settings": "Aufgaben-Einstellungen",
|
||||
"job_settings_description": "Gleichzeitige Aufgaben-Prozesse verwalten",
|
||||
"job_status": "Aufgaben-Status",
|
||||
"job_settings": "Aufgabeneinstellungen",
|
||||
"job_settings_description": "Die gleichzeitige Ausführung von Aufgaben verwalten",
|
||||
"job_status": "Aufgabenstatus",
|
||||
"jobs_delayed": "{jobCount, plural, other {# verzögert}}",
|
||||
"jobs_failed": "{jobCount, plural, other {# fehlgeschlagen}}",
|
||||
"library_created": "Bibliothek erstellt: {library}",
|
||||
|
@ -211,7 +211,7 @@
|
|||
"quota_size_gib": "Kontingent (GiB)",
|
||||
"refreshing_all_libraries": "Alle Bibliotheken aktualisieren",
|
||||
"registration": "Admin-Registrierung",
|
||||
"registration_description": "Da du der erste Benutzer im System bist, wirst du als Admin zugewiesen und bist für administrative Aufgaben zuständig. Weitere Benutzer werden von dir erstellt.",
|
||||
"registration_description": "Da du der erste Benutzer im System bist, wird dir die Rolle des Administrators zugewiesen, womit du für die Verwaltungsaufgaben verantwortlich bist. Weitere Benutzer werden von dir erstellt.",
|
||||
"repair_all": "Alle reparieren",
|
||||
"repair_matched_items": "{count, plural, one {# Eintrag} other {# Einträge}} gefunden",
|
||||
"repaired_items": "{count, plural, one {# Eintrag} other {# Einträge}} repariert",
|
||||
|
@ -287,7 +287,7 @@
|
|||
"transcoding_constant_quality_mode": "Modus für konstante Qualität",
|
||||
"transcoding_constant_quality_mode_description": "ICQ ist besser als CQP, aber einige Hardware-Beschleunigungsgeräte unterstützen diesen Modus nicht. Wenn diese Option gesetzt wird, wird der angegebene Modus bevorzugt, sobald qualitätsbasierte Kodierung verwendet wird. Wird von NVENC ignoriert, da es ICQ nicht unterstützt.",
|
||||
"transcoding_constant_rate_factor": "Faktor der konstanten Rate (-crf)",
|
||||
"transcoding_constant_rate_factor_description": "Video-Qualitätsstufe. Typische Werte sind 23 für H.264, 28 für HEVC, 31 für VP9 und 35 für AV1. Ein niedrigerer Wert ist besser, erzeugt aber größere Dateien.",
|
||||
"transcoding_constant_rate_factor_description": "Videoqualitätsstufe. Typische Werte sind 23 für H.264, 28 für HEVC, 31 für VP9 und 35 für AV1. Ein niedrigerer Wert ist besser, erzeugt aber größere Dateien.",
|
||||
"transcoding_disabled_description": "Videos nicht transkodieren, dies kann die Wiedergabe auf manchen Geräten beeinträchtigen",
|
||||
"transcoding_encoding_options": "Kodierungsoptionen",
|
||||
"transcoding_encoding_options_description": "Setze Codec, Auflösung, Qualität und andere Optionen für kodierte Videos",
|
||||
|
@ -312,14 +312,14 @@
|
|||
"transcoding_reference_frames": "Referenz-Frames",
|
||||
"transcoding_reference_frames_description": "Die Anzahl der Bilder, auf die bei der Komprimierung eines bestimmten Bildes Bezug genommen wird. Höhere Werte verbessern die Komprimierungseffizienz, verlangsamen aber die Kodierung. 0 setzt diesen Wert automatisch.",
|
||||
"transcoding_required_description": "Nur Videos in einem nicht akzeptierten Format",
|
||||
"transcoding_settings": "Video-Transkodierungseinstellungen",
|
||||
"transcoding_settings": "Einstellungen für die Videotranskodierung",
|
||||
"transcoding_settings_description": "Verwalten welche Videos transkodiert werden und wie diese verarbeitet werden",
|
||||
"transcoding_target_resolution": "Ziel-Auflösung",
|
||||
"transcoding_target_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Codierung, haben größere Dateigrößen und können die Reaktionszeit der Anwendung beeinträchtigen.",
|
||||
"transcoding_temporal_aq": "Temporäre AQ",
|
||||
"transcoding_temporal_aq_description": "Gilt nur für NVENC. Verbessert die Qualität von Szenen mit hohem Detailreichtum und geringen Bewegungen. Dies ist möglicherweise nicht mit älteren Geräten kompatibel.",
|
||||
"transcoding_threads": "Threads",
|
||||
"transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Codierung, lassen dem Server aber weniger Spielraum für die Verarbeitung anderer Aufgaben, solange dies aktiv ist. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Nutzt die maximale Auslastung, wenn der Wert auf 0 gesetzt ist.",
|
||||
"transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Kodierung, lassen dem Server jedoch weniger Spielraum für die Verarbeitung anderer Aufgaben im aktiven Zustand. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Maximiert die Auslastung, wenn der Wert auf 0 gesetzt wird.",
|
||||
"transcoding_tone_mapping": "Farbton-Mapping",
|
||||
"transcoding_tone_mapping_description": "Versucht, das Aussehen von HDR-Videos bei der Konvertierung in SDR beizubehalten. Jeder Algorithmus geht unterschiedliche Kompromisse bei Farbe, Details und Helligkeit ein. Hable bewahrt Details, Mobius bewahrt die Farbe und Reinhard bewahrt die Helligkeit.",
|
||||
"transcoding_transcode_policy": "Transcodierungsrichtlinie",
|
||||
|
@ -328,11 +328,11 @@
|
|||
"transcoding_two_pass_encoding_setting_description": "Führt eine Transkodierung in zwei Durchgängen durch, um besser kodierte Videos zu erzeugen. Wenn die maximale Bitrate aktiviert ist (erforderlich für die Verwendung mit H.264 und HEVC), verwendet dieser Modus einen Bitratenbereich, der auf der maximalen Bitrate basiert, und ignoriert CRF. Für VP9 kann CRF verwendet werden, wenn die maximale Bitrate deaktiviert ist.",
|
||||
"transcoding_video_codec": "Video-Codec",
|
||||
"transcoding_video_codec_description": "VP9 hat eine hohe Effizienz und Webkompatibilität, braucht aber länger für die Transkodierung. HEVC bietet eine ähnliche Leistung, ist aber weniger web-kompatibel. H.264 ist weitgehend kompatibel und lässt sich schnell transkodieren, erzeugt aber viel größere Dateien. AV1 ist der effizienteste Codec, wird aber von älteren Geräten nicht unterstützt.",
|
||||
"trash_enabled_description": "Papierkorb-Funktionen aktivieren",
|
||||
"trash_enabled_description": "Papierkorbfunktionen aktivieren",
|
||||
"trash_number_of_days": "Anzahl der Tage",
|
||||
"trash_number_of_days_description": "Anzahl der Tage, welche die Objekte im Papierkorb verbleiben, bevor sie endgültig entfernt werden",
|
||||
"trash_settings": "Papierkorb-Einstellungen",
|
||||
"trash_settings_description": "Papierkorb-Einstellungen verwalten",
|
||||
"trash_settings": "Papierkorbeinstellungen",
|
||||
"trash_settings_description": "Papierkorbeinstellungen verwalten",
|
||||
"untracked_files": "Unverfolgte Dateien",
|
||||
"untracked_files_description": "Diese Dateien werden nicht von der Anwendung getrackt. Sie können das Ergebnis fehlgeschlagener Verschiebungen, unterbrochener Uploads oder aufgrund eines Fehlers sein",
|
||||
"user_cleanup_job": "Benutzer aufräumen",
|
||||
|
@ -346,8 +346,8 @@
|
|||
"user_password_reset_description": "Bitte gib dem Benutzer das temporäre Passwort und informiere ihn, dass das Passwort beim nächsten Login geändert werden muss.",
|
||||
"user_restore_description": "Das Konto von <b>{user}</b> wird wiederhergestellt.",
|
||||
"user_restore_scheduled_removal": "Wiederherstellung des Benutzers - geplante Entfernung am {date, date, long}",
|
||||
"user_settings": "Benutzer-Einstellungen",
|
||||
"user_settings_description": "Benutzer-Einstellungen verwalten",
|
||||
"user_settings": "Benutzereinstellungen",
|
||||
"user_settings_description": "Benutzereinstellungen verwalten",
|
||||
"user_successfully_removed": "Benutzer {email} wurde erfolgreich entfernt.",
|
||||
"version_check_enabled_description": "Versionsprüfung aktivieren",
|
||||
"version_check_implications": "Die Funktion zur Versionsprüfung basiert auf regelmäßiger Kommunikation mit GitHub.com",
|
||||
|
@ -1324,7 +1324,7 @@
|
|||
"version_history_item": "{version} am {date} installiert",
|
||||
"video": "Video",
|
||||
"video_hover_setting": "Videovorschau beim Hovern abspielen",
|
||||
"video_hover_setting_description": "Video-Miniaturansicht wiedergeben, wenn der Mauszeiger über dem Element verweilt. Auch wenn diese Funktion deaktiviert ist, kann die Wiedergabe gestartet werden, indem der Mauszeiger auf das Wiedergabesymbol bewegt wird.",
|
||||
"video_hover_setting_description": "Spiele die Miniaturansicht des Videos ab, wenn sich die Maus über dem Element befindet. Auch wenn die Funktion deaktiviert ist, kann die Wiedergabe gestartet werden, indem du mit der Maus über das Wiedergabesymbol fährst.",
|
||||
"videos": "Videos",
|
||||
"videos_count": "{count, plural, one {# Video} other {# Videos}}",
|
||||
"view": "Ansicht",
|
||||
|
|
19
i18n/fi.json
19
i18n/fi.json
|
@ -182,7 +182,7 @@
|
|||
"oauth_auto_register_description": "Rekisteröi uudet OAuth:lla kirjautuvat käyttäjät automaattisesti",
|
||||
"oauth_button_text": "Painikkeen teksti",
|
||||
"oauth_client_id": "Client ID",
|
||||
"oauth_client_secret": "Client Secret",
|
||||
"oauth_client_secret": "Asiakassalaisuusavain",
|
||||
"oauth_enable_description": "Kirjaudu käyttäen OAuthia",
|
||||
"oauth_issuer_url": "Toimitsijan URL",
|
||||
"oauth_mobile_redirect_uri": "Mobiilin uudellenohjaus-URI",
|
||||
|
@ -289,11 +289,13 @@
|
|||
"transcoding_constant_rate_factor": "Vakionopeustekijä",
|
||||
"transcoding_constant_rate_factor_description": "Videon laatu. Yleisimmät arvot ovat 23 H.264:lle, 28 HEVC:lle, 31 VP9:lle ja 35 AV1:lle. Matalampi arvo on parempi, mutta tekee isompia tiedostoja.",
|
||||
"transcoding_disabled_description": "Älä muunna videoita. Voi joissakin päätelaitteissa aiheuttaa videotoiston toimimattomuutta",
|
||||
"transcoding_encoding_options": "Enkoodausasetukset",
|
||||
"transcoding_encoding_options_description": "Aseta koodekit, tarkkuus, laatu ja muut asetukset enkoodatuille videoille",
|
||||
"transcoding_hardware_acceleration": "Laitteistokiihdytys",
|
||||
"transcoding_hardware_acceleration_description": "Kokeellinen. Paljon nopeampi, mutta huonompaa laatua samalla bittinopeudella",
|
||||
"transcoding_hardware_decoding": "Laitteiston dekoodaus",
|
||||
"transcoding_hardware_decoding_setting_description": "Ottaa käyttöön end-to-end kiihdytyksen pelkän muuntamisen sijasta. Ei välttämättä toimi kaikissa videoissa.",
|
||||
"transcoding_hevc_codec": "HEVC koodekki",
|
||||
"transcoding_hevc_codec": "HEVC-koodekki",
|
||||
"transcoding_max_b_frames": "B-kehysten enimmäismäärä",
|
||||
"transcoding_max_b_frames_description": "Korkeampi arvo parantaa pakkausta, mutta hidastaa enkoodausta. Ei välttämättä ole yhteensopiva vanhempien laitteiden kanssa. 0 poistaa B-kehykset käytöstä, -1 määrittää arvon automaattisesti.",
|
||||
"transcoding_max_bitrate": "Suurin bittinopeus",
|
||||
|
@ -301,6 +303,8 @@
|
|||
"transcoding_max_keyframe_interval": "Suurin avainkehysten väli",
|
||||
"transcoding_max_keyframe_interval_description": "Asettaa avainkehysten välin maksimiarvon. Alempi arvo huonontaa pakkauksen tehoa, mutta parantaa hakuaikoja ja voi parantaa laatua nopealiikkeisissä kohtauksissa. 0 asettaa arvon automaattisesti.",
|
||||
"transcoding_optimal_description": "Videot, joiden resoluutio on korkeampi kuin kohteen, tai ei hyväksytyssä formaatissa",
|
||||
"transcoding_policy": "Transkoodauskäytäntö",
|
||||
"transcoding_policy_description": "Aseta milloin video transkoodataan",
|
||||
"transcoding_preferred_hardware_device": "Ensisijainen laite",
|
||||
"transcoding_preferred_hardware_device_description": "On voimassa vain VAAPI ja QSV -määritteille. Asettaa laitteistokoodauksessa käytetyn DRI noodin.",
|
||||
"transcoding_preset_preset": "Esiasetus (-asetus)",
|
||||
|
@ -309,7 +313,7 @@
|
|||
"transcoding_reference_frames_description": "Viittaavien kehysten määrä kun tiettyä kehystä pakataan. Korkeampi arvo parantaa pakkausta mutta hidastaa enkoodausta. 0 määrittää arvon automaattisesti.",
|
||||
"transcoding_required_description": "Vain videoille, jotka eivät ole hyväksytyssä muodossa",
|
||||
"transcoding_settings": "Videoiden transkoodausasetukset",
|
||||
"transcoding_settings_description": "Hallitse videoiden resoluutiota ja koodaustietueita",
|
||||
"transcoding_settings_description": "Hallitse, mitkä videot transkoodataan ja miten niitä käsitellään",
|
||||
"transcoding_target_resolution": "Kohderesoluutio",
|
||||
"transcoding_target_resolution_description": "Korkeampi resoluutio on tarkempi, mutta kestää kauemmin enkoodata, vie enemmän tilaa ja voi hidastaa sovelluksen responsiivisuutta.",
|
||||
"transcoding_temporal_aq": "Temporal AQ",
|
||||
|
@ -519,6 +523,10 @@
|
|||
"date_range": "Päivämäärän rajaus",
|
||||
"day": "Päivä",
|
||||
"deduplicate_all": "Poista kaikkien kaksoiskappaleet",
|
||||
"deduplication_criteria_1": "Kuvan koko tavuina",
|
||||
"deduplication_criteria_2": "EXIF-datan määrä",
|
||||
"deduplication_info": "Deduplikaatiotieto",
|
||||
"deduplication_info_description": "Jotta voimme automaattisesti esivalita aineistot ja poistaa duplikaatit suurina erinä, tarkastelemme:",
|
||||
"default_locale": "Oletuskieliasetus",
|
||||
"default_locale_description": "Muotoile päivämäärät ja numerot selaimesi kielen mukaan",
|
||||
"delete": "Poista",
|
||||
|
@ -532,7 +540,7 @@
|
|||
"delete_shared_link": "Poista jaettu linkki",
|
||||
"delete_tag": "Poista tunniste",
|
||||
"delete_tag_confirmation_prompt": "Haluatko varmasti poistaa tunnisteen {tagName}?",
|
||||
"delete_user": "Poista käyttäjä",
|
||||
"delete_user": "Poista käyttäjä pysyvästi",
|
||||
"deleted_shared_link": "Jaettu linkki poistettu",
|
||||
"deletes_missing_assets": "Poistaa levyltä puuttuvat resurssit",
|
||||
"description": "Kuvaus",
|
||||
|
@ -755,6 +763,7 @@
|
|||
"get_help": "Hae apua",
|
||||
"getting_started": "Aloittaminen",
|
||||
"go_back": "Palaa",
|
||||
"go_to_folder": "Mene kansioon",
|
||||
"go_to_search": "Siirry hakuun",
|
||||
"group_albums_by": "Ryhmitä albumi...",
|
||||
"group_no": "Ei ryhmitystä",
|
||||
|
@ -1141,6 +1150,7 @@
|
|||
"server_version": "Palvelimen versio",
|
||||
"set": "Aseta",
|
||||
"set_as_album_cover": "Aseta albumin kanneksi",
|
||||
"set_as_featured_photo": "Käytä esittelykuvana",
|
||||
"set_as_profile_picture": "Aseta profiilikuvaksi",
|
||||
"set_date_of_birth": "Aseta syntymäaika",
|
||||
"set_profile_picture": "Aseta profiilikuva",
|
||||
|
@ -1196,6 +1206,7 @@
|
|||
"sort_items": "Tietueiden määrä",
|
||||
"sort_modified": "Muokkauspäivä",
|
||||
"sort_oldest": "Vanhin kuva",
|
||||
"sort_people_by_similarity": "Lajittele ihmiset samankaltaisuuden mukaan",
|
||||
"sort_recent": "Tuorein kuva",
|
||||
"sort_title": "Otsikko",
|
||||
"source": "Lähdekoodi",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"about": "I-refresh",
|
||||
"about": "Tungkol sa app na ito",
|
||||
"account": "Account",
|
||||
"account_settings": "Mga Setting ng Account",
|
||||
"acknowledge": "Tanggapin",
|
||||
|
@ -24,9 +24,15 @@
|
|||
"added_to_favorites_count": "Idinagdag ang {count, number} sa mga paborito",
|
||||
"admin": {
|
||||
"asset_offline_description": "Ang external library asset na ito ay hindi na makikita sa disk at nailipat na sa trash. Kung ang file ay nailipat sa loob ng library, tignan ang iyong timeline para sa kaukulang asset. Para maibalik ang asset na ito, siguraduhin na ang file path ay maa-access ng Immich para iscan ang library.",
|
||||
"authentication_settings_disable_all": "Sigurado ka bang gusto mo patayin lahat ng paraan ng pag-login? Ang pag-login ay ganap na idi-disable.",
|
||||
"authentication_settings_reenable": "Para i-enable ulit, gamitin ang <link>Server Command</link>.",
|
||||
"cleared_jobs": "Lahat nang mga trabaho para sa {job} ay tinanggal na",
|
||||
"confirm_delete_library": "Sigurado ka na gusto mo tanggalin ang {library} library?",
|
||||
"confirm_email_below": "Para isigurado, i-type ito sa baba: \"{email}\"",
|
||||
"confirm_user_password_reset": "Sigurado ka na gusto mo i-reset ang password ni {user}?",
|
||||
"disable_login": "I-disable ang login",
|
||||
"force_delete_user_warning": "BABALA:",
|
||||
"force_delete_user_warning": "BABALA: Tatanggalin itong user at lahat ng asset nila, Hindi ito mababawi at ang kanilang files ay hindi na mababalik",
|
||||
"image_format": "Format",
|
||||
"library_import_path_description": "Tukuyin ang folder na i-import. Ang folder na ito, kasama ang subfolders, ay mag sa-scan para sa mga imahe at mga videos.",
|
||||
"note_cannot_be_changed_later": "TANDAAN: Hindi na ito pwede baguhin sa susunod!",
|
||||
"repair_all": "Ayusin lahat",
|
||||
|
@ -40,5 +46,22 @@
|
|||
"are_these_the_same_person": "Itong tao na ito ay parehas?",
|
||||
"asset_adding_to_album": "Dinadagdag sa album...",
|
||||
"asset_filename_is_offline": "Offline ang asset {filename}",
|
||||
"asset_uploading": "Ina-upload..."
|
||||
"asset_uploading": "Ina-upload...",
|
||||
"discord": "Discord",
|
||||
"documentation": "Dokumentasyion",
|
||||
"done": "Tapos na",
|
||||
"download": "I-download",
|
||||
"edit": "I-edit",
|
||||
"edited": "Inedit",
|
||||
"editor_close_without_save_title": "Isara ang editor?",
|
||||
"email": "Email",
|
||||
"exif": "Exif",
|
||||
"explore": "I-explore",
|
||||
"export": "I-export",
|
||||
"has_quota": "May quota",
|
||||
"hour": "Oras",
|
||||
"jobs": "Mga trabaho",
|
||||
"language": "Wika",
|
||||
"leave": "Umalis",
|
||||
"no_results": "Walang resulta"
|
||||
}
|
||||
|
|
30
i18n/he.json
30
i18n/he.json
|
@ -529,20 +529,20 @@
|
|||
"deduplication_info_description": "כדי לבחור מראש נכסים באופן אוטומטי ולהסיר כפילויות בכמות גדולה, אנו מסתכלים על:",
|
||||
"default_locale": "שפת ברירת מחדל",
|
||||
"default_locale_description": "פורמט תאריכים ומספרים מבוסס שפת הדפדפן שלך",
|
||||
"delete": "הסרה",
|
||||
"delete_album": "הסרת אלבום",
|
||||
"delete_api_key_prompt": "האם ברצונך למחוק מפתח ה-API הזה?",
|
||||
"delete_duplicates_confirmation": "האם ברצונך להסיר לצמיתות את הכפילויות האלה?",
|
||||
"delete_key": "הסרת מפתח",
|
||||
"delete_library": "הסרת ספרייה",
|
||||
"delete_link": "הסרת קישור",
|
||||
"delete_others": "הסרת אחרים",
|
||||
"delete_shared_link": "הסרת קישור משותף",
|
||||
"delete_tag": "הסרת תג",
|
||||
"delete_tag_confirmation_prompt": "האם ברצונך להסיר תג {tagName}?",
|
||||
"delete_user": "הסרת משתמש",
|
||||
"deleted_shared_link": "קישור משותף הוסר",
|
||||
"deletes_missing_assets": "מסיר נכסים שחסרים בדיסק",
|
||||
"delete": "מחק",
|
||||
"delete_album": "מחק אלבום",
|
||||
"delete_api_key_prompt": "האם את/ה בטוח/ה שברצונך למחוק מפתח ה-API הזה?",
|
||||
"delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק לצמיתות את הכפילויות האלה?",
|
||||
"delete_key": "מחק מפתח",
|
||||
"delete_library": "מחק ספרייה",
|
||||
"delete_link": "מחק קישור",
|
||||
"delete_others": "מחק אחרים",
|
||||
"delete_shared_link": "מחק קישור משותף",
|
||||
"delete_tag": "מחק תג",
|
||||
"delete_tag_confirmation_prompt": "האם את/ה בטוח/ה שברצונך למחוק תג {tagName}?",
|
||||
"delete_user": "מחק משתמש",
|
||||
"deleted_shared_link": "קישור משותף נמחק",
|
||||
"deletes_missing_assets": "מוחק נכסים שחסרים בדיסק",
|
||||
"description": "תיאור",
|
||||
"details": "פרטים",
|
||||
"direction": "כיוון",
|
||||
|
@ -553,7 +553,7 @@
|
|||
"dismiss_all_errors": "התעלמות מכל השגיאות",
|
||||
"dismiss_error": "התעלמות מהשגיאה",
|
||||
"display_options": "הצגת אפשרויות",
|
||||
"display_order": "סידור תצוגה",
|
||||
"display_order": "סדר תצוגה",
|
||||
"display_original_photos": "הצגת תמונות מקוריות",
|
||||
"display_original_photos_setting_description": "העדף להציג את התמונה המקורית בעת צפיית נכס במקום תמונות ממוזערות כאשר הנכס המקורי תומך בתצוגה בדפדפן. זה עלול לגרום לתמונות להיות מוצגות באיטיות.",
|
||||
"do_not_show_again": "אל תציג את ההודעה הזאת שוב",
|
||||
|
|
65
i18n/lt.json
65
i18n/lt.json
|
@ -30,7 +30,7 @@
|
|||
"admin": {
|
||||
"asset_offline_description": "Šis išorinės bibliotekos elementas nebepasiekiamas diske ir buvo perkeltas į šiukšliadėžę. Jei failas buvo perkeltas toje pačioje bibliotekoje, laiko skalėje rasite naują atitinkamą elementą. Jei norite šį elementą atkurti, įsitikinkite, kad Immich gali pasiekti failą žemiau nurodytu adresu, ir suvykdykite bibliotekos skanavimą.",
|
||||
"authentication_settings": "Autentifikavimo nustatymai",
|
||||
"authentication_settings_description": "Tvarkyti slaptažodžių, OAuth ir kitus autentifikavimo parametrus",
|
||||
"authentication_settings_description": "Tvarkyti slaptažodžių, OAuth ir kitus autentifikavimo nustatymus",
|
||||
"authentication_settings_disable_all": "Ar tikrai norite išjungti visus prisijungimo būdus? Prisijungimas bus visiškai išjungtas.",
|
||||
"authentication_settings_reenable": "Norėdami vėl įjungti, naudokite <link>Serverio komandą</link>.",
|
||||
"background_task_job": "Foninės užduotys",
|
||||
|
@ -38,14 +38,20 @@
|
|||
"backup_database_enable_description": "Įgalinti duomenų bazės atsarginė kopijas",
|
||||
"backup_keep_last_amount": "Išsaugomų ankstesnių atsarginių duomenų bazės kopijų skaičius",
|
||||
"backup_settings": "Atsarginės kopijos nustatymai",
|
||||
"backup_settings_description": "Tvarkyti duomenų bazės atsarginės kopijos nustatymus",
|
||||
"check_all": "Pažymėti viską",
|
||||
"config_set_by_file": "Konfigūracija dabar nustatyta konfigūracinio failo",
|
||||
"cleared_jobs": "Išvalyti darbai: {job}",
|
||||
"config_set_by_file": "Konfigūracija nustatyta pagal konfigūracinį failą",
|
||||
"confirm_delete_library": "Ar tikrai norite ištrinti {library} biblioteką?",
|
||||
"confirm_delete_library_assets": "Ar tikrai norite ištrinti šią biblioteką? Šis veiksmas ištrins {count, plural, one {# contained asset} other {all # contained assets}} iš Immich ir negali būti grąžintas. Failai liks diske.",
|
||||
"confirm_email_below": "Patvirtinimui įveskite \"{email}\" žemiau",
|
||||
"confirm_reprocess_all_faces": "Ar tikrai norite iš naujo apdoroti visus veidus? Tai taip pat ištrins įvardytus asmenis.",
|
||||
"confirm_user_password_reset": "Ar tikrai norite iš naujo nustatyti {user} slaptažodį?",
|
||||
"create_job": "Sukurti darbą",
|
||||
"cron_expression": "Cron išraiška",
|
||||
"cron_expression_description": "Nustatyti skanavimo intervalą naudojant cron formatą. Norėdami gauti daugiau informacijos žiūrėkite <link>Crontab Guru</link>",
|
||||
"disable_login": "Išjungti prisijungimą",
|
||||
"duplicate_detection_job_description": "Vykdykite mašininį mokymąsi panašių vaizdų aptikimui. Priklauso nuo išmaniosios paieškos.",
|
||||
"duplicate_detection_job_description": "Vykdykite mašininį mokymąsi panašių vaizdų aptikimui. Priklauso nuo išmaniosios paieškos",
|
||||
"exclusion_pattern_description": "Išimčių šablonai leidžia nepaisyti failų ir aplankų skenuojant jūsų biblioteką. Tai yra naudinga, jei turite aplankų su failais, kurių nenorite importuoti, pavyzdžiui, RAW failai.",
|
||||
"external_library_created_at": "Išorinė biblioteka (sukurta {date})",
|
||||
"external_library_management": "Išorinių bibliotekų tvarkymas",
|
||||
|
@ -61,11 +67,19 @@
|
|||
"image_prefer_embedded_preview_setting_description": "",
|
||||
"image_prefer_wide_gamut": "Teikti pirmenybę plačiai gamai",
|
||||
"image_prefer_wide_gamut_setting_description": "",
|
||||
"image_preview_description": "Vidutinio dydžio vaizdas su išvalytais metaduomenimis, naudojamas kai žiūrimas vienas objektas arba mašininiam mokymuisi",
|
||||
"image_preview_quality_description": "Peržiūros kokybė nuo 1-100. Aukštesnės reikšmės yra geriau, bet sukuriami didesni failai gali sumažinti programos reagavimo laiką. Mažos vertės nustatymas gali paveikti mašininio mokymo kokybę.",
|
||||
"image_preview_title": "Peržiūros nustatymai",
|
||||
"image_quality": "Kokybė",
|
||||
"image_resolution": "Rezoliucija",
|
||||
"image_resolution_description": "Didesnės rezoliucijos gali išsaugoti daugiau detalių, bet ilgiau užtrunka užkoduoti, failai yra didesni ir programos reagavimo laikas gali sumažėti.",
|
||||
"image_settings": "Nuotraukos nustatymai",
|
||||
"image_settings_description": "Keisti sugeneruotų nuotraukų kokybę ir rezoliuciją",
|
||||
"image_thumbnail_description": "Maža miniatiūra su išvalytais metaduomenimis, naudojama kai žiūrima nuotraukų grupės, kaip pagrindinėje laiko juostoje",
|
||||
"image_thumbnail_quality_description": "Miniatiūros kokybė nuo 1-100. Aukštesnės reikšmės yra geriau, bet pagaminami didesni failai ir gali būti sulėtintas programos reagavimo greitis.",
|
||||
"image_thumbnail_title": "Miniatiūros nustatymai",
|
||||
"job_concurrency": "{job} lygiagretumas",
|
||||
"job_created": "Darbas sukurtas",
|
||||
"job_not_concurrency_safe": "Šis darbas nėra saugus apdoroti lygiagrečiai.",
|
||||
"job_settings": "Darbų nustatymai",
|
||||
"job_settings_description": "Keisti darbų lygiagretumą",
|
||||
|
@ -79,17 +93,17 @@
|
|||
"library_settings": "Išorinė biblioteka",
|
||||
"library_settings_description": "Tvarkyti išorinės bibliotekos parametrus",
|
||||
"library_tasks_description": "Atlikit bibliotekos užduotis",
|
||||
"library_watching_enable_description": "",
|
||||
"library_watching_settings": "",
|
||||
"library_watching_settings_description": "",
|
||||
"logging_enable_description": "",
|
||||
"logging_level_description": "",
|
||||
"logging_settings": "",
|
||||
"library_watching_enable_description": "Stebėti išorines bibliotekas dėl failų pakeitimų",
|
||||
"library_watching_settings": "Bibliotekų stebėjimas (EKSPERIMENTINIS)",
|
||||
"library_watching_settings_description": "Automatiškai stebėti dėl pakeistų failų",
|
||||
"logging_enable_description": "Įjungti žurnalo vedimą",
|
||||
"logging_level_description": "Įjungus, kokį žurnalo vedimo lygį naudot.",
|
||||
"logging_settings": "Žurnalo vedimas",
|
||||
"machine_learning_clip_model": "CLIP modelis",
|
||||
"machine_learning_duplicate_detection": "Dublikatų aptikimas",
|
||||
"machine_learning_duplicate_detection_enabled": "Įjungti dublikatų aptikimą",
|
||||
"machine_learning_duplicate_detection_enabled_description": "",
|
||||
"machine_learning_duplicate_detection_setting_description": "",
|
||||
"machine_learning_duplicate_detection_setting_description": "Naudoti CLIP įterpimus, norint rasti galimus duplikatus",
|
||||
"machine_learning_enabled": "Įgalinti mašininį mokymąsi",
|
||||
"machine_learning_enabled_description": "Jei išjungta, visos „ML“ funkcijos bus išjungtos, nepaisant toliau pateiktų nustatymų.",
|
||||
"machine_learning_facial_recognition": "Veidų atpažinimas",
|
||||
|
@ -97,28 +111,29 @@
|
|||
"machine_learning_facial_recognition_model": "Veidų atpažinimo modelis",
|
||||
"machine_learning_facial_recognition_model_description": "",
|
||||
"machine_learning_facial_recognition_setting": "Įgalinti veidų atpažinimą",
|
||||
"machine_learning_facial_recognition_setting_description": "",
|
||||
"machine_learning_facial_recognition_setting_description": "Išjungus, vaizdai nebus užšifruoti veidų atpažinimui ir nebus naudojami Žmonių sekcijoje Naršymo puslapyje.",
|
||||
"machine_learning_max_detection_distance": "Maksimalus aptikimo atstumas",
|
||||
"machine_learning_max_detection_distance_description": "Didžiausias atstumas tarp dviejų vaizdų, kad jie būtų laikomi dublikatais, svyruoja nuo 0,001 iki 0,1. Didesnės vertės aptiks daugiau dublikatų, tačiau gali būti klaidingai teigiami.",
|
||||
"machine_learning_max_recognition_distance": "Maksimalus atpažinimo atstumas",
|
||||
"machine_learning_max_recognition_distance_description": "",
|
||||
"machine_learning_min_detection_score": "",
|
||||
"machine_learning_min_detection_score": "Minimalus aptikimo balas",
|
||||
"machine_learning_min_detection_score_description": "",
|
||||
"machine_learning_min_recognized_faces": "Mažiausias atpažintų veidų skaičius",
|
||||
"machine_learning_min_recognized_faces_description": "Mažiausias atpažintų veidų skaičius asmeniui, kurį reikia sukurti. Tai padidinus, veido atpažinimas tampa tikslesnis, bet padidėja tikimybė, kad veidas žmogui nepriskirtas.",
|
||||
"machine_learning_settings": "Mašininio mokymosi nustatymai",
|
||||
"machine_learning_settings_description": "Tvarkyti mašininio mokymosi funkcijas ir nustatymus",
|
||||
"machine_learning_smart_search": "Išmanioji paieška",
|
||||
"machine_learning_smart_search_description": "",
|
||||
"machine_learning_smart_search_description": "Semantiškai ieškoti vaizdų naudojant CLIP įtarpius",
|
||||
"machine_learning_smart_search_enabled": "Įjungti išmaniąją paiešką",
|
||||
"machine_learning_smart_search_enabled_description": "Jei išjungta, vaizdai nebus užkoduoti išmaniajai paieškai.",
|
||||
"machine_learning_url_description": "Mašininio mokymosi serverio URL. Jei pateikta daugiau nei vienas URL, serveriai bus bandomi eilės tvarka nuo pirmo iki paskutinio tol, kol bus rastas vienas veikiantis serveris.",
|
||||
"manage_concurrency": "Tvarkyti lygiagretumą",
|
||||
"manage_log_settings": "",
|
||||
"manage_log_settings": "Valdyti žurnalo nuostatas",
|
||||
"map_dark_style": "Tamsioji tema",
|
||||
"map_enable_description": "Įgalinti žemėlapio funkcijas",
|
||||
"map_gps_settings": "Žemėlapio ir GPS nustatymai",
|
||||
"map_gps_settings_description": "Tvarkyti žemėlapio ir GPS (atvirkštinio geokodavimo) nustatymus",
|
||||
"map_implications": "Žemėlapio funkcija naudojasi išoriniu plytelių servisu (tiles.immich.cloud)",
|
||||
"map_light_style": "Šviesioji tema",
|
||||
"map_manage_reverse_geocoding_settings": "Tvarkyti <link>atvirkštinio geokodavimo</link> nustatymus",
|
||||
"map_reverse_geocoding": "Atvirkštinis geokodavimas",
|
||||
|
@ -126,29 +141,33 @@
|
|||
"map_reverse_geocoding_settings": "Atvirkštinio geokodavimo nustatymai",
|
||||
"map_settings": "Žemėlapis",
|
||||
"map_settings_description": "Tvarkyti žemėlapio parametrus",
|
||||
"map_style_description": "",
|
||||
"map_style_description": "URL į style.json žemėlapio temą",
|
||||
"metadata_extraction_job": "Metaduomenų nuskaitymas",
|
||||
"metadata_extraction_job_description": "Kiekvieno bibliotekos elemento metaduomenų nuskaitymas, tokių kaip GPS koordinatės, veidai ar rezoliucija",
|
||||
"metadata_faces_import_setting": "Įjungti veidų importą",
|
||||
"metadata_faces_import_setting_description": "Importuoti veidus iš vaizdo EXIF duomenų ir papildomų failų",
|
||||
"metadata_settings": "Metaduomenų nustatymai",
|
||||
"metadata_settings_description": "Tvarkyti metaduomenų nustatymus",
|
||||
"migration_job": "Migracija",
|
||||
"migration_job_description": "",
|
||||
"no_paths_added": "Keliai nepridėti",
|
||||
"no_pattern_added": "Šablonas nepridėtas",
|
||||
"note_apply_storage_label_previous_assets": "Pastaba: norėdami pritaikyti saugyklos etiketę seniau įkeltiems ištekliams, paleiskite",
|
||||
"note_cannot_be_changed_later": "PASTABA: Vėliau to pakeisti negalima!",
|
||||
"notification_email_from_address": "",
|
||||
"notification_email_from_address_description": "",
|
||||
"notification_email_host_description": "",
|
||||
"note_unlimited_quota": "Pastaba: įveskite 0 norint neribotos kvotos",
|
||||
"notification_email_from_address": "Iš adreso",
|
||||
"notification_email_from_address_description": "Siuntėjo elektroninis adresas, pavyzdžiui: \"Immich Photo Server <noreply@example.com>\"",
|
||||
"notification_email_host_description": "Elektroninio pašto serverio savininkas (pvz. smtp.immich.app)",
|
||||
"notification_email_ignore_certificate_errors": "Nepaisyti sertifikatų klaidų",
|
||||
"notification_email_ignore_certificate_errors_description": "Nepaisyti TLS sertifikato patvirtinimo klaidų (nerekomenduojama)",
|
||||
"notification_email_password_description": "",
|
||||
"notification_email_password_description": "Slaptažodis, naudojant autentikacijai su elektroninio pašto serveriu",
|
||||
"notification_email_port_description": "El. pašto serverio prievadas (pvz. 25, 465 arba 587)",
|
||||
"notification_email_sent_test_email_button": "Siųsti bandomąjį el. laišką ir išsaugoti",
|
||||
"notification_email_setting_description": "El. pašto pranešimų siuntimo nustatymai",
|
||||
"notification_email_test_email": "Išsiųsti bandomąjį el. laišką",
|
||||
"notification_email_test_email_failed": "Nepavyko išsiųsti bandomojo el. laiško, patikrinkite savo nustatymus",
|
||||
"notification_email_test_email_sent": "Bandomasis el. laiškas buvo išsiųstas į {email}. Patikrinkite savo pašto dėžutę.",
|
||||
"notification_email_username_description": "",
|
||||
"notification_email_username_description": "Vartotojo vardas, naudojant autentikacijai su elektroninio pašto serveriu",
|
||||
"notification_enable_email_notifications": "Įgalinti el. pašto pranešimus",
|
||||
"notification_settings": "Pranešimų nustatymai",
|
||||
"notification_settings_description": "Tvarkyti pranešimų nustatymus, įskaitant el. pašto",
|
||||
|
@ -164,7 +183,9 @@
|
|||
"oauth_mobile_redirect_uri": "Mobiliojo peradresavimo URI",
|
||||
"oauth_mobile_redirect_uri_override": "Mobiliojo peradresavimo URI pakeitimas",
|
||||
"oauth_mobile_redirect_uri_override_description": "Įjunkite, kai OAuth teikėjas nepalaiko mobiliojo URI, tokio kaip '{callback}'",
|
||||
"oauth_scope": "",
|
||||
"oauth_profile_signing_algorithm": "Profilio registracijos algoritmas",
|
||||
"oauth_profile_signing_algorithm_description": "Algoritmas naudojamas vartotojo profilio registracijai.",
|
||||
"oauth_scope": "Apimtis",
|
||||
"oauth_settings": "OAuth",
|
||||
"oauth_settings_description": "Tvarkyti OAuth prisijungimo nustatymus",
|
||||
"oauth_settings_more_details": "Detaliau apie šią funkciją galite paskaityti <link>dokumentacijoje</link>.",
|
||||
|
@ -602,7 +623,7 @@
|
|||
"external": "Išorinis",
|
||||
"external_libraries": "Išorinės bibliotekos",
|
||||
"face_unassigned": "Nepriskirta",
|
||||
"favorite": "Mėgstamiausi",
|
||||
"favorite": "Mėgstamiausias",
|
||||
"favorite_or_unfavorite_photo": "Įtraukti prie arba pašalinti iš mėgstamiausių",
|
||||
"favorites": "Mėgstamiausi",
|
||||
"feature_photo_updated": "",
|
||||
|
|
143
i18n/nb_NO.json
143
i18n/nb_NO.json
|
@ -471,7 +471,7 @@
|
|||
"clear_value": "Fjern verdi",
|
||||
"clockwise": "Med urviseren",
|
||||
"close": "Lukk",
|
||||
"collapse": "Slå sammen",
|
||||
"collapse": "Trekk sammen",
|
||||
"collapse_all": "Kollaps alt",
|
||||
"color": "Farge",
|
||||
"color_theme": "Fargetema",
|
||||
|
@ -582,7 +582,7 @@
|
|||
"edit_key": "Rediger nøkkel",
|
||||
"edit_link": "Endre lenke",
|
||||
"edit_location": "Endre lokasjon",
|
||||
"edit_name": "Endre navn",
|
||||
"edit_name": "Redigere navn",
|
||||
"edit_people": "Rediger personer",
|
||||
"edit_tag": "Rediger tag",
|
||||
"edit_title": "Rediger Tittel",
|
||||
|
@ -1020,75 +1020,137 @@
|
|||
"purchase_lifetime_description": "Kjøp for livstid",
|
||||
"purchase_option_title": "KJØPSVALG",
|
||||
"purchase_panel_info_1": "Å lage Immich tar mye tid og energi, og nå har vi en fulltidsansatt utvikler som jobber med å gjøre produktet så godt vi kan. Vårt oppdrag er for åpen-kildekode programvare og etisk virksomhets praktisk å kunne bli bærekraftig inntekt for utviklere og for å lage privat repekterte økesystem med mulighet for å tilby skytjeneste.",
|
||||
"purchase_panel_info_2": "Siden har forpliktet oss ikke å legge til betalingsmurer, vil dette kjøpet ikke gi deg noen tilleggsfunksjoner i Immich. Vi er avhengige av brukere som deg for å støtte Immichs pågående utvikling.",
|
||||
"purchase_panel_title": "Hjelp prosjektet",
|
||||
"purchase_per_server": "For hver server",
|
||||
"purchase_per_user": "For hver bruker",
|
||||
"purchase_remove_product_key": "Ta bor Produktnøkkel",
|
||||
"purchase_remove_product_key_prompt": "Er du sikker på at du vil ta bort produktnøkkelen?",
|
||||
"purchase_remove_server_product_key": "Ta bort Server Produktnøkkel",
|
||||
"purchase_remove_server_product_key_prompt": "Er du sikker på at du vil ta bort Server Produktnøkkelen?",
|
||||
"purchase_server_description_1": "For hele serveren",
|
||||
"purchase_server_description_2": "Støttespiller status",
|
||||
"purchase_server_title": "Server",
|
||||
"purchase_settings_server_activated": "Produktnøkkel for server er administrert av administratoren",
|
||||
"rating": "Stjernevurdering",
|
||||
"rating_clear": "Slett vurdering",
|
||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
||||
"rating_description": "Hvis EXIF vurdering i informasjons panelet",
|
||||
"reaction_options": "Reaksjonsalternativer",
|
||||
"read_changelog": "Les endringslogg",
|
||||
"reassign": "Tilordne på nytt",
|
||||
"reassigned_assets_to_existing_person": "Tildelt på nytt {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}",
|
||||
"reassigned_assets_to_new_person": "Tildelt på nytt {count, plural, one {# asset} other {# assets}} til en ny person",
|
||||
"reassing_hint": "Tilordne valgte eiendeler til en eksisterende person",
|
||||
"recent": "Nylig",
|
||||
"recent-albums": "Nylige album",
|
||||
"recent_searches": "Nylige søk",
|
||||
"refresh": "Oppdater",
|
||||
"refresh_encoded_videos": "Oppdater kodete videoer",
|
||||
"refresh_faces": "Oppdater ansikter",
|
||||
"refresh_metadata": "Oppdater metadata",
|
||||
"refresh_thumbnails": "Oppdater miniatyrbilder",
|
||||
"refreshed": "Oppdatert",
|
||||
"refreshes_every_file": "Oppdaterer alle filer",
|
||||
"refreshing_encoded_video": "Oppdaterer kodete video",
|
||||
"refreshing_faces": "Oppdaterer ansikter",
|
||||
"refreshing_metadata": "Oppdaterer matadata",
|
||||
"regenerating_thumbnails": "Regenererer miniatyrbilder",
|
||||
"remove": "Fjern",
|
||||
"remove_assets_album_confirmation": "Er du sikker på at du fil slette {count, plural, one {# asset} other {# assets}} fra albumet?",
|
||||
"remove_assets_shared_link_confirmation": "Er du sikker på at du vil slette {count, plural, one {# asset} other {# assets}} fra den delte lenken?",
|
||||
"remove_assets_title": "Vil du fjerne eiendeler?",
|
||||
"remove_custom_date_range": "Fjern egendefinert datoperiode",
|
||||
"remove_deleted_assets": "Fjern fra frakoblede filer",
|
||||
"remove_from_album": "Fjern fra album",
|
||||
"remove_from_favorites": "Fjern fra favoritter",
|
||||
"remove_from_shared_link": "Fjern fra delt lenke",
|
||||
"remove_url": "Fjern URL",
|
||||
"remove_user": "Fjern bruker",
|
||||
"removed_api_key": "Fjernet API-nøkkel: {name}",
|
||||
"removed_from_archive": "Fjernet fra arkivet",
|
||||
"removed_from_favorites": "Fjernet fra favoritter",
|
||||
"removed_from_favorites_count": "{count, plural, other {Removed #}} fra favoritter",
|
||||
"removed_tagged_assets": "Fjern tag fra {count, plural, one {# asset} other {# assets}}",
|
||||
"rename": "Gi nytt navn",
|
||||
"repair": "Reparer",
|
||||
"repair_no_results_message": "Usporrede og savnede filer vil vises her",
|
||||
"replace_with_upload": "Erstatte med opplasting",
|
||||
"repository": "Depot",
|
||||
"require_password": "Krev passord",
|
||||
"require_user_to_change_password_on_first_login": "Krev at brukeren endrer passord ved første pålogging",
|
||||
"reset": "Tilbakestill",
|
||||
"reset_password": "Tilbakestill passord",
|
||||
"reset_people_visibility": "Tilbakestill personsynlighet",
|
||||
"reset_to_default": "Tilbakestill til standard",
|
||||
"resolve_duplicates": "Løs duplikater",
|
||||
"resolved_all_duplicates": "Løste alle duplikater",
|
||||
"restore": "Gjenopprett",
|
||||
"restore_all": "Gjenopprett alle",
|
||||
"restore_user": "Gjenopprett bruker",
|
||||
"restored_asset": "Gjenopprettet ressurs",
|
||||
"resume": "Fortsett",
|
||||
"retry_upload": "Prøv opplasting på nytt",
|
||||
"review_duplicates": "Gjennomgå duplikater",
|
||||
"role": "Rolle",
|
||||
"role_editor": "Editor",
|
||||
"role_viewer": "Visning",
|
||||
"save": "Lagre",
|
||||
"saved_api_key": "Lagret API-nøkkel",
|
||||
"saved_profile": "Lagret profil",
|
||||
"saved_settings": "Lagret instillinger",
|
||||
"say_something": "Si noe",
|
||||
"scan_all_libraries": "Skann alle biblioteker",
|
||||
"scan_library": "Skann",
|
||||
"scan_settings": "Skanneinnstillinger",
|
||||
"scanning_for_album": "Skanner etter album...",
|
||||
"search": "Søk",
|
||||
"search_albums": "Søk i album",
|
||||
"search_by_context": "Søk etter kontekst",
|
||||
"search_by_filename": "Søk etter filnavn og filtype",
|
||||
"search_by_filename_example": "f.eks. IMG_1234.JPG eller PNG",
|
||||
"search_camera_make": "Søk etter kameramerke...",
|
||||
"search_camera_model": "Søk etter kamera modell...",
|
||||
"search_city": "Søk etter by...",
|
||||
"search_country": "Søk etter land...",
|
||||
"search_for_existing_person": "Søk etter eksisterende person",
|
||||
"search_no_people": "Ingen personer",
|
||||
"search_no_people_named": "Ingen personer med navnet \"{name}\"",
|
||||
"search_options": "Søke alternativer",
|
||||
"search_people": "Søk personer",
|
||||
"search_places": "Søk steder",
|
||||
"search_settings": "Søke instillinger",
|
||||
"search_state": "Søk etter stat...",
|
||||
"search_tags": "Søk tags...",
|
||||
"search_timezone": "Søk etter tidssone....",
|
||||
"search_type": "Søk etter type",
|
||||
"search_your_photos": "Søk i dine bilder",
|
||||
"searching_locales": "Søker lokaler...",
|
||||
"second": "Sekund",
|
||||
"see_all_people": "Vis alle mennesker",
|
||||
"select_album_cover": "Velg albumomslag",
|
||||
"select_all": "Velg alle",
|
||||
"select_all_duplicates": "Velg alle duplikater",
|
||||
"select_avatar_color": "Velg avatarfarge",
|
||||
"select_face": "Velg ansikt",
|
||||
"select_featured_photo": "Velg fremhevet bilde",
|
||||
"select_from_computer": "Velg fra datamaskin",
|
||||
"select_keep_all": "Velg beholde alle",
|
||||
"select_library_owner": "Velg bibliotekseier",
|
||||
"select_new_face": "Velg nytt ansikt",
|
||||
"select_photos": "Velg bilder",
|
||||
"select_trash_all": "Velg å flytte alt til papirkurven",
|
||||
"selected": "Valgt",
|
||||
"selected_count": "{count, plural, other {# selected}}",
|
||||
"send_message": "Send melding",
|
||||
"send_welcome_email": "Send velkomstmelding",
|
||||
"server_offline": "Server frakoblet",
|
||||
"server_online": "Server tilkoblet",
|
||||
"server_stats": "Server Statistikk",
|
||||
"server_version": "Server Versjon",
|
||||
"set": "Sett",
|
||||
"set_as_album_cover": "Sett som albumomslag",
|
||||
"set_as_featured_photo": "Angi som fremhevet bilde",
|
||||
"set_as_profile_picture": "Sett som profilbilde",
|
||||
"set_date_of_birth": "Sett fødselsdato",
|
||||
"set_profile_picture": "Sett profilbilde",
|
||||
|
@ -1098,14 +1160,20 @@
|
|||
"share": "Del",
|
||||
"shared": "Delt",
|
||||
"shared_by": "Delt av",
|
||||
"shared_by_user": "Delt av {user}",
|
||||
"shared_by_you": "Delt av deg",
|
||||
"shared_from_partner": "Bilder fra {partner}",
|
||||
"shared_link_options": "Alternativer for delte lenke",
|
||||
"shared_links": "Delte linker",
|
||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# delte bilder og videoer.}}",
|
||||
"shared_with_partner": "Delt med {partner}",
|
||||
"sharing": "Deling",
|
||||
"sharing_enter_password": "Vennligst skriv inn passordet for å se denne siden.",
|
||||
"sharing_sidebar_description": "Vis en lenke til Deling i sidepanelet",
|
||||
"shift_to_permanent_delete": "trykk ⇧ for å slette eiendeler permanent",
|
||||
"show_album_options": "Vis albumalternativer",
|
||||
"show_albums": "Vis album",
|
||||
"show_all_people": "Vis alle mennesker",
|
||||
"show_and_hide_people": "Vis og skjul personer",
|
||||
"show_file_location": "Vis filplassering",
|
||||
"show_gallery": "Vis galleri",
|
||||
|
@ -1119,16 +1187,34 @@
|
|||
"show_person_options": "Vis personalternativer",
|
||||
"show_progress_bar": "Vis fremdriftslinje",
|
||||
"show_search_options": "Vis søkealternativer",
|
||||
"show_slideshow_transition": "Vis overgang til lysbildefremvisning",
|
||||
"show_supporter_badge": "Supportermerke",
|
||||
"show_supporter_badge_description": "Vis et supportermerke",
|
||||
"shuffle": "Bland",
|
||||
"sidebar": "Sidefelt",
|
||||
"sidebar_display_description": "Vis en lenke for visningen i sidefeltet",
|
||||
"sign_out": "Logg ut",
|
||||
"sign_up": "Registrer deg",
|
||||
"size": "Størrelse",
|
||||
"skip_to_content": "Gå til innhold",
|
||||
"skip_to_folders": "Hopp til mapper",
|
||||
"skip_to_tags": "Hopp til tagger",
|
||||
"slideshow": "Lysbildefremvisning",
|
||||
"slideshow_settings": "Lysbildefremvisning innstillinger",
|
||||
"sort_albums_by": "Sorter album etter...",
|
||||
"sort_created": "Dato opprettet",
|
||||
"sort_items": "Antall enheter",
|
||||
"sort_modified": "Dato modifisert",
|
||||
"sort_oldest": "Eldste bilde",
|
||||
"sort_people_by_similarity": "Sorter folk etter likhet",
|
||||
"sort_recent": "Nyeste bilde",
|
||||
"sort_title": "Tittel",
|
||||
"source": "Kilde",
|
||||
"stack": "Stable",
|
||||
"stack_duplicates": "Stable duplikater",
|
||||
"stack_select_one_photo": "Velg hovedbilde for bildestabbel",
|
||||
"stack_selected_photos": "Stable valgte bilder",
|
||||
"stacked_assets_count": "Stable {count, plural, one {# asset} other {# assets}}",
|
||||
"stacktrace": "Stakkspor",
|
||||
"start": "Start",
|
||||
"start_date": "Startdato",
|
||||
|
@ -1144,68 +1230,121 @@
|
|||
"submit": "Send inn",
|
||||
"suggestions": "Forslag",
|
||||
"sunrise_on_the_beach": "Soloppgang på stranden",
|
||||
"support": "Støtte",
|
||||
"support_and_feedback": "Støtte og Tilbakemelding",
|
||||
"support_third_party_description": "Immich-installasjonen din ble pakket av en tredjepart. Problemer du opplever kan være forårsaket av den pakken, så vennligst ta opp problemer med dem i første omgang ved å bruke koblingene nedenfor.",
|
||||
"swap_merge_direction": "Bytt retning på sammenslåingen",
|
||||
"sync": "Synkroniser",
|
||||
"tag": "Tagg",
|
||||
"tag_assets": "Merk ressurser",
|
||||
"tag_created": "Lag merke: {tag}",
|
||||
"tag_feature_description": "Bla gjennom bilder og videoer gruppert etter logiske merke-emner",
|
||||
"tag_not_found_question": "Finner du ikke en merke? <link>Opprett en nytt merke.</link>",
|
||||
"tag_updated": "Oppdater merke: {tag}",
|
||||
"tagged_assets": "Merket {count, plural, one {# asset} other {# assets}}",
|
||||
"tags": "Merker",
|
||||
"template": "Mal",
|
||||
"theme": "Tema",
|
||||
"theme_selection": "Temavalg",
|
||||
"theme_selection_description": "Automatisk sett tema til lys eller mørk basert på nettleserens systeminnstilling",
|
||||
"they_will_be_merged_together": "De vil bli slått sammen",
|
||||
"third_party_resources": "Tredjeparts Ressurser",
|
||||
"time_based_memories": "Tidsbaserte minner",
|
||||
"timeline": "Tidslinje",
|
||||
"timezone": "Tidssone",
|
||||
"to_archive": "Arkiv",
|
||||
"to_change_password": "Endre passord",
|
||||
"to_favorite": "Favoritt",
|
||||
"to_login": "Logg inn",
|
||||
"to_parent": "Gå til overodnet",
|
||||
"to_trash": "Papirkurv",
|
||||
"toggle_settings": "Bytt innstillinger",
|
||||
"toggle_theme": "Bytt tema",
|
||||
"total": "Total",
|
||||
"total_usage": "Totalt brukt",
|
||||
"trash": "Papirkurv",
|
||||
"trash_all": "Slett alt",
|
||||
"trash_count": "Slett {count, number}",
|
||||
"trash_delete_asset": "Slett ressurs",
|
||||
"trash_no_results_message": "Her vises bilder og videoer som er flyttet til papirkurven.",
|
||||
"trashed_items_will_be_permanently_deleted_after": "Elementer i papirkurven vil bli permanent slettet etter {days, plural, one {# dag} other {# dager}}.",
|
||||
"type": "Type",
|
||||
"unarchive": "Fjern fra arkiv",
|
||||
"unarchived_count": "{count, plural, other {Unarchived #}}",
|
||||
"unfavorite": "Fjern favoritt",
|
||||
"unhide_person": "Vis person",
|
||||
"unknown": "Ukjent",
|
||||
"unknown_year": "Ukjent År",
|
||||
"unlimited": "Ubegrenset",
|
||||
"unlink_motion_video": "Koble fra bevegelsesvideo",
|
||||
"unlink_oauth": "Fjern kobling til OAuth",
|
||||
"unlinked_oauth_account": "Koblet fra OAuth-konto",
|
||||
"unnamed_album": "Navnløst album",
|
||||
"unnamed_album_delete_confirmation": "Er du sikker på at du vil slette dette albumet?",
|
||||
"unnamed_share": "Deling uten navn",
|
||||
"unsaved_change": "Ulagrede endringer",
|
||||
"unselect_all": "Fjern alle valg",
|
||||
"unselect_all_duplicates": "Fjern markeringen av alle duplikater",
|
||||
"unstack": "avstable",
|
||||
"unstacked_assets_count": "Ikke stablet {count, plural, one {# asset} other {# assets}}",
|
||||
"untracked_files": "Usporede Filer",
|
||||
"untracked_files_decription": "Disse filene er ikke sporet av applikasjonen. De kan være resultatet av mislykkede flyttinger, avbrutte opplastinger eller etterlatt på grunn av en feil",
|
||||
"up_next": "Neste",
|
||||
"updated_password": "Passord oppdatert",
|
||||
"upload": "Last opp",
|
||||
"upload_concurrency": "Samtidig opplastning",
|
||||
"upload_errors": "Opplasting fullført med {count, plural, one {# error} other {# errors}}, oppdater siden for å se nye opplastingsressurser.",
|
||||
"upload_progress": "Gjenstående {remaining, number} – behandlet {processed, number}/{total, number}",
|
||||
"upload_skipped_duplicates": "Hoppet over {count, plural, one {# duplicate asset} other {# duplicate assets}}",
|
||||
"upload_status_duplicates": "Duplikater",
|
||||
"upload_status_errors": "Feil",
|
||||
"upload_status_uploaded": "Opplastet",
|
||||
"upload_success": "Opplasting vellykket, oppdater siden for å se nye opplastninger.",
|
||||
"url": "URL",
|
||||
"usage": "Bruk",
|
||||
"use_custom_date_range": "Bruk egendefinert datoperiode i stedet",
|
||||
"user": "Bruker",
|
||||
"user_id": "Bruker ID",
|
||||
"user_liked": "{user} likte {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
|
||||
"user_purchase_settings": "Kjøpe",
|
||||
"user_purchase_settings_description": "Administrer dine kjøp",
|
||||
"user_role_set": "Sett {user} som {role}",
|
||||
"user_usage_detail": "Detaljer av brukers forbruk",
|
||||
"user_usage_stats": "Kontobruksstatistikk",
|
||||
"user_usage_stats_description": "Vis kontobruksstatistikk",
|
||||
"username": "Brukernavn",
|
||||
"users": "Brukere",
|
||||
"utilities": "Verktøy",
|
||||
"validate": "Valider",
|
||||
"variables": "Variabler",
|
||||
"version": "Versjon",
|
||||
"version_announcement_closing": "Din venn, Alex",
|
||||
"version_announcement_message": "Hei! En ny versjon av Immich er tilgjengelig. Vennligst ta deg tid til å lese <link>utgivelsesnotatene</link> for å sikre at oppsettet ditt er oppdatert for å forhindre feilkonfigurasjoner, spesielt hvis du bruker WatchTower eller en annen mekanisme som håndterer oppdatering av Immich-forekomsten din automatisk.",
|
||||
"version_history": "Verson Historie",
|
||||
"version_history_item": "Installert {version} den {date}",
|
||||
"video": "Video",
|
||||
"video_hover_setting": "Spill av forhåndsvisining mens du holder over musepekeren",
|
||||
"video_hover_setting_description": "Spill av forhåndsvisning mens en musepeker er over elementet. Selv når den er deaktivert, kan avspilling startes ved å holde musepekeren over avspillingsikonet.",
|
||||
"videos": "Videoer",
|
||||
"videos_count": "{count, plural, one {# Video} other {# Videoer}}",
|
||||
"view": "Vis",
|
||||
"view_album": "Vis Album",
|
||||
"view_all": "Vis alle",
|
||||
"view_all_users": "Vis alle brukere",
|
||||
"view_in_timeline": "Vis i tidslinje",
|
||||
"view_links": "Vis lenker",
|
||||
"view_name": "Vis",
|
||||
"view_next_asset": "Vis neste fil",
|
||||
"view_previous_asset": "Vis forrige fil",
|
||||
"view_stack": "Vis Stabbel",
|
||||
"visibility_changed": "Synlighet endret for {count, plural, one {# person} other {# people}}",
|
||||
"waiting": "Venter",
|
||||
"warning": "Advarsel",
|
||||
"week": "Uke",
|
||||
"welcome": "Velkommen",
|
||||
"welcome_to_immich": "Velkommen til Immich",
|
||||
"year": "År",
|
||||
"years_ago": "{years, plural, one {# year} other {# years}} siden",
|
||||
"yes": "Ja",
|
||||
"you_dont_have_any_shared_links": "Du har ingen delte lenker",
|
||||
"zoom_image": "Zoom Bilde"
|
||||
|
|
262
i18n/nn.json
262
i18n/nn.json
|
@ -28,6 +28,264 @@
|
|||
"added_to_favorites": "Lagt til favorittar",
|
||||
"added_to_favorites_count": "Lagt {count, number} til favorittar",
|
||||
"admin": {
|
||||
"confirm_delete_library": "Er du sikker på at du vil slette biblioteket {library}?"
|
||||
}
|
||||
"asset_offline_description": "Denne eksterne bibliotekressursen finst ikkje lenger på disk og har blitt flytta til papirkurven. Om fila blei flytta innad i biblioteket, sjekk tidslinja di for den tilsvarande ressursen. For å gjenopprette ressursen, vennligst sørg for at filstien under er tilgjengeleg for Immich og skann biblioteket.",
|
||||
"backup_settings": "Backupinnstillingar",
|
||||
"check_all": "Sjekk alle",
|
||||
"confirm_delete_library": "Er du sikker på at du vil slette biblioteket {library}?",
|
||||
"create_job": "Lag jobb",
|
||||
"disable_login": "Deaktiver innlogging",
|
||||
"face_detection": "Ansiktsdeteksjon",
|
||||
"image_format": "Format",
|
||||
"image_preview_title": "Forhandsvis innstillingar",
|
||||
"image_quality": "Kvalitet",
|
||||
"image_resolution": "Oppløysing",
|
||||
"image_thumbnail_description": "Lite miniatyrbilete med fjerna metadata, brukt når ein ser på grupper av bilete som hovudtidslinja",
|
||||
"job_created": "Jobb laga",
|
||||
"job_settings": "Jobbinnstillingar",
|
||||
"job_status": "Jobbstatus",
|
||||
"library_deleted": "Bibliotek sletta",
|
||||
"library_scanning": "Periodisk skanning",
|
||||
"library_settings": "Eksternt Bibliotek",
|
||||
"logging_settings": "Logging",
|
||||
"machine_learning_duplicate_detection": "Duplikatdeteksjon",
|
||||
"machine_learning_facial_recognition": "Ansiktsgjenkjenning",
|
||||
"machine_learning_smart_search": "Smart Søk",
|
||||
"map_dark_style": "Mørk modus",
|
||||
"map_light_style": "Lys modus",
|
||||
"map_settings": "Kart",
|
||||
"metadata_extraction_job": "Hent ut metadata",
|
||||
"metadata_settings": "Metadata Innstillinger",
|
||||
"migration_job": "Migrasjon",
|
||||
"notification_email_from_address": "Frå adresse",
|
||||
"notification_settings": "Varselinnstillingar",
|
||||
"oauth_auto_launch": "Autostart",
|
||||
"oauth_button_text": "Tekst på knapp",
|
||||
"password_settings": "Passord innlogging",
|
||||
"person_cleanup_job": "Personopprydding",
|
||||
"registration": "Administrator registrering",
|
||||
"registration_description": "Sidan du er den første brukaren på systemet, vil du bli utnevnt til administrator og ha ansvar for administrative oppgåver. Du vil òg opprette eventuelle nye brukarar.",
|
||||
"repair_all": "Reparer alle",
|
||||
"repair_matched_items": "Samsvarte med {count, plural, one {# element} other {# elementer}}",
|
||||
"repaired_items": "Reparerte {count, plural, one {# item} other {# items}}",
|
||||
"require_password_change_on_login": "Krev at brukaren endrar passord ved første pålogging",
|
||||
"reset_settings_to_default": "Tilbakestill innstillingar til standard",
|
||||
"reset_settings_to_recent_saved": "Tilbakestill innstillingane til de nyleg lagra innstillingane",
|
||||
"scanning_library": "Skann bibliotek",
|
||||
"search_jobs": "Søk etter jobbar",
|
||||
"send_welcome_email": "Send velkomst-e-post",
|
||||
"server_external_domain_settings": "Eksternt domene",
|
||||
"server_external_domain_settings_description": "Domene for offentlege delingslenkjer, inkludert http(s)://",
|
||||
"server_public_users": "Offentlege brukarar",
|
||||
"server_public_users_description": "Alle brukarar (namn og epost) blir vist når ein brukar blir lagt til eit delt album. Når deaktivert, vil brukarane berre bli synlege for administratorar.",
|
||||
"server_settings": "Serverinstillingar",
|
||||
"server_settings_description": "Administrer serverinnstillingar",
|
||||
"server_welcome_message": "Velkomstmelding",
|
||||
"server_welcome_message_description": "Ei melding som synast på innloggingssida.",
|
||||
"template_email_preview": "Førehandsvisning"
|
||||
},
|
||||
"administration": "Administrasjon",
|
||||
"advanced": "Avansert",
|
||||
"album_with_link_access": "Lat kven som helst med lenka sjå bilete og folk i dette albumet.",
|
||||
"albums": "Album",
|
||||
"all": "Alle",
|
||||
"anti_clockwise": "Mot klokka",
|
||||
"archive": "Arkiv",
|
||||
"asset_skipped": "Hoppa over",
|
||||
"asset_uploaded": "Opplasta",
|
||||
"asset_uploading": "Lastar opp...",
|
||||
"back": "Tilbake",
|
||||
"backward": "Bakover",
|
||||
"camera": "Kamera",
|
||||
"cancel": "Avbryt",
|
||||
"city": "By",
|
||||
"clear": "Fjern",
|
||||
"clockwise": "Med klokka",
|
||||
"close": "Lukk",
|
||||
"color": "Farge",
|
||||
"confirm": "Bekreft",
|
||||
"contain": "Inneheld",
|
||||
"continue": "Hald fram",
|
||||
"country": "Land",
|
||||
"cover": "Dekk",
|
||||
"covers": "Dekker",
|
||||
"create": "Opprett",
|
||||
"created": "Oppretta",
|
||||
"dark": "Mørk",
|
||||
"day": "Dag",
|
||||
"delete": "Slett",
|
||||
"description": "Beskrivelse",
|
||||
"details": "Detaljer",
|
||||
"direction": "Retning",
|
||||
"discover": "Oppdag",
|
||||
"display_original_photos": "Vis originale bilete",
|
||||
"display_original_photos_setting_description": "Føretrekk å vise det originale biletet når ein ser på eit aktivum i staden for miniatyrbilete når det originale aktivumet er nettkompatibelt. Dette kan føre til tregare biletvisingshastigheiter.",
|
||||
"documentation": "Dokumentasjon",
|
||||
"done": "Ferdig",
|
||||
"download": "Last ned",
|
||||
"download_include_embedded_motion_videos_description": "Inkluder videoar innebygd i rørslefoto som ein eigen fil",
|
||||
"download_settings": "Last ned",
|
||||
"downloading": "Laster ned",
|
||||
"duplicates": "Duplikat",
|
||||
"duration": "Lengde",
|
||||
"edit": "Rediger",
|
||||
"edited": "Redigert",
|
||||
"editor": "Redigeringsverktøy",
|
||||
"explore": "Utforsk",
|
||||
"explorer": "Utforsker",
|
||||
"folders_feature_description": "Bla gjennom mappe for bileta og videoane på filsystemet",
|
||||
"hour": "Time",
|
||||
"image": "Bilde",
|
||||
"info": "Info",
|
||||
"jobs": "Oppgåver",
|
||||
"keep": "Behald",
|
||||
"language": "Språk",
|
||||
"latitude": "Lengdegrad",
|
||||
"leave": "Forlat",
|
||||
"level": "Nivå",
|
||||
"library": "Bibliotek",
|
||||
"light": "Lys",
|
||||
"list": "Liste",
|
||||
"loading": "Lastar",
|
||||
"login": "Login",
|
||||
"longitude": "Lengdegrad",
|
||||
"look": "Utsjånad",
|
||||
"make": "Produsent",
|
||||
"map": "Kart",
|
||||
"matches": "Treff",
|
||||
"memories": "Minner",
|
||||
"memory": "Minne",
|
||||
"menu": "Meny",
|
||||
"merge": "Slå saman",
|
||||
"minimize": "Minimere",
|
||||
"minute": "Minutt",
|
||||
"missing": "Mangler",
|
||||
"model": "Modell",
|
||||
"month": "Månad",
|
||||
"more": "Meir",
|
||||
"name": "Namn",
|
||||
"never": "Aldri",
|
||||
"next": "Neste",
|
||||
"no": "Nei",
|
||||
"no_albums_message": "Lag eit album for å organisere bileta og videoane dine.",
|
||||
"no_archived_assets_message": "Arkiver bilder og videoar for å skjule dei frå bileta dine",
|
||||
"no_explore_results_message": "Last opp fleire bilete for å utforske samlinga di.",
|
||||
"no_libraries_message": "Lag eit eksternt bibliotek for å sjå bileta og videoane dine",
|
||||
"no_shared_albums_message": "Lag eit album for å dele bilete og videoar med folk i nettverket ditt",
|
||||
"notes": "Noter",
|
||||
"notifications": "Varsel",
|
||||
"ok": "Ok",
|
||||
"options": "Val",
|
||||
"or": "eller",
|
||||
"original": "original",
|
||||
"other": "Anna",
|
||||
"owner": "Eigar",
|
||||
"partner": "Partner",
|
||||
"partner_can_access_assets": "Alle bileta og videoane dine unntatt dei i Arkivert og Sletta",
|
||||
"partner_can_access_location": "Staden der bileta dine vart tekne",
|
||||
"password": "Passord",
|
||||
"path": "Sti",
|
||||
"pattern": "Mønster",
|
||||
"pause": "Pause",
|
||||
"paused": "Pausa",
|
||||
"pending": "Ventar",
|
||||
"people": "Folk",
|
||||
"people_feature_description": "Bla gjennom foto og videoar gruppert etter folk",
|
||||
"person": "Person",
|
||||
"photo_shared_all_users": "Ser ut som du delte bileta dine med alle brukarar eller at du ikkje har nokon brukar å dele med.",
|
||||
"photos": "Bilete",
|
||||
"photos_and_videos": "Foto og Video",
|
||||
"photos_from_previous_years": "Bilete frå tidlegare år",
|
||||
"place": "Stad",
|
||||
"places": "Stad",
|
||||
"play": "Spel av",
|
||||
"port": "Port",
|
||||
"preview": "Førehandsvisning",
|
||||
"previous": "Forrige",
|
||||
"primary": "Hoved",
|
||||
"privacy": "Personvern",
|
||||
"purchase_button_activate": "Aktiver",
|
||||
"purchase_button_buy": "Kjøp",
|
||||
"purchase_button_select": "Vel",
|
||||
"purchase_individual_title": "Induviduell",
|
||||
"purchase_server_title": "Server",
|
||||
"reassign": "Vel på nytt",
|
||||
"recent": "Nyleg",
|
||||
"refresh": "Oppdater",
|
||||
"refreshed": "Oppdatert",
|
||||
"remove": "Fjern",
|
||||
"rename": "Endre namn",
|
||||
"repair": "Reparasjon",
|
||||
"reset": "Tilbakestill",
|
||||
"restore": "Tilbakestill",
|
||||
"resume": "Fortsett",
|
||||
"role": "Rolle",
|
||||
"save": "Lagre",
|
||||
"scan_library": "Skann",
|
||||
"search": "Søk",
|
||||
"search_your_photos": "Søk i dine bilete",
|
||||
"second": "Sekund",
|
||||
"selected": "Valgt",
|
||||
"set": "Sett",
|
||||
"settings": "Innstillingar",
|
||||
"share": "Del",
|
||||
"shared": "Delt",
|
||||
"shared_from_partner": "Bilete frå {partner}",
|
||||
"sharing": "Deling",
|
||||
"show_in_timeline_setting_description": "Vis bilete og videoar frå denne brukaren i tidslinja di",
|
||||
"sidebar": "Sidebar",
|
||||
"size": "Størrelse",
|
||||
"slideshow": "Lysbildeframvisning",
|
||||
"sort_title": "Tittel",
|
||||
"source": "Kjelde",
|
||||
"stack": "Stabel",
|
||||
"start": "Start",
|
||||
"state": "Region",
|
||||
"status": "Status",
|
||||
"stop_photo_sharing": "Stopp å dele bileta dine?",
|
||||
"stop_photo_sharing_description": "{partner} vil ikkje lenger kunne få tilgang til bileta dine.",
|
||||
"stop_sharing_photos_with_user": "Stopp å dele bileta dine med denne brukaren",
|
||||
"storage": "Lagringsplass",
|
||||
"submit": "Send inn",
|
||||
"suggestions": "Forslag",
|
||||
"support": "Support",
|
||||
"sync": "Synk",
|
||||
"tag": "Tag",
|
||||
"tag_feature_description": "Bla gjennom bilete og videoar gruppert etter logiske tag-tema",
|
||||
"tags": "Tags",
|
||||
"theme": "Tema",
|
||||
"timeline": "Tidslinje",
|
||||
"timezone": "Tidssone",
|
||||
"to_archive": "Arkiv",
|
||||
"to_favorite": "Favoritt",
|
||||
"to_login": "Innlogging",
|
||||
"to_trash": "Søppel",
|
||||
"total": "Total",
|
||||
"trash": "Søppel",
|
||||
"trash_no_results_message": "Sletta foto og videoar vil dukke opp her.",
|
||||
"type": "Type",
|
||||
"unfavorite": "Fjern favoritt",
|
||||
"unknown": "Ukjent",
|
||||
"unlimited": "Ubegrensa",
|
||||
"upload": "Last opp",
|
||||
"upload_status_duplicates": "Duplikater",
|
||||
"upload_status_errors": "Feil",
|
||||
"upload_status_uploaded": "Opplasta",
|
||||
"url": "URL",
|
||||
"usage": "Bruk",
|
||||
"user": "Brukar",
|
||||
"user_purchase_settings": "Kjøp",
|
||||
"username": "Brukarnamn",
|
||||
"users": "Brukarar",
|
||||
"utilities": "Verktøy",
|
||||
"validate": "Validere",
|
||||
"variables": "Variablar",
|
||||
"version": "Versjon",
|
||||
"video": "Video",
|
||||
"videos": "Videoar",
|
||||
"waiting": "Ventar",
|
||||
"warning": "Advarsel",
|
||||
"week": "Veke",
|
||||
"welcome": "Velkomen",
|
||||
"year": "År",
|
||||
"yes": "Ja"
|
||||
}
|
||||
|
|
13
i18n/ro.json
13
i18n/ro.json
|
@ -289,6 +289,8 @@
|
|||
"transcoding_constant_rate_factor": "Factor de rată constantă (-crf)",
|
||||
"transcoding_constant_rate_factor_description": "Nivelul de calitate al videoclipului. Valorile tipice sunt 23 pentru H.264, 28 pentru HEVC, 31 pentru VP9 și 35 pentru AV1. Cu cât valoarea este mai mică, cu atât calitatea este mai bună, dar se generează fișiere mai mari.",
|
||||
"transcoding_disabled_description": "Nu transcodifică niciun videoclip; acest lucru poate afecta redarea pe anumite dispozitive",
|
||||
"transcoding_encoding_options": "Opțiuni codificare",
|
||||
"transcoding_encoding_options_description": "Setează codecuri , calitatea, rezoluția și alte opțiuni pentru videoclipuri codificare",
|
||||
"transcoding_hardware_acceleration": "Accelerare Hardware",
|
||||
"transcoding_hardware_acceleration_description": "Experimental; mult mai rapid, dar va avea o calitate mai scăzută la același bitrate",
|
||||
"transcoding_hardware_decoding": "Decodare hardware",
|
||||
|
@ -301,6 +303,8 @@
|
|||
"transcoding_max_keyframe_interval": "Interval maxim între cadre cheie",
|
||||
"transcoding_max_keyframe_interval_description": "Setează distanța maximă între cadrele cheie. Valorile mai mici reduc eficiența compresiei, dar îmbunătățesc timpii de căutare și pot îmbunătăți calitatea în scenele cu mișcare rapidă. 0 setează această valoare automat.",
|
||||
"transcoding_optimal_description": "Videoclipuri cu rezoluție mai mare decât cea țintă sau care nu sunt într-un format acceptat",
|
||||
"transcoding_policy": "Politică de transcodare",
|
||||
"transcoding_policy_description": "Setează când un video va fi transcodat",
|
||||
"transcoding_preferred_hardware_device": "Dispozitiv hardware preferat",
|
||||
"transcoding_preferred_hardware_device_description": "Se aplică doar la VAAPI și QSV. Setează nodul DRI utilizat pentru transcodarea hardware.",
|
||||
"transcoding_preset_preset": "Presetare (-preset)",
|
||||
|
@ -309,7 +313,7 @@
|
|||
"transcoding_reference_frames_description": "Numărul de cadre de referință atunci când se comprimă un cadru dat. Valorile mai mari îmbunătățesc eficiența compresiei, dar încetinesc codarea. 0 setează această valoare automat.",
|
||||
"transcoding_required_description": "Numai videoclipuri care nu sunt într-un format acceptat",
|
||||
"transcoding_settings": "Setări de Transcodare Video",
|
||||
"transcoding_settings_description": "Gestionează rezoluția și informațiile de codare ale fișierelor video",
|
||||
"transcoding_settings_description": "Gestionează care videoclipuri să transcodam și cum să le procesam",
|
||||
"transcoding_target_resolution": "Rezoluția țintă",
|
||||
"transcoding_target_resolution_description": "Rezoluțiile mai mari pot păstra mai multe detalii, dar necesită mai mult timp pentru codare, au dimensiuni mai mari ale fișierelor și pot reduce răspunsul aplicației.",
|
||||
"transcoding_temporal_aq": "AQ temporal",
|
||||
|
@ -322,7 +326,7 @@
|
|||
"transcoding_transcode_policy_description": "Politica pentru momentul când un videoclip ar trebui să fie transcodificat. Videoclipurile HDR vor fi întotdeauna transcodificate (cu excepția cazului în care transcodarea este dezactivată).",
|
||||
"transcoding_two_pass_encoding": "Codare în doi pași",
|
||||
"transcoding_two_pass_encoding_setting_description": "Transcodificare în două treceri pentru a produce videoclipuri codificate mai bine. Când rata maximă de biți este activată (necesară pentru a funcționa cu H.264 și HEVC), acest mod utilizează un interval de rată de biți bazat pe rata maximă de biți și ignoră CRF. Pentru VP9, CRF poate fi utilizat dacă rata maximă de biți este dezactivată.",
|
||||
"transcoding_video_codec": "Codec Video",
|
||||
"transcoding_video_codec": "Codec video",
|
||||
"transcoding_video_codec_description": "VP9 are eficiențǎ mare și compatibilitate web, însǎ transcodarea este de duratǎ mai mare. HEVC se comportǎ asemǎnǎtor, însǎ are compatibilitate web mai micǎ. H.264 este foarte compatibil și rapid în transcodare, însǎ genereazǎ fișiere mult mai mari. AV1 este cel mai eficient codec dar nu este compatibil cu dispozitivele mai vechi.",
|
||||
"trash_enabled_description": "Activează funcțiile Coșului de Gunoi",
|
||||
"trash_number_of_days": "Numǎr de zile",
|
||||
|
@ -519,6 +523,10 @@
|
|||
"date_range": "Interval de date",
|
||||
"day": "Zi",
|
||||
"deduplicate_all": "Deduplicați Toate",
|
||||
"deduplication_criteria_1": "Marimea imagini în octeți",
|
||||
"deduplication_criteria_2": "Numărul de date EXIF",
|
||||
"deduplication_info": "Informați despre deduplicare",
|
||||
"deduplication_info_description": "Ca să preselecționăm activele și să scoatem duplicatele în vrac , ne uităm la:",
|
||||
"default_locale": "Setare Regională Implicită",
|
||||
"default_locale_description": "Formatați datele și numerele în funcție de regiunea browserului dvs",
|
||||
"delete": "Ștergere",
|
||||
|
@ -755,6 +763,7 @@
|
|||
"get_help": "Obțineți Ajutor",
|
||||
"getting_started": "Noțiuni de Bază",
|
||||
"go_back": "Întoarcere",
|
||||
"go_to_folder": "Accesați folderul",
|
||||
"go_to_search": "Spre căutare",
|
||||
"group_albums_by": "Grupați albume de...",
|
||||
"group_no": "Fără grupare",
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"add_a_location": "Додај Локацију",
|
||||
"add_a_name": "Додај име",
|
||||
"add_a_title": "Додај наслов",
|
||||
"add_exclusion_pattern": "Додајте образац изузимања",
|
||||
"add_exclusion_pattern": "Додај образац изузимања",
|
||||
"add_import_path": "Додај путању за преузимање",
|
||||
"add_location": "Додај локацију",
|
||||
"add_more_users": "Додај кориснике",
|
||||
|
@ -23,7 +23,7 @@
|
|||
"add_to": "Додај у...",
|
||||
"add_to_album": "Додај у албум",
|
||||
"add_to_shared_album": "Додај у дељен албум",
|
||||
"add_url": "Додајте URL",
|
||||
"add_url": "Додај URL",
|
||||
"added_to_archive": "Додато у архиву",
|
||||
"added_to_favorites": "Додато у фаворите",
|
||||
"added_to_favorites_count": "Додато {count, number} у фаворите",
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"add_a_location": "Dodaj Lokaciju",
|
||||
"add_a_name": "Dodaj ime",
|
||||
"add_a_title": "Dodaj naslov",
|
||||
"add_exclusion_pattern": "Dodajte obrazac izuzimanja",
|
||||
"add_exclusion_pattern": "Dodaj obrazac izuzimanja",
|
||||
"add_import_path": "Dodaj putanju za preuzimanje",
|
||||
"add_location": "Dodaj lokaciju",
|
||||
"add_more_users": "Dodaj korisnike",
|
||||
|
@ -23,7 +23,7 @@
|
|||
"add_to": "Dodaj u...",
|
||||
"add_to_album": "Dodaj u album",
|
||||
"add_to_shared_album": "Dodaj u deljen album",
|
||||
"add_url": "Dodajte URL",
|
||||
"add_url": "Dodaj URL",
|
||||
"added_to_archive": "Dodato u arhivu",
|
||||
"added_to_favorites": "Dodato u favorite",
|
||||
"added_to_favorites_count": "Dodato {count, number} u favorite",
|
||||
|
|
|
@ -909,12 +909,14 @@
|
|||
"no_results_description": "Pröva en synonym eller ett annat mer allmänt sökord",
|
||||
"no_shared_albums_message": "Skapa ett album för att dela bilder och videor med andra personer",
|
||||
"not_in_any_album": "Inte i något album",
|
||||
"note_apply_storage_label_to_previously_uploaded assets": "Obs: Om du vill använda lagringsetiketten på tidigare uppladdade tillgångar kör du",
|
||||
"note_unlimited_quota": "Notera: Ange 0 för obegränsad mängd",
|
||||
"notes": "Notera",
|
||||
"notification_toggle_setting_description": "Aktivera e-postaviseringar",
|
||||
"notifications": "Notifikationer",
|
||||
"notifications_setting_description": "Hantera aviseringar",
|
||||
"oauth": "OAuth",
|
||||
"official_immich_resources": "Officiella Immich-resurser",
|
||||
"offline": "Frånkopplad",
|
||||
"offline_paths": "Offlinevägar",
|
||||
"offline_paths_description": "Dessa resultat kan bero på att filer som ej ingår i ett externt bibliotek har tagits bort manuellt.",
|
||||
|
@ -941,11 +943,12 @@
|
|||
"owner": "Ägare",
|
||||
"partner": "Partner",
|
||||
"partner_can_access": "{partner} har åtkomst",
|
||||
"partner_can_access_assets": "Alla dina foton och videoklipp förutom de i Arkiverade och Raderade",
|
||||
"partner_can_access_location": "Platsen där dina foton togs",
|
||||
"partner_sharing": "Partnerdelning",
|
||||
"partners": "Partners",
|
||||
"password": "Lösenord",
|
||||
"password_does_not_match": "",
|
||||
"password_does_not_match": "Lösenorden stämmer inte överens",
|
||||
"password_required": "Lösenord krävs",
|
||||
"password_reset_success": "Lösenord återställt",
|
||||
"past_durations": {
|
||||
|
@ -960,14 +963,14 @@
|
|||
"paused": "Pausad",
|
||||
"pending": "Väntande",
|
||||
"people": "Personer",
|
||||
"people_edits_count": "Redigerad {count, plural, one {# person} other {# people}}",
|
||||
"people_edits_count": "Redigerad {count, plural, one {# person} other {# personer}}",
|
||||
"people_feature_description": "Visar foton och videor grupperade efter personer",
|
||||
"people_sidebar_description": "Visa en länk till Personer i sidopanelen",
|
||||
"permanent_deletion_warning": "Varning om permanent radering",
|
||||
"permanent_deletion_warning_setting_description": "Visa en varning när tillgångar raderas permanent",
|
||||
"permanently_delete": "Radera permanent",
|
||||
"permanently_delete_assets_count": "Radera {count, plural, one {asset} other {assets}} permanent",
|
||||
"permanently_deleted_asset": "",
|
||||
"permanently_deleted_asset": "Permanent raderad tillgång",
|
||||
"person": "Person",
|
||||
"photos": "Foton",
|
||||
"photos_and_videos": "Foton & videor",
|
||||
|
|
11
i18n/tr.json
11
i18n/tr.json
|
@ -289,6 +289,8 @@
|
|||
"transcoding_constant_rate_factor": "Sabit oran faktörü (-SOF)",
|
||||
"transcoding_constant_rate_factor_description": "Video kalite seviyesi. Tipik değerler H.264 için 23, HEVC için 28, VP9 için 31 ve AV1 için 35'tir. Daha düşük değerler daha iyi kalite sağlar, ancak daha büyük dosyalar üretir.",
|
||||
"transcoding_disabled_description": "Videoları dönüştürmeyin, bazı istemcilerde oynatma bozulabilir",
|
||||
"transcoding_encoding_options": "Kodlama Seçenekleri",
|
||||
"transcoding_encoding_options_description": "Kodlanmış videolar için kodekleri, çözünürlüğü, kaliteyi ve diğer seçenekleri ayarlayın",
|
||||
"transcoding_hardware_acceleration": "Donanım Hızlandırma",
|
||||
"transcoding_hardware_acceleration_description": "Deneysel; daha hızlı, fakat aynı bitrate ayarlarında daha düşük kaliteye sahip",
|
||||
"transcoding_hardware_decoding": "Donanım çözücü",
|
||||
|
@ -301,6 +303,8 @@
|
|||
"transcoding_max_keyframe_interval": "Maksimum ana kare aralığı",
|
||||
"transcoding_max_keyframe_interval_description": "Ana kareler arasındaki maksimum kare mesafesini ayarlar. Düşük değerler sıkıştırma verimliliğini kötüleştirir, ancak arama sürelerini iyileştirir ve hızlı hareket içeren sahnelerde kaliteyi artırabilir. 0 bu değeri otomatik olarak ayarlar.",
|
||||
"transcoding_optimal_description": "Hedef çözünürlükten yüksek veya kabul edilen formatta olmayan videolar",
|
||||
"transcoding_policy": "Kod Dönüştürme Politikası",
|
||||
"transcoding_policy_description": "Bir videonun ne zaman kod dönüştürüleceğini ayarlama",
|
||||
"transcoding_preferred_hardware_device": "Tercih edilen donanım cihazı",
|
||||
"transcoding_preferred_hardware_device_description": "Sadece VAAPI ve QSV için uygulanır. Donanım kod çevrimi için DRI Node ayarlar.",
|
||||
"transcoding_preset_preset": "Ön ayar (-ön)",
|
||||
|
@ -309,7 +313,7 @@
|
|||
"transcoding_reference_frames_description": "Belirli bir kareyi sıkıştırırken referans alınacak kare sayısı. Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. 0 bu değeri otomatik olarak ayarlar.",
|
||||
"transcoding_required_description": "Yalnızca kabul edilen formatta olmayan videolar",
|
||||
"transcoding_settings": "Video Dönüştürme Ayarları",
|
||||
"transcoding_settings_description": "Video dosyalarının çözünürlük ve kodlama bilgilerini yönetir",
|
||||
"transcoding_settings_description": "Hangi videoların dönüştürüleceğini ve nasıl işleneceğini yönetin",
|
||||
"transcoding_target_resolution": "Hedef çözünürlük",
|
||||
"transcoding_target_resolution_description": "Daha yüksek çözünürlükler daha fazla detayı koruyabilir fakat işlemesi daha uzun sürer, dosya boyutu daha yüksek olur ve uygulamanın akıcılığını etkileyebilir.",
|
||||
"transcoding_temporal_aq": "Zamansal AQ",
|
||||
|
@ -519,6 +523,10 @@
|
|||
"date_range": "Tarih aralığı",
|
||||
"day": "Gün",
|
||||
"deduplicate_all": "Tüm kopyaları kaldır",
|
||||
"deduplication_criteria_1": "Resim boyutu (bayt olarak)",
|
||||
"deduplication_criteria_2": "EXIF veri sayısı",
|
||||
"deduplication_info": "Tekilleştirme Bilgileri",
|
||||
"deduplication_info_description": "Varlıkları otomatik olarak önceden seçmek ve yinelenenleri toplu olarak kaldırmak için şunlara bakıyoruz:",
|
||||
"default_locale": "Varsayılan Yerel Ayar",
|
||||
"default_locale_description": "Tarihleri ve sayıları tarayıcınızın yerel ayarına göre biçimlendirin",
|
||||
"delete": "Sil",
|
||||
|
@ -755,6 +763,7 @@
|
|||
"get_help": "Yardım Al",
|
||||
"getting_started": "Başlarken",
|
||||
"go_back": "Geri git",
|
||||
"go_to_folder": "Klasöre git",
|
||||
"go_to_search": "Aramaya git",
|
||||
"group_albums_by": "Albümleri gruplandır...",
|
||||
"group_no": "Gruplama yok",
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
"add_to_album": "加入到相簿",
|
||||
"add_to_shared_album": "加到共享相簿",
|
||||
"add_url": "新增URL",
|
||||
"added_to_archive": "封存",
|
||||
"added_to_archive": "已新增至封存",
|
||||
"added_to_favorites": "加入收藏",
|
||||
"added_to_favorites_count": "將 {count, number} 個項目加入收藏",
|
||||
"admin": {
|
||||
|
@ -289,6 +289,8 @@
|
|||
"transcoding_constant_rate_factor": "恆定速率因子(-crf)",
|
||||
"transcoding_constant_rate_factor_description": "視頻質量級別。典型值為 H.264 的 23、HEVC 的 28、VP9 的 31 和 AV1 的 35。數值越低,質量越高,但會產生較大的文件。",
|
||||
"transcoding_disabled_description": "不轉碼影片,可能會讓某些客戶端無法正常播放",
|
||||
"transcoding_encoding_options": "編碼選項",
|
||||
"transcoding_encoding_options_description": "設定編碼影片的編解碼器、解析度、品質和其他選項",
|
||||
"transcoding_hardware_acceleration": "硬體加速",
|
||||
"transcoding_hardware_acceleration_description": "實驗性功能;速度更快,但在相同比特率下質量較低",
|
||||
"transcoding_hardware_decoding": "硬體解碼",
|
||||
|
@ -301,6 +303,8 @@
|
|||
"transcoding_max_keyframe_interval": "最大關鍵幀間隔",
|
||||
"transcoding_max_keyframe_interval_description": "設置關鍵幀之間的最大幀距。較低的值會降低壓縮效率,但可以改善尋找時間,並可能改善快速運動場景中的質量。0 會自動設置此值。",
|
||||
"transcoding_optimal_description": "高於目標解析度或格式不被支援的影片",
|
||||
"transcoding_policy": "轉碼策略",
|
||||
"transcoding_policy_description": "設定影片進行轉碼的條件",
|
||||
"transcoding_preferred_hardware_device": "首選硬件設備",
|
||||
"transcoding_preferred_hardware_device_description": "僅適用於 VAAPI 和 QSV。設置用於硬件轉碼的 DRI 節點。",
|
||||
"transcoding_preset_preset": "預設值(-preset)",
|
||||
|
@ -519,6 +523,10 @@
|
|||
"date_range": "日期範圍",
|
||||
"day": "日",
|
||||
"deduplicate_all": "刪除所有重複項目",
|
||||
"deduplication_criteria_1": "圖像大小(以位元組為單位)",
|
||||
"deduplication_criteria_2": "EXIF 資料數量",
|
||||
"deduplication_info": "重複資料刪除資訊",
|
||||
"deduplication_info_description": "為了自動預選資產並大量刪除重複項,我們查看:",
|
||||
"default_locale": "預設區域",
|
||||
"default_locale_description": "依瀏覽器區域設定日期和數字格式",
|
||||
"delete": "刪除",
|
||||
|
@ -755,6 +763,7 @@
|
|||
"get_help": "線上求助",
|
||||
"getting_started": "開始使用",
|
||||
"go_back": "返回",
|
||||
"go_to_folder": "轉至資料夾",
|
||||
"go_to_search": "前往搜尋",
|
||||
"group_albums_by": "分類群組的方式...",
|
||||
"group_no": "無分組",
|
||||
|
@ -1141,6 +1150,7 @@
|
|||
"server_version": "目前版本",
|
||||
"set": "設定",
|
||||
"set_as_album_cover": "設爲相簿封面",
|
||||
"set_as_featured_photo": "設為特色照片",
|
||||
"set_as_profile_picture": "設為個人資料圖片",
|
||||
"set_date_of_birth": "設定出生日期",
|
||||
"set_profile_picture": "設置個人資料圖片",
|
||||
|
@ -1191,7 +1201,7 @@
|
|||
"skip_to_tags": "跳轉到標籤",
|
||||
"slideshow": "幻燈片",
|
||||
"slideshow_settings": "幻燈片設定",
|
||||
"sort_albums_by": "排序相簿",
|
||||
"sort_albums_by": "相簿排序依據...",
|
||||
"sort_created": "建立日期",
|
||||
"sort_items": "項目數量",
|
||||
"sort_modified": "日期已修改",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:f997d3f71b7dcff3f937703c02861437f2b41a94e1ddbd1b5fa357ee99f5cce4 AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:adb581d8ed80edd03efd4dcad66db115b9ce8de8522b01720b9f3e6146f0884c AS builder-cpu
|
||||
|
||||
FROM builder-cpu AS builder-openvino
|
||||
|
||||
|
|
|
@ -77,29 +77,31 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
|
|||
async def preload_models(preload: PreloadModelData) -> None:
|
||||
log.info(f"Preloading models: clip:{preload.clip} facial_recognition:{preload.facial_recognition}")
|
||||
|
||||
async def load_models(model_string: str, model_type: ModelType, model_task: ModelTask) -> None:
|
||||
for model_name in model_string.split(","):
|
||||
model_name = model_name.strip()
|
||||
model = await model_cache.get(model_name, model_type, model_task)
|
||||
await load(model)
|
||||
|
||||
if preload.clip.textual is not None:
|
||||
model = await model_cache.get(preload.clip.textual, ModelType.TEXTUAL, ModelTask.SEARCH)
|
||||
await load(model)
|
||||
await load_models(preload.clip.textual, ModelType.TEXTUAL, ModelTask.SEARCH)
|
||||
|
||||
if preload.clip.visual is not None:
|
||||
model = await model_cache.get(preload.clip.visual, ModelType.VISUAL, ModelTask.SEARCH)
|
||||
await load(model)
|
||||
await load_models(preload.clip.visual, ModelType.VISUAL, ModelTask.SEARCH)
|
||||
|
||||
if preload.facial_recognition.detection is not None:
|
||||
model = await model_cache.get(
|
||||
await load_models(
|
||||
preload.facial_recognition.detection,
|
||||
ModelType.DETECTION,
|
||||
ModelTask.FACIAL_RECOGNITION,
|
||||
)
|
||||
await load(model)
|
||||
|
||||
if preload.facial_recognition.recognition is not None:
|
||||
model = await model_cache.get(
|
||||
await load_models(
|
||||
preload.facial_recognition.recognition,
|
||||
ModelType.RECOGNITION,
|
||||
ModelTask.FACIAL_RECOGNITION,
|
||||
)
|
||||
await load(model)
|
||||
|
||||
if preload.clip_fallback is not None:
|
||||
log.warning(
|
||||
|
|
|
@ -10,7 +10,7 @@ from tokenizers import Encoding, Tokenizer
|
|||
|
||||
from app.config import log
|
||||
from app.models.base import InferenceModel
|
||||
from app.models.transforms import clean_text
|
||||
from app.models.transforms import clean_text, serialize_np_array
|
||||
from app.schemas import ModelSession, ModelTask, ModelType
|
||||
|
||||
|
||||
|
@ -18,9 +18,9 @@ class BaseCLIPTextualEncoder(InferenceModel):
|
|||
depends = []
|
||||
identity = (ModelType.TEXTUAL, ModelTask.SEARCH)
|
||||
|
||||
def _predict(self, inputs: str, **kwargs: Any) -> NDArray[np.float32]:
|
||||
def _predict(self, inputs: str, **kwargs: Any) -> str:
|
||||
res: NDArray[np.float32] = self.session.run(None, self.tokenize(inputs))[0][0]
|
||||
return res
|
||||
return serialize_np_array(res)
|
||||
|
||||
def _load(self) -> ModelSession:
|
||||
session = super()._load()
|
||||
|
|
|
@ -10,7 +10,15 @@ from PIL import Image
|
|||
|
||||
from app.config import log
|
||||
from app.models.base import InferenceModel
|
||||
from app.models.transforms import crop_pil, decode_pil, get_pil_resampling, normalize, resize_pil, to_numpy
|
||||
from app.models.transforms import (
|
||||
crop_pil,
|
||||
decode_pil,
|
||||
get_pil_resampling,
|
||||
normalize,
|
||||
resize_pil,
|
||||
serialize_np_array,
|
||||
to_numpy,
|
||||
)
|
||||
from app.schemas import ModelSession, ModelTask, ModelType
|
||||
|
||||
|
||||
|
@ -18,10 +26,10 @@ class BaseCLIPVisualEncoder(InferenceModel):
|
|||
depends = []
|
||||
identity = (ModelType.VISUAL, ModelTask.SEARCH)
|
||||
|
||||
def _predict(self, inputs: Image.Image | bytes, **kwargs: Any) -> NDArray[np.float32]:
|
||||
def _predict(self, inputs: Image.Image | bytes, **kwargs: Any) -> str:
|
||||
image = decode_pil(inputs)
|
||||
res: NDArray[np.float32] = self.session.run(None, self.transform(image))[0][0]
|
||||
return res
|
||||
return serialize_np_array(res)
|
||||
|
||||
@abstractmethod
|
||||
def transform(self, image: Image.Image) -> dict[str, NDArray[np.float32]]:
|
||||
|
|
|
@ -12,7 +12,7 @@ from PIL import Image
|
|||
|
||||
from app.config import log, settings
|
||||
from app.models.base import InferenceModel
|
||||
from app.models.transforms import decode_cv2
|
||||
from app.models.transforms import decode_cv2, serialize_np_array
|
||||
from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelFormat, ModelSession, ModelTask, ModelType
|
||||
|
||||
|
||||
|
@ -61,7 +61,7 @@ class FaceRecognizer(InferenceModel):
|
|||
return [
|
||||
{
|
||||
"boundingBox": {"x1": x1, "y1": y1, "x2": x2, "y2": y2},
|
||||
"embedding": embedding,
|
||||
"embedding": serialize_np_array(embedding),
|
||||
"score": score,
|
||||
}
|
||||
for (x1, y1, x2, y2), embedding, score in zip(faces["boxes"], embeddings, faces["scores"])
|
||||
|
|
|
@ -4,6 +4,7 @@ from typing import IO
|
|||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import orjson
|
||||
from numpy.typing import NDArray
|
||||
from PIL import Image
|
||||
|
||||
|
@ -69,3 +70,9 @@ def clean_text(text: str, canonicalize: bool = False) -> str:
|
|||
if canonicalize:
|
||||
text = text.translate(_PUNCTUATION_TRANS).lower()
|
||||
return text
|
||||
|
||||
|
||||
# this allows the client to use the array as a string without deserializing only to serialize back to a string
|
||||
# TODO: use this in a less invasive way
|
||||
def serialize_np_array(arr: NDArray[np.float32]) -> str:
|
||||
return orjson.dumps(arr, option=orjson.OPT_SERIALIZE_NUMPY).decode()
|
||||
|
|
|
@ -79,7 +79,7 @@ class FaceDetectionOutput(TypedDict):
|
|||
|
||||
class DetectedFace(TypedDict):
|
||||
boundingBox: BoundingBox
|
||||
embedding: npt.NDArray[np.float32]
|
||||
embedding: str
|
||||
score: float
|
||||
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ from unittest import mock
|
|||
import cv2
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
import orjson
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
|
@ -346,11 +347,11 @@ class TestCLIP:
|
|||
mocked.run.return_value = [[self.embedding]]
|
||||
|
||||
clip_encoder = OpenClipVisualEncoder("ViT-B-32__openai", cache_dir="test_cache")
|
||||
embedding = clip_encoder.predict(pil_image)
|
||||
|
||||
assert isinstance(embedding, np.ndarray)
|
||||
assert embedding.shape[0] == clip_model_cfg["embed_dim"]
|
||||
assert embedding.dtype == np.float32
|
||||
embedding_str = clip_encoder.predict(pil_image)
|
||||
assert isinstance(embedding_str, str)
|
||||
embedding = orjson.loads(embedding_str)
|
||||
assert isinstance(embedding, list)
|
||||
assert len(embedding) == clip_model_cfg["embed_dim"]
|
||||
mocked.run.assert_called_once()
|
||||
|
||||
def test_basic_text(
|
||||
|
@ -368,11 +369,11 @@ class TestCLIP:
|
|||
mocker.patch("app.models.clip.textual.Tokenizer.from_file", autospec=True)
|
||||
|
||||
clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache")
|
||||
embedding = clip_encoder.predict("test search query")
|
||||
|
||||
assert isinstance(embedding, np.ndarray)
|
||||
assert embedding.shape[0] == clip_model_cfg["embed_dim"]
|
||||
assert embedding.dtype == np.float32
|
||||
embedding_str = clip_encoder.predict("test search query")
|
||||
assert isinstance(embedding_str, str)
|
||||
embedding = orjson.loads(embedding_str)
|
||||
assert isinstance(embedding, list)
|
||||
assert len(embedding) == clip_model_cfg["embed_dim"]
|
||||
mocked.run.assert_called_once()
|
||||
|
||||
def test_openclip_tokenizer(
|
||||
|
@ -508,8 +509,11 @@ class TestFaceRecognition:
|
|||
assert isinstance(face.get("boundingBox"), dict)
|
||||
assert set(face["boundingBox"]) == {"x1", "y1", "x2", "y2"}
|
||||
assert all(isinstance(val, np.float32) for val in face["boundingBox"].values())
|
||||
assert isinstance(face.get("embedding"), np.ndarray)
|
||||
assert face["embedding"].shape[0] == 512
|
||||
embedding_str = face.get("embedding")
|
||||
assert isinstance(embedding_str, str)
|
||||
embedding = orjson.loads(embedding_str)
|
||||
assert isinstance(embedding, list)
|
||||
assert len(embedding) == 512
|
||||
assert isinstance(face.get("score", None), np.float32)
|
||||
|
||||
rec_model.get_feat.assert_called_once()
|
||||
|
@ -880,8 +884,10 @@ class TestPredictionEndpoints:
|
|||
actual = response.json()
|
||||
assert response.status_code == 200
|
||||
assert isinstance(actual, dict)
|
||||
assert isinstance(actual.get("clip", None), list)
|
||||
assert np.allclose(expected, actual["clip"])
|
||||
embedding = actual.get("clip", None)
|
||||
assert isinstance(embedding, str)
|
||||
parsed_embedding = orjson.loads(embedding)
|
||||
assert np.allclose(expected, parsed_embedding)
|
||||
|
||||
def test_clip_text_endpoint(self, responses: dict[str, Any], deployed_app: TestClient) -> None:
|
||||
expected = responses["clip"]["text"]
|
||||
|
@ -901,8 +907,10 @@ class TestPredictionEndpoints:
|
|||
actual = response.json()
|
||||
assert response.status_code == 200
|
||||
assert isinstance(actual, dict)
|
||||
assert isinstance(actual.get("clip", None), list)
|
||||
assert np.allclose(expected, actual["clip"])
|
||||
embedding = actual.get("clip", None)
|
||||
assert isinstance(embedding, str)
|
||||
parsed_embedding = orjson.loads(embedding)
|
||||
assert np.allclose(expected, parsed_embedding)
|
||||
|
||||
def test_face_endpoint(self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient) -> None:
|
||||
byte_image = BytesIO()
|
||||
|
@ -933,5 +941,8 @@ class TestPredictionEndpoints:
|
|||
|
||||
for expected_face, actual_face in zip(responses["facial-recognition"], actual["facial-recognition"]):
|
||||
assert expected_face["boundingBox"] == actual_face["boundingBox"]
|
||||
assert np.allclose(expected_face["embedding"], actual_face["embedding"])
|
||||
embedding = actual_face.get("embedding", None)
|
||||
assert isinstance(embedding, str)
|
||||
parsed_embedding = orjson.loads(embedding)
|
||||
assert np.allclose(expected_face["embedding"], parsed_embedding)
|
||||
assert np.allclose(expected_face["score"], actual_face["score"])
|
||||
|
|
64
machine-learning/poetry.lock
generated
64
machine-learning/poetry.lock
generated
|
@ -1625,13 +1625,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
|||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.32.5"
|
||||
version = "2.32.6"
|
||||
description = "Developer-friendly load testing framework"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "locust-2.32.5-py3-none-any.whl", hash = "sha256:2f49509868ffc2e368be40921c6825f92147c84e997206760a85dab3058f5efb"},
|
||||
{file = "locust-2.32.5.tar.gz", hash = "sha256:ea7bc1e8ce2520e8893c471b4b0a56a4f53b01b4b618adfe8d2c8ab2728b5821"},
|
||||
{file = "locust-2.32.6-py3-none-any.whl", hash = "sha256:d5c0e4f73134415d250087034431cf3ea42ca695d3dee7f10812287cacb6c4ef"},
|
||||
{file = "locust-2.32.6.tar.gz", hash = "sha256:6600cc308398e724764aacc56ccddf6cfcd0127c4c92dedd5c4979dd37ef5b15"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1649,8 +1649,8 @@ psutil = ">=5.9.1"
|
|||
pywin32 = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
pyzmq = ">=25.0.0"
|
||||
requests = [
|
||||
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
|
||||
{version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""},
|
||||
{version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""},
|
||||
]
|
||||
setuptools = ">=70.0.0"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
|
@ -2165,26 +2165,26 @@ sympy = "*"
|
|||
|
||||
[[package]]
|
||||
name = "opencv-python-headless"
|
||||
version = "4.10.0.84"
|
||||
version = "4.11.0.86"
|
||||
description = "Wrapper package for OpenCV python bindings."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "opencv-python-headless-4.10.0.84.tar.gz", hash = "sha256:f2017c6101d7c2ef8d7bc3b414c37ff7f54d64413a1847d89970b6b7069b4e1a"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a4f4bcb07d8f8a7704d9c8564c224c8b064c63f430e95b61ac0bffaa374d330e"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:5ae454ebac0eb0a0b932e3406370aaf4212e6a3fdb5038cc86c7aea15a6851da"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46071015ff9ab40fccd8a163da0ee14ce9846349f06c6c8c0f2870856ffa45db"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:377d08a7e48a1405b5e84afcbe4798464ce7ee17081c1c23619c8b398ff18295"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:9092404b65458ed87ce932f613ffbb1106ed2c843577501e5768912360fc50ec"},
|
||||
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:afcf28bd1209dd58810d33defb622b325d3cbe49dcd7a43a902982c33e5fad05"},
|
||||
{file = "opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:48128188ade4a7e517237c8e1e11a9cdf5c282761473383e77beb875bb1e61ca"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:a66c1b286a9de872c343ee7c3553b084244299714ebb50fbdcd76f07ebbe6c81"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6efabcaa9df731f29e5ea9051776715b1bdd1845d7c9530065c7951d2a2899eb"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e0a27c19dd1f40ddff94976cfe43066fbbe9dfbb2ec1907d66c19caef42a57b"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:f447d8acbb0b6f2808da71fddd29c1cdd448d2bc98f72d9bb78a7a898fc9621b"},
|
||||
{file = "opencv_python_headless-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:6c304df9caa7a6a5710b91709dd4786bf20a74d57672b3c31f7033cc638174ca"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
numpy = [
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
{version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
|
||||
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""},
|
||||
{version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
|
||||
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3049,29 +3049,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.1"
|
||||
version = "0.9.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"},
|
||||
{file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"},
|
||||
{file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"},
|
||||
{file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"},
|
||||
{file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"},
|
||||
{file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"},
|
||||
{file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"},
|
||||
{file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"},
|
||||
{file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"},
|
||||
{file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"},
|
||||
{file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"},
|
||||
{file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"},
|
||||
{file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -14,6 +14,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
|
|||
final List<AssetPathEntity> assetPathEntities =
|
||||
await PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
filterOption: FilterOptionGroup(containsPathModified: true),
|
||||
);
|
||||
return assetPathEntities.map(_toAlbum).toList();
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
22.13.0
|
||||
22.13.1
|
||||
|
|
8
open-api/typescript-sdk/package-lock.json
generated
8
open-api/typescript-sdk/package-lock.json
generated
|
@ -12,7 +12,7 @@
|
|||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
|
@ -22,9 +22,9 @@
|
|||
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -28,6 +28,6 @@
|
|||
"directory": "open-api/typescript-sdk"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
22.13.0
|
||||
22.13.1
|
||||
|
|
8
server/package-lock.json
generated
8
server/package-lock.json
generated
|
@ -86,7 +86,7 @@
|
|||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
@ -5129,9 +5129,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
@ -145,6 +145,6 @@
|
|||
"vitest": "^2.0.5"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.13.0"
|
||||
"node": "22.13.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { getAssetFiles } from 'src/utils/asset.util';
|
||||
import { getConfig } from 'src/utils/config';
|
||||
|
||||
|
|
|
@ -100,6 +100,7 @@ export const DummyValue = {
|
|||
DATE: new Date(),
|
||||
TIME_BUCKET: '2024-01-01T00:00:00.000Z',
|
||||
BOOLEAN: true,
|
||||
VECTOR: '[1, 2, 3]',
|
||||
};
|
||||
|
||||
export const GENERATE_SQL_KEY = 'generate-sql-key';
|
||||
|
|
|
@ -29,7 +29,7 @@ export class AddUsersDto {
|
|||
albumUsers!: AlbumUserAddDto[];
|
||||
}
|
||||
|
||||
class AlbumUserCreateDto {
|
||||
export class AlbumUserCreateDto {
|
||||
@ValidateUUID()
|
||||
userId!: string;
|
||||
|
||||
|
|
|
@ -11,10 +11,6 @@ export class FaceSearchEntity {
|
|||
faceId!: string;
|
||||
|
||||
@Index('face_index', { synchronize: false })
|
||||
@Column({
|
||||
type: 'float4',
|
||||
array: true,
|
||||
transformer: { from: JSON.parse, to: (v) => `[${v}]` },
|
||||
})
|
||||
embedding!: number[];
|
||||
@Column({ type: 'float4', array: true })
|
||||
embedding!: string;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,6 @@ export class SmartSearchEntity {
|
|||
assetId!: string;
|
||||
|
||||
@Index('clip_index', { synchronize: false })
|
||||
@Column({ type: 'float4', array: true, transformer: { from: JSON.parse, to: (v) => v } })
|
||||
embedding!: number[];
|
||||
@Column({ type: 'float4', array: true })
|
||||
embedding!: string;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { SystemConfig } from 'src/config';
|
||||
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||
import { Column, DeepPartial, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { DeepPartial } from 'src/types';
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('system_metadata')
|
||||
export class SystemMetadataEntity<T extends keyof SystemMetadata = SystemMetadataKey> {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { UserAvatarColor, UserMetadataKey } from 'src/enum';
|
||||
import { DeepPartial } from 'src/types';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
import { Column, DeepPartial, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('user_metadata')
|
||||
export class UserMetadataEntity<T extends keyof UserMetadata = UserMetadataKey> {
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import { AlbumUserRole } from 'src/enum';
|
||||
|
||||
export const IAccessRepository = 'IAccessRepository';
|
||||
|
||||
export interface IAccessRepository {
|
||||
activity: {
|
||||
checkOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>>;
|
||||
checkAlbumOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>>;
|
||||
checkCreateAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
asset: {
|
||||
checkOwnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>>;
|
||||
checkAlbumAccess(userId: string, assetIds: Set<string>): Promise<Set<string>>;
|
||||
checkPartnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>>;
|
||||
checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
authDevice: {
|
||||
checkOwnerAccess(userId: string, deviceIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
album: {
|
||||
checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>;
|
||||
checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>>;
|
||||
checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
timeline: {
|
||||
checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
memory: {
|
||||
checkOwnerAccess(userId: string, memoryIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
person: {
|
||||
checkFaceOwnerAccess(userId: string, assetFaceId: Set<string>): Promise<Set<string>>;
|
||||
checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
partner: {
|
||||
checkUpdateAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
stack: {
|
||||
checkOwnerAccess(userId: string, stackIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
tag: {
|
||||
checkOwnerAccess(userId: string, tagIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
import { Insertable, Updateable } from 'kysely';
|
||||
import { Albums } from 'src/db';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { IBulkAsset } from 'src/utils/asset.util';
|
||||
|
||||
|
@ -15,7 +18,7 @@ export interface AlbumInfoOptions {
|
|||
}
|
||||
|
||||
export interface IAlbumRepository extends IBulkAsset {
|
||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
|
||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined>;
|
||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||
removeAsset(assetId: string): Promise<void>;
|
||||
getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||
|
@ -25,8 +28,8 @@ export interface IAlbumRepository extends IBulkAsset {
|
|||
restoreAll(userId: string): Promise<void>;
|
||||
softDeleteAll(userId: string): Promise<void>;
|
||||
deleteAll(userId: string): Promise<void>;
|
||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
|
||||
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity>;
|
||||
update(id: string, album: Updateable<Albums>): Promise<AlbumEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
updateThumbnails(): Promise<number | undefined>;
|
||||
}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import { Insertable } from 'kysely';
|
||||
import { ApiKeys } from 'src/db';
|
||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { AuthApiKey } from 'src/types';
|
||||
|
||||
export const IKeyRepository = 'IKeyRepository';
|
||||
|
||||
export interface IKeyRepository {
|
||||
create(dto: Insertable<ApiKeys>): Promise<APIKeyEntity>;
|
||||
update(userId: string, id: string, dto: Partial<APIKeyEntity>): Promise<APIKeyEntity>;
|
||||
delete(userId: string, id: string): Promise<void>;
|
||||
/**
|
||||
* Includes the hashed `key` for verification
|
||||
* @param id
|
||||
*/
|
||||
getKey(hashedToken: string): Promise<AuthApiKey | undefined>;
|
||||
getById(userId: string, id: string): Promise<APIKeyEntity | null>;
|
||||
getByUserId(userId: string): Promise<APIKeyEntity[]>;
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import { DatabaseAction, EntityType } from 'src/enum';
|
||||
|
||||
export const IAuditRepository = 'IAuditRepository';
|
||||
|
||||
export interface AuditSearch {
|
||||
action?: DatabaseAction;
|
||||
entityType?: EntityType;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface IAuditRepository {
|
||||
getAfter(since: Date, options: AuditSearch): Promise<string[]>;
|
||||
removeBefore(before: Date): Promise<void>;
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
import { RegisterQueueOptions } from '@nestjs/bullmq';
|
||||
import { QueueOptions } from 'bullmq';
|
||||
import { RedisOptions } from 'ioredis';
|
||||
import { KyselyConfig } from 'kysely';
|
||||
import { ClsModuleOptions } from 'nestjs-cls';
|
||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||
import { ImmichEnvironment, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
|
||||
import { DatabaseConnectionParams, VectorExtension } from 'src/interfaces/database.interface';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
||||
|
||||
export const IConfigRepository = 'IConfigRepository';
|
||||
|
||||
export interface EnvData {
|
||||
host?: string;
|
||||
port: number;
|
||||
environment: ImmichEnvironment;
|
||||
configFile?: string;
|
||||
logLevel?: LogLevel;
|
||||
|
||||
buildMetadata: {
|
||||
build?: string;
|
||||
buildUrl?: string;
|
||||
buildImage?: string;
|
||||
buildImageUrl?: string;
|
||||
repository?: string;
|
||||
repositoryUrl?: string;
|
||||
sourceRef?: string;
|
||||
sourceCommit?: string;
|
||||
sourceUrl?: string;
|
||||
thirdPartySourceUrl?: string;
|
||||
thirdPartyBugFeatureUrl?: string;
|
||||
thirdPartyDocumentationUrl?: string;
|
||||
thirdPartySupportUrl?: string;
|
||||
};
|
||||
|
||||
bull: {
|
||||
config: QueueOptions;
|
||||
queues: RegisterQueueOptions[];
|
||||
};
|
||||
|
||||
cls: {
|
||||
config: ClsModuleOptions;
|
||||
};
|
||||
|
||||
database: {
|
||||
config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: KyselyConfig };
|
||||
skipMigrations: boolean;
|
||||
vectorExtension: VectorExtension;
|
||||
};
|
||||
|
||||
licensePublicKey: {
|
||||
client: string;
|
||||
server: string;
|
||||
};
|
||||
|
||||
network: {
|
||||
trustedProxies: string[];
|
||||
};
|
||||
|
||||
otel: OpenTelemetryModuleOptions;
|
||||
|
||||
resourcePaths: {
|
||||
lockFile: string;
|
||||
geodata: {
|
||||
dateFile: string;
|
||||
admin1: string;
|
||||
admin2: string;
|
||||
cities500: string;
|
||||
naturalEarthCountriesPath: string;
|
||||
};
|
||||
web: {
|
||||
root: string;
|
||||
indexHtml: string;
|
||||
};
|
||||
};
|
||||
|
||||
redis: RedisOptions;
|
||||
|
||||
telemetry: {
|
||||
apiPort: number;
|
||||
microservicesPort: number;
|
||||
metrics: Set<ImmichTelemetry>;
|
||||
};
|
||||
|
||||
storage: {
|
||||
ignoreMountCheckErrors: boolean;
|
||||
};
|
||||
|
||||
workers: ImmichWorker[];
|
||||
|
||||
noColor: boolean;
|
||||
nodeVersion?: string;
|
||||
}
|
||||
|
||||
export interface IConfigRepository {
|
||||
getEnv(): EnvData;
|
||||
getWorker(): ImmichWorker | undefined;
|
||||
}
|
|
@ -28,10 +28,10 @@ export type FaceDetectionOptions = ModelOptions & { minScore: number };
|
|||
|
||||
type VisualResponse = { imageHeight: number; imageWidth: number };
|
||||
export type ClipVisualRequest = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: ModelOptions } };
|
||||
export type ClipVisualResponse = { [ModelTask.SEARCH]: number[] } & VisualResponse;
|
||||
export type ClipVisualResponse = { [ModelTask.SEARCH]: string } & VisualResponse;
|
||||
|
||||
export type ClipTextualRequest = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: ModelOptions } };
|
||||
export type ClipTextualResponse = { [ModelTask.SEARCH]: number[] };
|
||||
export type ClipTextualResponse = { [ModelTask.SEARCH]: string };
|
||||
|
||||
export type FacialRecognitionRequest = {
|
||||
[ModelTask.FACIAL_RECOGNITION]: {
|
||||
|
@ -42,7 +42,7 @@ export type FacialRecognitionRequest = {
|
|||
|
||||
export interface Face {
|
||||
boundingBox: BoundingBox;
|
||||
embedding: number[];
|
||||
embedding: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ export type DetectedFaces = { faces: Face[] } & VisualResponse;
|
|||
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
|
||||
|
||||
export interface IMachineLearningRepository {
|
||||
encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise<number[]>;
|
||||
encodeText(urls: string[], text: string, config: ModelOptions): Promise<number[]>;
|
||||
encodeImage(urls: string[], imagePath: string, config: ModelOptions): Promise<string>;
|
||||
encodeText(urls: string[], text: string, config: ModelOptions): Promise<string>;
|
||||
detectFaces(urls: string[], imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Insertable, Updateable } from 'kysely';
|
||||
import { AssetFaces, FaceSearch, Person } from 'src/db';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { FaceSearchEntity } from 'src/entities/face-search.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { SourceType } from 'src/enum';
|
||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
||||
import { FindOptionsRelations } from 'typeorm';
|
||||
|
||||
export const IPersonRepository = 'IPersonRepository';
|
||||
|
||||
|
@ -48,29 +49,31 @@ export interface DeleteFacesOptions {
|
|||
|
||||
export type UnassignFacesOptions = DeleteFacesOptions;
|
||||
|
||||
export type SelectFaceOptions = Partial<{ [K in keyof AssetFaceEntity]: boolean }>;
|
||||
|
||||
export interface IPersonRepository {
|
||||
getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
|
||||
getAll(options?: Partial<PersonEntity>): AsyncIterableIterator<PersonEntity>;
|
||||
getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
|
||||
getAllWithoutFaces(): Promise<PersonEntity[]>;
|
||||
getById(personId: string): Promise<PersonEntity | null>;
|
||||
getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
|
||||
getDistinctNames(userId: string, options: PersonNameSearchOptions): Promise<PersonNameResponse[]>;
|
||||
|
||||
create(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||
createAll(people: Partial<PersonEntity>[]): Promise<string[]>;
|
||||
create(person: Insertable<Person>): Promise<PersonEntity>;
|
||||
createAll(people: Insertable<Person>[]): Promise<string[]>;
|
||||
delete(entities: PersonEntity[]): Promise<void>;
|
||||
deleteFaces(options: DeleteFacesOptions): Promise<void>;
|
||||
refreshFaces(
|
||||
facesToAdd: Partial<AssetFaceEntity>[],
|
||||
facesToAdd: Insertable<AssetFaces>[],
|
||||
faceIdsToRemove: string[],
|
||||
embeddingsToAdd?: FaceSearchEntity[],
|
||||
embeddingsToAdd?: Insertable<FaceSearch>[],
|
||||
): Promise<void>;
|
||||
getAllFaces(pagination: PaginationOptions, options?: FindManyOptions<AssetFaceEntity>): Paginated<AssetFaceEntity>;
|
||||
getAllFaces(options?: Partial<AssetFaceEntity>): AsyncIterableIterator<AssetFaceEntity>;
|
||||
getFaceById(id: string): Promise<AssetFaceEntity>;
|
||||
getFaceByIdWithAssets(
|
||||
id: string,
|
||||
relations?: FindOptionsRelations<AssetFaceEntity>,
|
||||
select?: FindOptionsSelect<AssetFaceEntity>,
|
||||
select?: SelectFaceOptions,
|
||||
): Promise<AssetFaceEntity | null>;
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]>;
|
||||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
|
||||
|
@ -80,7 +83,7 @@ export interface IPersonRepository {
|
|||
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||
unassignFaces(options: UnassignFacesOptions): Promise<void>;
|
||||
update(person: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||
updateAll(people: Partial<PersonEntity>[]): Promise<void>;
|
||||
update(person: Updateable<Person> & { id: string }): Promise<PersonEntity>;
|
||||
updateAll(people: Insertable<Person>[]): Promise<void>;
|
||||
getLatestFaceDate(): Promise<string | undefined>;
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ export interface SearchExifOptions {
|
|||
}
|
||||
|
||||
export interface SearchEmbeddingOptions {
|
||||
embedding: number[];
|
||||
embedding: string;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
|
@ -152,7 +152,7 @@ export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
|||
|
||||
export interface AssetDuplicateSearch {
|
||||
assetId: string;
|
||||
embedding: number[];
|
||||
embedding: string;
|
||||
maxDistance: number;
|
||||
type: AssetType;
|
||||
userIds: string[];
|
||||
|
@ -192,7 +192,7 @@ export interface ISearchRepository {
|
|||
searchDuplicates(options: AssetDuplicateSearch): Promise<AssetDuplicateResult[]>;
|
||||
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
||||
searchRandom(size: number, options: AssetSearchOptions): Promise<AssetEntity[]>;
|
||||
upsert(assetId: string, embedding: number[]): Promise<void>;
|
||||
upsert(assetId: string, embedding: string): Promise<void>;
|
||||
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
||||
getAssetsByCity(userIds: string[]): Promise<AssetEntity[]>;
|
||||
deleteAllSearchEmbeddings(): Promise<void>;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Updateable } from 'kysely';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
|
||||
export const IStackRepository = 'IStackRepository';
|
||||
|
@ -10,8 +11,8 @@ export interface StackSearch {
|
|||
export interface IStackRepository {
|
||||
search(query: StackSearch): Promise<StackEntity[]>;
|
||||
create(stack: { ownerId: string; assetIds: string[] }): Promise<StackEntity>;
|
||||
update(stack: Pick<StackEntity, 'id'> & Partial<StackEntity>): Promise<StackEntity>;
|
||||
update(id: string, entity: Updateable<StackEntity>): Promise<StackEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
deleteAll(ids: string[]): Promise<void>;
|
||||
getById(id: string): Promise<StackEntity | null>;
|
||||
getById(id: string): Promise<StackEntity | undefined>;
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export const IViewRepository = 'IViewRepository';
|
||||
|
||||
export interface IViewRepository {
|
||||
getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]>;
|
||||
getUniqueOriginalPaths(userId: string): Promise<string[]>;
|
||||
}
|
|
@ -3,7 +3,7 @@ import { IoAdapter } from '@nestjs/platform-socket.io';
|
|||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import { Redis } from 'ioredis';
|
||||
import { ServerOptions } from 'socket.io';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
export class WebSocketAdapter extends IoAdapter {
|
||||
constructor(private app: INestApplicationContext) {
|
||||
|
@ -11,7 +11,7 @@ export class WebSocketAdapter extends IoAdapter {
|
|||
}
|
||||
|
||||
createIOServer(port: number, options?: ServerOptions): any {
|
||||
const { redis } = this.app.get<IConfigRepository>(IConfigRepository).getEnv();
|
||||
const { redis } = this.app.get(ConfigRepository).getEnv();
|
||||
const server = super.createIOServer(port, options);
|
||||
const pubClient = new Redis(redis);
|
||||
const subClient = pubClient.duplicate();
|
||||
|
|
|
@ -1,460 +1,490 @@
|
|||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- AlbumRepository.getById
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AlbumEntity_id" AS "ids_AlbumEntity_id"
|
||||
FROM
|
||||
select
|
||||
"albums".*,
|
||||
(
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
|
||||
AND (
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id"
|
||||
WHERE
|
||||
((("AlbumEntity"."id" = $1)))
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AlbumEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"albums"."id" = $1
|
||||
and "albums"."deletedAt" is null
|
||||
|
||||
-- AlbumRepository.getByAssetId
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
|
||||
AND (
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "albums_assets_assets" "AlbumEntity_AlbumEntity__AlbumEntity_assets" ON "AlbumEntity_AlbumEntity__AlbumEntity_assets"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "assets" "AlbumEntity__AlbumEntity_assets" ON "AlbumEntity__AlbumEntity_assets"."id" = "AlbumEntity_AlbumEntity__AlbumEntity_assets"."assetsId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_assets"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
select
|
||||
"albums".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
left join "albums_shared_users_users" as "album_users" on "album_users"."albumsId" = "albums"."id"
|
||||
where
|
||||
(
|
||||
(
|
||||
(
|
||||
(
|
||||
("AlbumEntity"."ownerId" = $1)
|
||||
AND ((("AlbumEntity__AlbumEntity_assets"."id" = $2)))
|
||||
)
|
||||
)
|
||||
OR (
|
||||
(
|
||||
(
|
||||
(
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" = $3
|
||||
)
|
||||
)
|
||||
)
|
||||
AND ((("AlbumEntity__AlbumEntity_assets"."id" = $4)))
|
||||
)
|
||||
)
|
||||
"albums"."ownerId" = $1
|
||||
and "album_assets"."assetsId" = $2
|
||||
)
|
||||
or (
|
||||
"album_users"."usersId" = $3
|
||||
and "album_assets"."assetsId" = $4
|
||||
)
|
||||
)
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"AlbumEntity"."createdAt" DESC
|
||||
and "albums"."deletedAt" is null
|
||||
order by
|
||||
"albums"."createdAt" desc,
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getMetadataForIds
|
||||
SELECT
|
||||
"album"."id" AS "album_id",
|
||||
MIN("assets"."fileCreatedAt") AS "start_date",
|
||||
MAX("assets"."fileCreatedAt") AS "end_date",
|
||||
COUNT("assets"."id") AS "asset_count"
|
||||
FROM
|
||||
"albums" "album"
|
||||
LEFT JOIN "albums_assets_assets" "album_assets" ON "album_assets"."albumsId" = "album"."id"
|
||||
LEFT JOIN "assets" "assets" ON "assets"."id" = "album_assets"."assetsId"
|
||||
AND "assets"."deletedAt" IS NULL
|
||||
WHERE
|
||||
("album"."id" IN ($1))
|
||||
AND ("album"."deletedAt" IS NULL)
|
||||
GROUP BY
|
||||
"album"."id"
|
||||
select
|
||||
"albums"."id",
|
||||
min("assets"."fileCreatedAt") as "startDate",
|
||||
max("assets"."fileCreatedAt") as "endDate",
|
||||
count("assets"."id") as "assetCount"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_assets_assets" as "album_assets" on "album_assets"."albumsId" = "albums"."id"
|
||||
left join "assets" on "assets"."id" = "album_assets"."assetsId"
|
||||
where
|
||||
"albums"."id" in ($1)
|
||||
group by
|
||||
"albums"."id"
|
||||
|
||||
-- AlbumRepository.getOwned
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
|
||||
AND (
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
((("AlbumEntity"."ownerId" = $1)))
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"AlbumEntity"."createdAt" DESC
|
||||
select
|
||||
"albums".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"albums"
|
||||
where
|
||||
"albums"."ownerId" = $1
|
||||
and "albums"."deletedAt" is null
|
||||
order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getShared
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId"
|
||||
AND (
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
select distinct
|
||||
on ("albums"."createdAt") "albums".*,
|
||||
(
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" = $1
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
OR (
|
||||
(
|
||||
(
|
||||
(
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" = $2
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
OR (
|
||||
(
|
||||
("AlbumEntity"."ownerId" = $3)
|
||||
AND (
|
||||
(
|
||||
(
|
||||
NOT (
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" IS NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id"
|
||||
left join "shared_links" on "shared_links"."albumId" = "albums"."id"
|
||||
where
|
||||
(
|
||||
"shared_albums"."usersId" = $1
|
||||
or "shared_links"."userId" = $2
|
||||
or (
|
||||
"albums"."ownerId" = $3
|
||||
and "shared_albums"."usersId" is not null
|
||||
)
|
||||
)
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"AlbumEntity"."createdAt" DESC
|
||||
and "albums"."deletedAt" is null
|
||||
order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getNotShared
|
||||
SELECT
|
||||
"AlbumEntity"."id" AS "AlbumEntity_id",
|
||||
"AlbumEntity"."ownerId" AS "AlbumEntity_ownerId",
|
||||
"AlbumEntity"."albumName" AS "AlbumEntity_albumName",
|
||||
"AlbumEntity"."description" AS "AlbumEntity_description",
|
||||
"AlbumEntity"."createdAt" AS "AlbumEntity_createdAt",
|
||||
"AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt",
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."userId" AS "AlbumEntity__AlbumEntity_sharedLinks_userId",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."key" AS "AlbumEntity__AlbumEntity_sharedLinks_key",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."type" AS "AlbumEntity__AlbumEntity_sharedLinks_type",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."createdAt" AS "AlbumEntity__AlbumEntity_sharedLinks_createdAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."expiresAt" AS "AlbumEntity__AlbumEntity_sharedLinks_expiresAt",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowUpload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowUpload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."allowDownload" AS "AlbumEntity__AlbumEntity_sharedLinks_allowDownload",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."showExif" AS "AlbumEntity__AlbumEntity_sharedLinks_showExif",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
"AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath",
|
||||
"AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword",
|
||||
"AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt"
|
||||
FROM
|
||||
"albums" "AlbumEntity"
|
||||
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "shared_links" "AlbumEntity__AlbumEntity_sharedLinks" ON "AlbumEntity__AlbumEntity_sharedLinks"."albumId" = "AlbumEntity"."id"
|
||||
LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId"
|
||||
AND (
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
select distinct
|
||||
on ("albums"."createdAt") "albums".*,
|
||||
(
|
||||
(
|
||||
("AlbumEntity"."ownerId" = $1)
|
||||
AND (
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"album_users".*,
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."usersId" IS NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
AND (
|
||||
(
|
||||
(
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" IS NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
AND ("AlbumEntity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"AlbumEntity"."createdAt" DESC
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "album_users"."usersId"
|
||||
) as obj
|
||||
) as "user"
|
||||
from
|
||||
"albums_shared_users_users" as "album_users"
|
||||
where
|
||||
"album_users"."albumsId" = "albums"."id"
|
||||
) as agg
|
||||
) as "albumUsers",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"id",
|
||||
"email",
|
||||
"createdAt",
|
||||
"profileImagePath",
|
||||
"isAdmin",
|
||||
"shouldChangePassword",
|
||||
"deletedAt",
|
||||
"oauthId",
|
||||
"updatedAt",
|
||||
"storageLabel",
|
||||
"name",
|
||||
"quotaSizeInBytes",
|
||||
"quotaUsageInBytes",
|
||||
"status",
|
||||
"profileChangedAt"
|
||||
from
|
||||
"users"
|
||||
where
|
||||
"users"."id" = "albums"."ownerId"
|
||||
) as obj
|
||||
) as "owner",
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"shared_links"
|
||||
where
|
||||
"shared_links"."albumId" = "albums"."id"
|
||||
) as agg
|
||||
) as "sharedLinks"
|
||||
from
|
||||
"albums"
|
||||
left join "albums_shared_users_users" as "shared_albums" on "shared_albums"."albumsId" = "albums"."id"
|
||||
left join "shared_links" on "shared_links"."albumId" = "albums"."id"
|
||||
where
|
||||
"albums"."ownerId" = $1
|
||||
and "shared_albums"."usersId" is null
|
||||
and "shared_links"."userId" is null
|
||||
and "albums"."deletedAt" is null
|
||||
order by
|
||||
"albums"."createdAt" desc
|
||||
|
||||
-- AlbumRepository.getAssetIds
|
||||
SELECT
|
||||
"albums_assets"."assetsId" AS "assetId"
|
||||
FROM
|
||||
"albums_assets_assets" "albums_assets"
|
||||
WHERE
|
||||
"albums_assets"."albumsId" = $1
|
||||
AND "albums_assets"."assetsId" IN ($2)
|
||||
select
|
||||
*
|
||||
from
|
||||
"albums_assets_assets"
|
||||
where
|
||||
"albums_assets_assets"."albumsId" = $1
|
||||
and "albums_assets_assets"."assetsId" in ($2)
|
||||
|
|
|
@ -1,342 +1,252 @@
|
|||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- PersonRepository.reassignFaces
|
||||
UPDATE "asset_faces"
|
||||
SET
|
||||
update "asset_faces"
|
||||
set
|
||||
"personId" = $1
|
||||
WHERE
|
||||
"personId" = $2
|
||||
where
|
||||
"asset_faces"."personId" = $2
|
||||
|
||||
-- PersonRepository.getAllForUser
|
||||
SELECT
|
||||
"person"."id" AS "person_id",
|
||||
"person"."createdAt" AS "person_createdAt",
|
||||
"person"."updatedAt" AS "person_updatedAt",
|
||||
"person"."ownerId" AS "person_ownerId",
|
||||
"person"."name" AS "person_name",
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
FROM
|
||||
"person" "person"
|
||||
INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"person"."ownerId" = $1
|
||||
AND "asset"."isArchived" = false
|
||||
AND "person"."isHidden" = false
|
||||
GROUP BY
|
||||
"person"."id"
|
||||
HAVING
|
||||
"person"."name" != ''
|
||||
OR COUNT("face"."assetId") >= $2
|
||||
ORDER BY
|
||||
"person"."isHidden" ASC,
|
||||
NULLIF("person"."name", '') IS NULL ASC,
|
||||
COUNT("face"."assetId") DESC,
|
||||
NULLIF("person"."name", '') ASC NULLS LAST,
|
||||
"person"."createdAt" ASC
|
||||
LIMIT
|
||||
11
|
||||
OFFSET
|
||||
10
|
||||
-- PersonRepository.unassignFaces
|
||||
update "asset_faces"
|
||||
set
|
||||
"personId" = $1
|
||||
where
|
||||
"asset_faces"."sourceType" = $2
|
||||
VACUUM
|
||||
ANALYZE asset_faces,
|
||||
face_search,
|
||||
person
|
||||
REINDEX TABLE asset_faces
|
||||
REINDEX TABLE person
|
||||
|
||||
-- PersonRepository.delete
|
||||
delete from "person"
|
||||
where
|
||||
"person"."id" in ($1)
|
||||
|
||||
-- PersonRepository.deleteFaces
|
||||
delete from "asset_faces"
|
||||
where
|
||||
"asset_faces"."sourceType" = $1
|
||||
VACUUM
|
||||
ANALYZE asset_faces,
|
||||
face_search,
|
||||
person
|
||||
REINDEX TABLE asset_faces
|
||||
REINDEX TABLE person
|
||||
|
||||
-- PersonRepository.getAllWithoutFaces
|
||||
SELECT
|
||||
"person"."id" AS "person_id",
|
||||
"person"."createdAt" AS "person_createdAt",
|
||||
"person"."updatedAt" AS "person_updatedAt",
|
||||
"person"."ownerId" AS "person_ownerId",
|
||||
"person"."name" AS "person_name",
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
FROM
|
||||
"person" "person"
|
||||
LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
GROUP BY
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
left join "asset_faces" on "asset_faces"."personId" = "person"."id"
|
||||
group by
|
||||
"person"."id"
|
||||
HAVING
|
||||
COUNT("face"."assetId") = 0
|
||||
having
|
||||
count("asset_faces"."assetId") = $1
|
||||
|
||||
-- PersonRepository.getFaces
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
WHERE
|
||||
(("AssetFaceEntity"."assetId" = $1))
|
||||
ORDER BY
|
||||
"AssetFaceEntity"."boundingBoxX1" ASC
|
||||
select
|
||||
"asset_faces".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."id" = "asset_faces"."personId"
|
||||
) as obj
|
||||
) as "person"
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."assetId" = $1
|
||||
order by
|
||||
"asset_faces"."boundingBoxX1" asc
|
||||
|
||||
-- PersonRepository.getFaceById
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id"
|
||||
FROM
|
||||
select
|
||||
"asset_faces".*,
|
||||
(
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
WHERE
|
||||
(("AssetFaceEntity"."id" = $1))
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AssetFaceEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."id" = "asset_faces"."personId"
|
||||
) as obj
|
||||
) as "person"
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."id" = $1
|
||||
|
||||
-- PersonRepository.getFaceByIdWithAssets
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id"
|
||||
FROM
|
||||
select
|
||||
"asset_faces".*,
|
||||
(
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."duplicateId" AS "AssetFaceEntity__AssetFaceEntity_asset_duplicateId"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
|
||||
AND (
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
(("AssetFaceEntity"."id" = $1))
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AssetFaceEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."id" = "asset_faces"."personId"
|
||||
) as obj
|
||||
) as "person",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"assets".*
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."id" = "asset_faces"."assetId"
|
||||
) as obj
|
||||
) as "asset"
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."id" = $1
|
||||
|
||||
-- PersonRepository.reassignFace
|
||||
UPDATE "asset_faces"
|
||||
SET
|
||||
update "asset_faces"
|
||||
set
|
||||
"personId" = $1
|
||||
WHERE
|
||||
"id" = $2
|
||||
where
|
||||
"asset_faces"."id" = $2
|
||||
|
||||
-- PersonRepository.getByName
|
||||
SELECT
|
||||
"person"."id" AS "person_id",
|
||||
"person"."createdAt" AS "person_createdAt",
|
||||
"person"."updatedAt" AS "person_updatedAt",
|
||||
"person"."ownerId" AS "person_ownerId",
|
||||
"person"."name" AS "person_name",
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
FROM
|
||||
"person" "person"
|
||||
WHERE
|
||||
"person"."ownerId" = $1
|
||||
AND (
|
||||
LOWER("person"."name") LIKE $2
|
||||
OR LOWER("person"."name") LIKE $3
|
||||
)
|
||||
LIMIT
|
||||
1000
|
||||
|
||||
-- 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
|
||||
SELECT
|
||||
COUNT(DISTINCT ("asset"."id")) AS "count"
|
||||
FROM
|
||||
"asset_faces" "face"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"face"."personId" = $1
|
||||
AND "asset"."isArchived" = false
|
||||
AND "asset"."deletedAt" IS NULL
|
||||
AND "asset"."livePhotoVideoId" IS NULL
|
||||
|
||||
-- PersonRepository.getNumberOfPeople
|
||||
SELECT
|
||||
COUNT(DISTINCT ("person"."id")) AS "total",
|
||||
COUNT(DISTINCT ("person"."id")) FILTER (
|
||||
WHERE
|
||||
"person"."isHidden" = true
|
||||
) AS "hidden"
|
||||
FROM
|
||||
"person" "person"
|
||||
INNER JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"person"."ownerId" = $1
|
||||
AND "asset"."isArchived" = false
|
||||
|
||||
-- PersonRepository.getFacesByIds
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."stackId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."duplicateId" AS "AssetFaceEntity__AssetFaceEntity_asset_duplicateId"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
|
||||
WHERE
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
(
|
||||
(
|
||||
(
|
||||
("AssetFaceEntity"."assetId" = $1)
|
||||
AND ("AssetFaceEntity"."personId" = $2)
|
||||
)
|
||||
"person"."ownerId" = $1
|
||||
and (
|
||||
lower("person"."name") like $2
|
||||
or lower("person"."name") like $3
|
||||
)
|
||||
)
|
||||
limit
|
||||
$4
|
||||
|
||||
-- PersonRepository.getDistinctNames
|
||||
select distinct
|
||||
on (lower("person"."name")) "person"."id",
|
||||
"person"."name"
|
||||
from
|
||||
"person"
|
||||
where
|
||||
(
|
||||
"person"."ownerId" = $1
|
||||
and "person"."name" != $2
|
||||
)
|
||||
|
||||
-- PersonRepository.getStatistics
|
||||
select
|
||||
count(distinct ("assets"."id")) as "count"
|
||||
from
|
||||
"asset_faces"
|
||||
left join "assets" on "assets"."id" = "asset_faces"."assetId"
|
||||
and "asset_faces"."personId" = $1
|
||||
and "assets"."isArchived" = $2
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."livePhotoVideoId" is null
|
||||
|
||||
-- PersonRepository.getNumberOfPeople
|
||||
select
|
||||
count(distinct ("person"."id")) as "total",
|
||||
count(distinct ("person"."id")) filter (
|
||||
where
|
||||
"person"."isHidden" = $1
|
||||
) as "hidden"
|
||||
from
|
||||
"person"
|
||||
inner join "asset_faces" on "asset_faces"."personId" = "person"."id"
|
||||
inner join "assets" on "assets"."id" = "asset_faces"."assetId"
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."isArchived" = $2
|
||||
where
|
||||
"person"."ownerId" = $3
|
||||
|
||||
-- PersonRepository.refreshFaces
|
||||
with
|
||||
"added_embeddings" as (
|
||||
insert into
|
||||
"face_search" ("faceId", "embedding")
|
||||
values
|
||||
($1, $2)
|
||||
)
|
||||
select
|
||||
from
|
||||
(
|
||||
select
|
||||
1
|
||||
) as "dummy"
|
||||
|
||||
-- PersonRepository.getFacesByIds
|
||||
select
|
||||
"asset_faces".*,
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"assets".*
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."id" = "asset_faces"."assetId"
|
||||
) as obj
|
||||
) as "asset",
|
||||
(
|
||||
select
|
||||
to_json(obj)
|
||||
from
|
||||
(
|
||||
select
|
||||
"person".*
|
||||
from
|
||||
"person"
|
||||
where
|
||||
"person"."id" = "asset_faces"."personId"
|
||||
) as obj
|
||||
) as "person"
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."assetId" in ($1)
|
||||
and "asset_faces"."personId" in ($2)
|
||||
|
||||
-- PersonRepository.getRandomFace
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
WHERE
|
||||
(("AssetFaceEntity"."personId" = $1))
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
"asset_faces".*
|
||||
from
|
||||
"asset_faces"
|
||||
where
|
||||
"asset_faces"."personId" = $1
|
||||
|
||||
-- PersonRepository.getLatestFaceDate
|
||||
SELECT
|
||||
MAX("jobStatus"."facesRecognizedAt")::text AS "latestDate"
|
||||
FROM
|
||||
"asset_job_status" "jobStatus"
|
||||
select
|
||||
max("asset_job_status"."facesRecognizedAt")::text as "latestDate"
|
||||
from
|
||||
"asset_job_status"
|
||||
|
|
|
@ -76,7 +76,7 @@ where
|
|||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
order by
|
||||
smart_search.embedding <= > $6::vector
|
||||
smart_search.embedding <= > $6
|
||||
limit
|
||||
$7
|
||||
offset
|
||||
|
@ -88,7 +88,7 @@ with
|
|||
select
|
||||
"assets"."id" as "assetId",
|
||||
"assets"."duplicateId",
|
||||
smart_search.embedding <= > $1::vector as "distance"
|
||||
smart_search.embedding <= > $1 as "distance"
|
||||
from
|
||||
"assets"
|
||||
inner join "smart_search" on "assets"."id" = "smart_search"."assetId"
|
||||
|
@ -99,7 +99,7 @@ with
|
|||
and "assets"."type" = $4
|
||||
and "assets"."id" != $5::uuid
|
||||
order by
|
||||
smart_search.embedding <= > $6::vector
|
||||
smart_search.embedding <= > $6
|
||||
limit
|
||||
$7
|
||||
)
|
||||
|
@ -116,7 +116,7 @@ with
|
|||
select
|
||||
"asset_faces"."id",
|
||||
"asset_faces"."personId",
|
||||
face_search.embedding <= > $1::vector as "distance"
|
||||
face_search.embedding <= > $1 as "distance"
|
||||
from
|
||||
"asset_faces"
|
||||
inner join "assets" on "assets"."id" = "asset_faces"."assetId"
|
||||
|
@ -125,7 +125,7 @@ with
|
|||
"assets"."ownerId" = any ($2::uuid [])
|
||||
and "assets"."deletedAt" is null
|
||||
order by
|
||||
face_search.embedding <= > $3::vector
|
||||
face_search.embedding <= > $3
|
||||
limit
|
||||
$4
|
||||
)
|
||||
|
|
|
@ -1,257 +1,95 @@
|
|||
-- NOTE: This file is auto generated by ./sql-generator
|
||||
|
||||
-- StackRepository.search
|
||||
SELECT
|
||||
"StackEntity"."id" AS "StackEntity_id",
|
||||
"StackEntity"."ownerId" AS "StackEntity_ownerId",
|
||||
"StackEntity"."primaryAssetId" AS "StackEntity_primaryAssetId",
|
||||
"StackEntity__StackEntity_assets"."id" AS "StackEntity__StackEntity_assets_id",
|
||||
"StackEntity__StackEntity_assets"."deviceAssetId" AS "StackEntity__StackEntity_assets_deviceAssetId",
|
||||
"StackEntity__StackEntity_assets"."ownerId" AS "StackEntity__StackEntity_assets_ownerId",
|
||||
"StackEntity__StackEntity_assets"."libraryId" AS "StackEntity__StackEntity_assets_libraryId",
|
||||
"StackEntity__StackEntity_assets"."deviceId" AS "StackEntity__StackEntity_assets_deviceId",
|
||||
"StackEntity__StackEntity_assets"."type" AS "StackEntity__StackEntity_assets_type",
|
||||
"StackEntity__StackEntity_assets"."status" AS "StackEntity__StackEntity_assets_status",
|
||||
"StackEntity__StackEntity_assets"."originalPath" AS "StackEntity__StackEntity_assets_originalPath",
|
||||
"StackEntity__StackEntity_assets"."thumbhash" AS "StackEntity__StackEntity_assets_thumbhash",
|
||||
"StackEntity__StackEntity_assets"."encodedVideoPath" AS "StackEntity__StackEntity_assets_encodedVideoPath",
|
||||
"StackEntity__StackEntity_assets"."createdAt" AS "StackEntity__StackEntity_assets_createdAt",
|
||||
"StackEntity__StackEntity_assets"."updatedAt" AS "StackEntity__StackEntity_assets_updatedAt",
|
||||
"StackEntity__StackEntity_assets"."deletedAt" AS "StackEntity__StackEntity_assets_deletedAt",
|
||||
"StackEntity__StackEntity_assets"."fileCreatedAt" AS "StackEntity__StackEntity_assets_fileCreatedAt",
|
||||
"StackEntity__StackEntity_assets"."localDateTime" AS "StackEntity__StackEntity_assets_localDateTime",
|
||||
"StackEntity__StackEntity_assets"."fileModifiedAt" AS "StackEntity__StackEntity_assets_fileModifiedAt",
|
||||
"StackEntity__StackEntity_assets"."isFavorite" AS "StackEntity__StackEntity_assets_isFavorite",
|
||||
"StackEntity__StackEntity_assets"."isArchived" AS "StackEntity__StackEntity_assets_isArchived",
|
||||
"StackEntity__StackEntity_assets"."isExternal" AS "StackEntity__StackEntity_assets_isExternal",
|
||||
"StackEntity__StackEntity_assets"."isOffline" AS "StackEntity__StackEntity_assets_isOffline",
|
||||
"StackEntity__StackEntity_assets"."checksum" AS "StackEntity__StackEntity_assets_checksum",
|
||||
"StackEntity__StackEntity_assets"."duration" AS "StackEntity__StackEntity_assets_duration",
|
||||
"StackEntity__StackEntity_assets"."isVisible" AS "StackEntity__StackEntity_assets_isVisible",
|
||||
"StackEntity__StackEntity_assets"."livePhotoVideoId" AS "StackEntity__StackEntity_assets_livePhotoVideoId",
|
||||
"StackEntity__StackEntity_assets"."originalFileName" AS "StackEntity__StackEntity_assets_originalFileName",
|
||||
"StackEntity__StackEntity_assets"."sidecarPath" AS "StackEntity__StackEntity_assets_sidecarPath",
|
||||
"StackEntity__StackEntity_assets"."stackId" AS "StackEntity__StackEntity_assets_stackId",
|
||||
"StackEntity__StackEntity_assets"."duplicateId" AS "StackEntity__StackEntity_assets_duplicateId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_assetId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."description" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_description",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageWidth" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageWidth",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageHeight" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageHeight",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fileSizeInByte" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fileSizeInByte",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."orientation" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_orientation",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."dateTimeOriginal" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_dateTimeOriginal",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."modifyDate" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_modifyDate",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."timeZone" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_timeZone",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."latitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_latitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."longitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_longitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."projectionType" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_projectionType",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."city" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_city",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."livePhotoCID" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_livePhotoCID",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."autoStackId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_autoStackId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."state" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_state",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."country" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_country",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."make" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_make",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."model" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_model",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."lensModel" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_lensModel",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fNumber" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fNumber",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."focalLength" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_focalLength",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."iso" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_iso",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exposureTime" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exposureTime",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."profileDescription" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_profileDescription",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."colorspace" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_colorspace",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."bitsPerSample" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_bitsPerSample",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."rating" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_rating",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fps" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fps"
|
||||
FROM
|
||||
"asset_stack" "StackEntity"
|
||||
LEFT JOIN "assets" "StackEntity__StackEntity_assets" ON "StackEntity__StackEntity_assets"."stackId" = "StackEntity"."id"
|
||||
AND (
|
||||
"StackEntity__StackEntity_assets"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "exif" "01db479afeb88793eed8e0d1dde6ccfccf1698b9" ON "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" = "StackEntity__StackEntity_assets"."id"
|
||||
WHERE
|
||||
(("StackEntity"."ownerId" = $1))
|
||||
select
|
||||
"asset_stack".*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."stackId" = "asset_stack"."id"
|
||||
) as agg
|
||||
) as "assets"
|
||||
from
|
||||
"asset_stack"
|
||||
where
|
||||
"asset_stack"."ownerId" = $1
|
||||
|
||||
-- StackRepository.delete
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."StackEntity_id" AS "ids_StackEntity_id",
|
||||
"distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt"
|
||||
FROM
|
||||
select
|
||||
*,
|
||||
(
|
||||
SELECT
|
||||
"StackEntity"."id" AS "StackEntity_id",
|
||||
"StackEntity"."ownerId" AS "StackEntity_ownerId",
|
||||
"StackEntity"."primaryAssetId" AS "StackEntity_primaryAssetId",
|
||||
"StackEntity__StackEntity_assets"."id" AS "StackEntity__StackEntity_assets_id",
|
||||
"StackEntity__StackEntity_assets"."deviceAssetId" AS "StackEntity__StackEntity_assets_deviceAssetId",
|
||||
"StackEntity__StackEntity_assets"."ownerId" AS "StackEntity__StackEntity_assets_ownerId",
|
||||
"StackEntity__StackEntity_assets"."libraryId" AS "StackEntity__StackEntity_assets_libraryId",
|
||||
"StackEntity__StackEntity_assets"."deviceId" AS "StackEntity__StackEntity_assets_deviceId",
|
||||
"StackEntity__StackEntity_assets"."type" AS "StackEntity__StackEntity_assets_type",
|
||||
"StackEntity__StackEntity_assets"."status" AS "StackEntity__StackEntity_assets_status",
|
||||
"StackEntity__StackEntity_assets"."originalPath" AS "StackEntity__StackEntity_assets_originalPath",
|
||||
"StackEntity__StackEntity_assets"."thumbhash" AS "StackEntity__StackEntity_assets_thumbhash",
|
||||
"StackEntity__StackEntity_assets"."encodedVideoPath" AS "StackEntity__StackEntity_assets_encodedVideoPath",
|
||||
"StackEntity__StackEntity_assets"."createdAt" AS "StackEntity__StackEntity_assets_createdAt",
|
||||
"StackEntity__StackEntity_assets"."updatedAt" AS "StackEntity__StackEntity_assets_updatedAt",
|
||||
"StackEntity__StackEntity_assets"."deletedAt" AS "StackEntity__StackEntity_assets_deletedAt",
|
||||
"StackEntity__StackEntity_assets"."fileCreatedAt" AS "StackEntity__StackEntity_assets_fileCreatedAt",
|
||||
"StackEntity__StackEntity_assets"."localDateTime" AS "StackEntity__StackEntity_assets_localDateTime",
|
||||
"StackEntity__StackEntity_assets"."fileModifiedAt" AS "StackEntity__StackEntity_assets_fileModifiedAt",
|
||||
"StackEntity__StackEntity_assets"."isFavorite" AS "StackEntity__StackEntity_assets_isFavorite",
|
||||
"StackEntity__StackEntity_assets"."isArchived" AS "StackEntity__StackEntity_assets_isArchived",
|
||||
"StackEntity__StackEntity_assets"."isExternal" AS "StackEntity__StackEntity_assets_isExternal",
|
||||
"StackEntity__StackEntity_assets"."isOffline" AS "StackEntity__StackEntity_assets_isOffline",
|
||||
"StackEntity__StackEntity_assets"."checksum" AS "StackEntity__StackEntity_assets_checksum",
|
||||
"StackEntity__StackEntity_assets"."duration" AS "StackEntity__StackEntity_assets_duration",
|
||||
"StackEntity__StackEntity_assets"."isVisible" AS "StackEntity__StackEntity_assets_isVisible",
|
||||
"StackEntity__StackEntity_assets"."livePhotoVideoId" AS "StackEntity__StackEntity_assets_livePhotoVideoId",
|
||||
"StackEntity__StackEntity_assets"."originalFileName" AS "StackEntity__StackEntity_assets_originalFileName",
|
||||
"StackEntity__StackEntity_assets"."sidecarPath" AS "StackEntity__StackEntity_assets_sidecarPath",
|
||||
"StackEntity__StackEntity_assets"."stackId" AS "StackEntity__StackEntity_assets_stackId",
|
||||
"StackEntity__StackEntity_assets"."duplicateId" AS "StackEntity__StackEntity_assets_duplicateId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_assetId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."description" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_description",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageWidth" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageWidth",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageHeight" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageHeight",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fileSizeInByte" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fileSizeInByte",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."orientation" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_orientation",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."dateTimeOriginal" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_dateTimeOriginal",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."modifyDate" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_modifyDate",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."timeZone" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_timeZone",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."latitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_latitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."longitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_longitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."projectionType" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_projectionType",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."city" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_city",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."livePhotoCID" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_livePhotoCID",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."autoStackId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_autoStackId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."state" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_state",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."country" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_country",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."make" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_make",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."model" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_model",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."lensModel" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_lensModel",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fNumber" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fNumber",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."focalLength" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_focalLength",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."iso" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_iso",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exposureTime" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exposureTime",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."profileDescription" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_profileDescription",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."colorspace" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_colorspace",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."bitsPerSample" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_bitsPerSample",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."rating" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_rating",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fps" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fps",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_id",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."value" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_value",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."createdAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_createdAt",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."updatedAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_updatedAt",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."color" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_color",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."parentId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_parentId",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."userId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_userId"
|
||||
FROM
|
||||
"asset_stack" "StackEntity"
|
||||
LEFT JOIN "assets" "StackEntity__StackEntity_assets" ON "StackEntity__StackEntity_assets"."stackId" = "StackEntity"."id"
|
||||
AND (
|
||||
"StackEntity__StackEntity_assets"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "exif" "01db479afeb88793eed8e0d1dde6ccfccf1698b9" ON "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" = "StackEntity__StackEntity_assets"."id"
|
||||
LEFT JOIN "tag_asset" "4f1c9474d4596aede2814ee2eb938eecf7a93b95" ON "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."assetsId" = "StackEntity__StackEntity_assets"."id"
|
||||
LEFT JOIN "tags" "3e3064f11b97177a1e1ce3c77ecf32850343aba1" ON "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" = "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."tagsId"
|
||||
WHERE
|
||||
(("StackEntity"."id" = $1))
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt" ASC,
|
||||
"StackEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"tags".*
|
||||
from
|
||||
"tags"
|
||||
inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId"
|
||||
where
|
||||
"tag_asset"."assetsId" = "assets"."id"
|
||||
) as agg
|
||||
) as "tags"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."stackId" = "asset_stack"."id"
|
||||
) as agg
|
||||
) as "assets"
|
||||
from
|
||||
"asset_stack"
|
||||
where
|
||||
"id" = $1::uuid
|
||||
|
||||
-- StackRepository.getById
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."StackEntity_id" AS "ids_StackEntity_id",
|
||||
"distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt"
|
||||
FROM
|
||||
select
|
||||
*,
|
||||
(
|
||||
SELECT
|
||||
"StackEntity"."id" AS "StackEntity_id",
|
||||
"StackEntity"."ownerId" AS "StackEntity_ownerId",
|
||||
"StackEntity"."primaryAssetId" AS "StackEntity_primaryAssetId",
|
||||
"StackEntity__StackEntity_assets"."id" AS "StackEntity__StackEntity_assets_id",
|
||||
"StackEntity__StackEntity_assets"."deviceAssetId" AS "StackEntity__StackEntity_assets_deviceAssetId",
|
||||
"StackEntity__StackEntity_assets"."ownerId" AS "StackEntity__StackEntity_assets_ownerId",
|
||||
"StackEntity__StackEntity_assets"."libraryId" AS "StackEntity__StackEntity_assets_libraryId",
|
||||
"StackEntity__StackEntity_assets"."deviceId" AS "StackEntity__StackEntity_assets_deviceId",
|
||||
"StackEntity__StackEntity_assets"."type" AS "StackEntity__StackEntity_assets_type",
|
||||
"StackEntity__StackEntity_assets"."status" AS "StackEntity__StackEntity_assets_status",
|
||||
"StackEntity__StackEntity_assets"."originalPath" AS "StackEntity__StackEntity_assets_originalPath",
|
||||
"StackEntity__StackEntity_assets"."thumbhash" AS "StackEntity__StackEntity_assets_thumbhash",
|
||||
"StackEntity__StackEntity_assets"."encodedVideoPath" AS "StackEntity__StackEntity_assets_encodedVideoPath",
|
||||
"StackEntity__StackEntity_assets"."createdAt" AS "StackEntity__StackEntity_assets_createdAt",
|
||||
"StackEntity__StackEntity_assets"."updatedAt" AS "StackEntity__StackEntity_assets_updatedAt",
|
||||
"StackEntity__StackEntity_assets"."deletedAt" AS "StackEntity__StackEntity_assets_deletedAt",
|
||||
"StackEntity__StackEntity_assets"."fileCreatedAt" AS "StackEntity__StackEntity_assets_fileCreatedAt",
|
||||
"StackEntity__StackEntity_assets"."localDateTime" AS "StackEntity__StackEntity_assets_localDateTime",
|
||||
"StackEntity__StackEntity_assets"."fileModifiedAt" AS "StackEntity__StackEntity_assets_fileModifiedAt",
|
||||
"StackEntity__StackEntity_assets"."isFavorite" AS "StackEntity__StackEntity_assets_isFavorite",
|
||||
"StackEntity__StackEntity_assets"."isArchived" AS "StackEntity__StackEntity_assets_isArchived",
|
||||
"StackEntity__StackEntity_assets"."isExternal" AS "StackEntity__StackEntity_assets_isExternal",
|
||||
"StackEntity__StackEntity_assets"."isOffline" AS "StackEntity__StackEntity_assets_isOffline",
|
||||
"StackEntity__StackEntity_assets"."checksum" AS "StackEntity__StackEntity_assets_checksum",
|
||||
"StackEntity__StackEntity_assets"."duration" AS "StackEntity__StackEntity_assets_duration",
|
||||
"StackEntity__StackEntity_assets"."isVisible" AS "StackEntity__StackEntity_assets_isVisible",
|
||||
"StackEntity__StackEntity_assets"."livePhotoVideoId" AS "StackEntity__StackEntity_assets_livePhotoVideoId",
|
||||
"StackEntity__StackEntity_assets"."originalFileName" AS "StackEntity__StackEntity_assets_originalFileName",
|
||||
"StackEntity__StackEntity_assets"."sidecarPath" AS "StackEntity__StackEntity_assets_sidecarPath",
|
||||
"StackEntity__StackEntity_assets"."stackId" AS "StackEntity__StackEntity_assets_stackId",
|
||||
"StackEntity__StackEntity_assets"."duplicateId" AS "StackEntity__StackEntity_assets_duplicateId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_assetId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."description" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_description",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageWidth" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageWidth",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exifImageHeight" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exifImageHeight",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fileSizeInByte" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fileSizeInByte",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."orientation" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_orientation",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."dateTimeOriginal" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_dateTimeOriginal",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."modifyDate" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_modifyDate",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."timeZone" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_timeZone",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."latitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_latitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."longitude" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_longitude",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."projectionType" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_projectionType",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."city" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_city",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."livePhotoCID" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_livePhotoCID",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."autoStackId" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_autoStackId",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."state" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_state",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."country" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_country",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."make" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_make",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."model" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_model",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."lensModel" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_lensModel",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fNumber" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fNumber",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."focalLength" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_focalLength",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."iso" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_iso",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."exposureTime" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_exposureTime",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."profileDescription" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_profileDescription",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."colorspace" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_colorspace",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."bitsPerSample" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_bitsPerSample",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."rating" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_rating",
|
||||
"01db479afeb88793eed8e0d1dde6ccfccf1698b9"."fps" AS "01db479afeb88793eed8e0d1dde6ccfccf1698b9_fps",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_id",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."value" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_value",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."createdAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_createdAt",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."updatedAt" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_updatedAt",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."color" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_color",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."parentId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_parentId",
|
||||
"3e3064f11b97177a1e1ce3c77ecf32850343aba1"."userId" AS "3e3064f11b97177a1e1ce3c77ecf32850343aba1_userId"
|
||||
FROM
|
||||
"asset_stack" "StackEntity"
|
||||
LEFT JOIN "assets" "StackEntity__StackEntity_assets" ON "StackEntity__StackEntity_assets"."stackId" = "StackEntity"."id"
|
||||
AND (
|
||||
"StackEntity__StackEntity_assets"."deletedAt" IS NULL
|
||||
)
|
||||
LEFT JOIN "exif" "01db479afeb88793eed8e0d1dde6ccfccf1698b9" ON "01db479afeb88793eed8e0d1dde6ccfccf1698b9"."assetId" = "StackEntity__StackEntity_assets"."id"
|
||||
LEFT JOIN "tag_asset" "4f1c9474d4596aede2814ee2eb938eecf7a93b95" ON "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."assetsId" = "StackEntity__StackEntity_assets"."id"
|
||||
LEFT JOIN "tags" "3e3064f11b97177a1e1ce3c77ecf32850343aba1" ON "3e3064f11b97177a1e1ce3c77ecf32850343aba1"."id" = "4f1c9474d4596aede2814ee2eb938eecf7a93b95"."tagsId"
|
||||
WHERE
|
||||
(("StackEntity"."id" = $1))
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"distinctAlias"."StackEntity__StackEntity_assets_fileCreatedAt" ASC,
|
||||
"StackEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"tags".*
|
||||
from
|
||||
"tags"
|
||||
inner join "tag_asset" on "tags"."id" = "tag_asset"."tagsId"
|
||||
where
|
||||
"tag_asset"."assetsId" = "assets"."id"
|
||||
) as agg
|
||||
) as "tags"
|
||||
from
|
||||
"assets"
|
||||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."stackId" = "asset_stack"."id"
|
||||
) as agg
|
||||
) as "assets"
|
||||
from
|
||||
"asset_stack"
|
||||
where
|
||||
"id" = $1::uuid
|
||||
|
|
|
@ -1,33 +1,18 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Kysely, sql } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
|
||||
import { AlbumUserRole } from 'src/enum';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
type IActivityAccess = IAccessRepository['activity'];
|
||||
type IAlbumAccess = IAccessRepository['album'];
|
||||
type IAssetAccess = IAccessRepository['asset'];
|
||||
type IAuthDeviceAccess = IAccessRepository['authDevice'];
|
||||
type IMemoryAccess = IAccessRepository['memory'];
|
||||
type IPersonAccess = IAccessRepository['person'];
|
||||
type IPartnerAccess = IAccessRepository['partner'];
|
||||
type IStackAccess = IAccessRepository['stack'];
|
||||
type ITagAccess = IAccessRepository['tag'];
|
||||
type ITimelineAccess = IAccessRepository['timeline'];
|
||||
|
||||
@Injectable()
|
||||
class ActivityAccess implements IActivityAccess {
|
||||
class ActivityAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, activityIds: Set<string>) {
|
||||
if (activityIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -36,17 +21,14 @@ class ActivityAccess implements IActivityAccess {
|
|||
.where('activity.id', 'in', [...activityIds])
|
||||
.where('activity.userId', '=', userId)
|
||||
.execute()
|
||||
.then((activities) => {
|
||||
console.log('activities', activities);
|
||||
return new Set(activities.map((activity) => activity.id));
|
||||
});
|
||||
.then((activities) => new Set(activities.map((activity) => activity.id)));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkAlbumOwnerAccess(userId: string, activityIds: Set<string>): Promise<Set<string>> {
|
||||
async checkAlbumOwnerAccess(userId: string, activityIds: Set<string>) {
|
||||
if (activityIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -61,9 +43,9 @@ class ActivityAccess implements IActivityAccess {
|
|||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkCreateAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
|
||||
async checkCreateAccess(userId: string, albumIds: Set<string>) {
|
||||
if (albumIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -80,14 +62,14 @@ class ActivityAccess implements IActivityAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class AlbumAccess implements IAlbumAccess {
|
||||
class AlbumAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, albumIds: Set<string>) {
|
||||
if (albumIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -102,9 +84,9 @@ class AlbumAccess implements IAlbumAccess {
|
|||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>> {
|
||||
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole) {
|
||||
if (albumIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
const accessRole =
|
||||
|
@ -125,9 +107,9 @@ class AlbumAccess implements IAlbumAccess {
|
|||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>> {
|
||||
async checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>) {
|
||||
if (albumIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -142,14 +124,14 @@ class AlbumAccess implements IAlbumAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class AssetAccess implements IAssetAccess {
|
||||
class AssetAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkAlbumAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
|
||||
async checkAlbumAccess(userId: string, assetIds: Set<string>) {
|
||||
if (assetIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -185,9 +167,9 @@ class AssetAccess implements IAssetAccess {
|
|||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, assetIds: Set<string>) {
|
||||
if (assetIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -201,9 +183,9 @@ class AssetAccess implements IAssetAccess {
|
|||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkPartnerAccess(userId: string, assetIds: Set<string>): Promise<Set<string>> {
|
||||
async checkPartnerAccess(userId: string, assetIds: Set<string>) {
|
||||
if (assetIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -224,9 +206,9 @@ class AssetAccess implements IAssetAccess {
|
|||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>): Promise<Set<string>> {
|
||||
async checkSharedLinkAccess(sharedLinkId: string, assetIds: Set<string>) {
|
||||
if (assetIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -276,14 +258,14 @@ class AssetAccess implements IAssetAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class AuthDeviceAccess implements IAuthDeviceAccess {
|
||||
class AuthDeviceAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, deviceIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, deviceIds: Set<string>) {
|
||||
if (deviceIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -296,14 +278,14 @@ class AuthDeviceAccess implements IAuthDeviceAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class StackAccess implements IStackAccess {
|
||||
class StackAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, stackIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, stackIds: Set<string>) {
|
||||
if (stackIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -316,14 +298,14 @@ class StackAccess implements IStackAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class TimelineAccess implements ITimelineAccess {
|
||||
class TimelineAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
|
||||
async checkPartnerAccess(userId: string, partnerIds: Set<string>) {
|
||||
if (partnerIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -336,14 +318,14 @@ class TimelineAccess implements ITimelineAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class MemoryAccess implements IMemoryAccess {
|
||||
class MemoryAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, memoryIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, memoryIds: Set<string>) {
|
||||
if (memoryIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -357,14 +339,14 @@ class MemoryAccess implements IMemoryAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class PersonAccess implements IPersonAccess {
|
||||
class PersonAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, personIds: Set<string>) {
|
||||
if (personIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -378,9 +360,9 @@ class PersonAccess implements IPersonAccess {
|
|||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkFaceOwnerAccess(userId: string, assetFaceIds: Set<string>): Promise<Set<string>> {
|
||||
async checkFaceOwnerAccess(userId: string, assetFaceIds: Set<string>) {
|
||||
if (assetFaceIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -396,14 +378,14 @@ class PersonAccess implements IPersonAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class PartnerAccess implements IPartnerAccess {
|
||||
class PartnerAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkUpdateAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>> {
|
||||
async checkUpdateAccess(userId: string, partnerIds: Set<string>) {
|
||||
if (partnerIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -416,14 +398,14 @@ class PartnerAccess implements IPartnerAccess {
|
|||
}
|
||||
}
|
||||
|
||||
class TagAccess implements ITagAccess {
|
||||
class TagAccess {
|
||||
constructor(private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||
@ChunkedSet({ paramIndex: 1 })
|
||||
async checkOwnerAccess(userId: string, tagIds: Set<string>): Promise<Set<string>> {
|
||||
async checkOwnerAccess(userId: string, tagIds: Set<string>) {
|
||||
if (tagIds.size === 0) {
|
||||
return new Set();
|
||||
return new Set<string>();
|
||||
}
|
||||
|
||||
return this.db
|
||||
|
@ -436,17 +418,17 @@ class TagAccess implements ITagAccess {
|
|||
}
|
||||
}
|
||||
|
||||
export class AccessRepository implements IAccessRepository {
|
||||
activity: IActivityAccess;
|
||||
album: IAlbumAccess;
|
||||
asset: IAssetAccess;
|
||||
authDevice: IAuthDeviceAccess;
|
||||
memory: IMemoryAccess;
|
||||
person: IPersonAccess;
|
||||
partner: IPartnerAccess;
|
||||
stack: IStackAccess;
|
||||
tag: ITagAccess;
|
||||
timeline: ITimelineAccess;
|
||||
export class AccessRepository {
|
||||
activity: ActivityAccess;
|
||||
album: AlbumAccess;
|
||||
asset: AssetAccess;
|
||||
authDevice: AuthDeviceAccess;
|
||||
memory: MemoryAccess;
|
||||
person: PersonAccess;
|
||||
partner: PartnerAccess;
|
||||
stack: StackAccess;
|
||||
tag: TagAccess;
|
||||
timeline: TimelineAccess;
|
||||
|
||||
constructor(@InjectKysely() db: Kysely<DB>) {
|
||||
this.activity = new ActivityAccess(db);
|
||||
|
|
|
@ -1,72 +1,116 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ExpressionBuilder, Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Albums, DB } from 'src/db';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AlbumUserCreateDto } from 'src/dtos/album.dto';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import {
|
||||
DataSource,
|
||||
EntityManager,
|
||||
FindOptionsOrder,
|
||||
FindOptionsRelations,
|
||||
In,
|
||||
IsNull,
|
||||
Not,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
const withoutDeletedUsers = <T extends AlbumEntity | null>(album: T) => {
|
||||
if (album) {
|
||||
album.albumUsers = album.albumUsers.filter((albumUser) => albumUser.user && !albumUser.user.deletedAt);
|
||||
}
|
||||
return album;
|
||||
const userColumns = [
|
||||
'id',
|
||||
'email',
|
||||
'createdAt',
|
||||
'profileImagePath',
|
||||
'isAdmin',
|
||||
'shouldChangePassword',
|
||||
'deletedAt',
|
||||
'oauthId',
|
||||
'updatedAt',
|
||||
'storageLabel',
|
||||
'name',
|
||||
'quotaSizeInBytes',
|
||||
'quotaUsageInBytes',
|
||||
'status',
|
||||
'profileChangedAt',
|
||||
] as const;
|
||||
|
||||
const withOwner = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
return jsonObjectFrom(eb.selectFrom('users').select(userColumns).whereRef('users.id', '=', 'albums.ownerId')).as(
|
||||
'owner',
|
||||
);
|
||||
};
|
||||
|
||||
const withAlbumUsers = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('albums_shared_users_users as album_users')
|
||||
.selectAll('album_users')
|
||||
.select((eb) =>
|
||||
jsonObjectFrom(eb.selectFrom('users').select(userColumns).whereRef('users.id', '=', 'album_users.usersId')).as(
|
||||
'user',
|
||||
),
|
||||
)
|
||||
.whereRef('album_users.albumsId', '=', 'albums.id'),
|
||||
).as('albumUsers');
|
||||
};
|
||||
|
||||
const withSharedLink = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
return jsonArrayFrom(eb.selectFrom('shared_links').selectAll().whereRef('shared_links.albumId', '=', 'albums.id')).as(
|
||||
'sharedLinks',
|
||||
);
|
||||
};
|
||||
|
||||
const withAssets = (eb: ExpressionBuilder<DB, 'albums'>) => {
|
||||
return eb
|
||||
.selectFrom((eb) =>
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.selectAll('assets')
|
||||
.innerJoin('exif', 'assets.id', 'exif.assetId')
|
||||
.select((eb) => eb.fn.toJson('exif').as('exifInfo'))
|
||||
.innerJoin('albums_assets_assets', 'albums_assets_assets.assetsId', 'assets.id')
|
||||
.whereRef('albums_assets_assets.albumsId', '=', 'albums.id')
|
||||
.orderBy('assets.fileCreatedAt', 'desc')
|
||||
.as('asset'),
|
||||
)
|
||||
.select((eb) => eb.fn.jsonAgg('asset').as('assets'))
|
||||
.as('assets');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AlbumRepository implements IAlbumRepository {
|
||||
constructor(
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(AlbumEntity) private repository: Repository<AlbumEntity>,
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@InjectKysely() private db: Kysely<DB>,
|
||||
) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, {}] })
|
||||
async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null> {
|
||||
const relations: FindOptionsRelations<AlbumEntity> = {
|
||||
owner: true,
|
||||
albumUsers: { user: true },
|
||||
assets: false,
|
||||
sharedLinks: true,
|
||||
};
|
||||
|
||||
const order: FindOptionsOrder<AlbumEntity> = {};
|
||||
|
||||
if (options.withAssets) {
|
||||
relations.assets = {
|
||||
exifInfo: true,
|
||||
};
|
||||
|
||||
order.assets = {
|
||||
fileCreatedAt: 'DESC',
|
||||
};
|
||||
}
|
||||
|
||||
const album = await this.repository.findOne({ where: { id }, relations, order });
|
||||
return withoutDeletedUsers(album);
|
||||
async getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | undefined> {
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.where('albums.id', '=', id)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.select(withSharedLink)
|
||||
.$if(options.withAssets, (eb) => eb.select(withAssets))
|
||||
.executeTakeFirst() as Promise<AlbumEntity | undefined>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
where: [
|
||||
{ ownerId, assets: { id: assetId } },
|
||||
{ albumUsers: { userId: ownerId }, assets: { id: assetId } },
|
||||
],
|
||||
relations: { owner: true, albumUsers: { user: true } },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return albums.map((album) => withoutDeletedUsers(album));
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.leftJoin('albums_shared_users_users as album_users', 'album_users.albumsId', 'albums.id')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb.and([eb('albums.ownerId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]),
|
||||
eb.and([eb('album_users.usersId', '=', ownerId), eb('album_assets.assetsId', '=', assetId)]),
|
||||
]),
|
||||
)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||
|
@ -77,36 +121,38 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
return [];
|
||||
}
|
||||
|
||||
// Only possible with query builder because of GROUP BY.
|
||||
const albumMetadatas = await this.repository
|
||||
.createQueryBuilder('album')
|
||||
.select('album.id')
|
||||
.addSelect('MIN(assets.fileCreatedAt)', 'start_date')
|
||||
.addSelect('MAX(assets.fileCreatedAt)', 'end_date')
|
||||
.addSelect('COUNT(assets.id)', 'asset_count')
|
||||
.leftJoin('albums_assets_assets', 'album_assets', 'album_assets.albumsId = album.id')
|
||||
.leftJoin('assets', 'assets', 'assets.id = album_assets.assetsId')
|
||||
.where('album.id IN (:...ids)', { ids })
|
||||
.groupBy('album.id')
|
||||
.getRawMany();
|
||||
const metadatas = await this.db
|
||||
.selectFrom('albums')
|
||||
.leftJoin('albums_assets_assets as album_assets', 'album_assets.albumsId', 'albums.id')
|
||||
.leftJoin('assets', 'assets.id', 'album_assets.assetsId')
|
||||
.select('albums.id')
|
||||
.select((eb) => eb.fn.min('assets.fileCreatedAt').as('startDate'))
|
||||
.select((eb) => eb.fn.max('assets.fileCreatedAt').as('endDate'))
|
||||
.select((eb) => eb.fn.count('assets.id').as('assetCount'))
|
||||
.where('albums.id', 'in', ids)
|
||||
.groupBy('albums.id')
|
||||
.execute();
|
||||
|
||||
return albumMetadatas.map<AlbumAssetCount>((metadatas) => ({
|
||||
albumId: metadatas['album_id'],
|
||||
assetCount: Number(metadatas['asset_count']),
|
||||
startDate: metadatas['end_date'] ? new Date(metadatas['start_date']) : undefined,
|
||||
endDate: metadatas['end_date'] ? new Date(metadatas['end_date']) : undefined,
|
||||
return metadatas.map((metadatas) => ({
|
||||
albumId: metadatas.id,
|
||||
assetCount: Number(metadatas.assetCount),
|
||||
startDate: metadatas.startDate ? new Date(metadatas.startDate) : undefined,
|
||||
endDate: metadatas.endDate ? new Date(metadatas.endDate) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getOwned(ownerId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
relations: { albumUsers: { user: true }, sharedLinks: true, owner: true },
|
||||
where: { ownerId },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return albums.map((album) => withoutDeletedUsers(album));
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.select(withOwner)
|
||||
.select(withAlbumUsers)
|
||||
.select(withSharedLink)
|
||||
.where('albums.ownerId', '=', ownerId)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,17 +160,25 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getShared(ownerId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
relations: { albumUsers: { user: true }, sharedLinks: true, owner: true },
|
||||
where: [
|
||||
{ albumUsers: { userId: ownerId } },
|
||||
{ sharedLinks: { userId: ownerId } },
|
||||
{ ownerId, albumUsers: { user: Not(IsNull()) } },
|
||||
],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return albums.map((album) => withoutDeletedUsers(album));
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.distinctOn('albums.createdAt')
|
||||
.leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id')
|
||||
.leftJoin('shared_links', 'shared_links.albumId', 'albums.id')
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb('shared_albums.usersId', '=', ownerId),
|
||||
eb('shared_links.userId', '=', ownerId),
|
||||
eb.and([eb('albums.ownerId', '=', ownerId), eb('shared_albums.usersId', 'is not', null)]),
|
||||
]),
|
||||
)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.select(withAlbumUsers)
|
||||
.select(withOwner)
|
||||
.select(withSharedLink)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,35 +186,37 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
*/
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getNotShared(ownerId: string): Promise<AlbumEntity[]> {
|
||||
const albums = await this.repository.find({
|
||||
relations: { albumUsers: true, sharedLinks: true, owner: true },
|
||||
where: { ownerId, albumUsers: { user: IsNull() }, sharedLinks: { id: IsNull() } },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return albums.map((album) => withoutDeletedUsers(album));
|
||||
return this.db
|
||||
.selectFrom('albums')
|
||||
.selectAll('albums')
|
||||
.distinctOn('albums.createdAt')
|
||||
.leftJoin('albums_shared_users_users as shared_albums', 'shared_albums.albumsId', 'albums.id')
|
||||
.leftJoin('shared_links', 'shared_links.albumId', 'albums.id')
|
||||
.where('albums.ownerId', '=', ownerId)
|
||||
.where('shared_albums.usersId', 'is', null)
|
||||
.where('shared_links.userId', 'is', null)
|
||||
.where('albums.deletedAt', 'is', null)
|
||||
.select(withAlbumUsers)
|
||||
.select(withOwner)
|
||||
.select(withSharedLink)
|
||||
.orderBy('albums.createdAt', 'desc')
|
||||
.execute() as unknown as Promise<AlbumEntity[]>;
|
||||
}
|
||||
|
||||
async restoreAll(userId: string): Promise<void> {
|
||||
await this.repository.restore({ ownerId: userId });
|
||||
await this.db.updateTable('albums').set({ deletedAt: null }).where('ownerId', '=', userId).execute();
|
||||
}
|
||||
|
||||
async softDeleteAll(userId: string): Promise<void> {
|
||||
await this.repository.softDelete({ ownerId: userId });
|
||||
await this.db.updateTable('albums').set({ deletedAt: new Date() }).where('ownerId', '=', userId).execute();
|
||||
}
|
||||
|
||||
async deleteAll(userId: string): Promise<void> {
|
||||
await this.repository.delete({ ownerId: userId });
|
||||
await this.db.deleteFrom('albums').where('ownerId', '=', userId).execute();
|
||||
}
|
||||
|
||||
async removeAsset(assetId: string): Promise<void> {
|
||||
// Using dataSource, because there is no direct access to albums_assets_assets.
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('albums_assets_assets')
|
||||
.where('"albums_assets_assets"."assetsId" = :assetId', { assetId })
|
||||
.execute();
|
||||
await this.db.deleteFrom('albums_assets_assets').where('albums_assets_assets.assetsId', '=', assetId).execute();
|
||||
}
|
||||
|
||||
@Chunked({ paramIndex: 1 })
|
||||
|
@ -169,14 +225,10 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
return;
|
||||
}
|
||||
|
||||
await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.from('albums_assets_assets')
|
||||
.where({
|
||||
albumsId: albumId,
|
||||
assetsId: In(assetIds),
|
||||
})
|
||||
await this.db
|
||||
.deleteFrom('albums_assets_assets')
|
||||
.where('albums_assets_assets.albumsId', '=', albumId)
|
||||
.where('albums_assets_assets.assetsId', 'in', assetIds)
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
@ -194,73 +246,80 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
return new Set();
|
||||
}
|
||||
|
||||
const results = await this.dataSource
|
||||
.createQueryBuilder()
|
||||
.select('albums_assets.assetsId', 'assetId')
|
||||
.from('albums_assets_assets', 'albums_assets')
|
||||
.where('"albums_assets"."albumsId" = :albumId', { albumId })
|
||||
.andWhere('"albums_assets"."assetsId" IN (:...assetIds)', { assetIds })
|
||||
.getRawMany<{ assetId: string }>();
|
||||
|
||||
return new Set(results.map(({ assetId }) => assetId));
|
||||
return this.db
|
||||
.selectFrom('albums_assets_assets')
|
||||
.selectAll()
|
||||
.where('albums_assets_assets.albumsId', '=', albumId)
|
||||
.where('albums_assets_assets.assetsId', 'in', assetIds)
|
||||
.execute()
|
||||
.then((results) => new Set(results.map(({ assetsId }) => assetsId)));
|
||||
}
|
||||
|
||||
async addAssetIds(albumId: string, assetIds: string[]): Promise<void> {
|
||||
await this.addAssets(this.dataSource.manager, albumId, assetIds);
|
||||
await this.addAssets(this.db, albumId, assetIds);
|
||||
}
|
||||
|
||||
create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
|
||||
return this.dataSource.transaction<AlbumEntity>(async (manager) => {
|
||||
const { id } = await manager.save(AlbumEntity, { ...album, assets: [] });
|
||||
const assetIds = (album.assets || []).map((asset) => asset.id);
|
||||
await this.addAssets(manager, id, assetIds);
|
||||
return manager.findOneOrFail(AlbumEntity, {
|
||||
where: { id },
|
||||
relations: {
|
||||
owner: true,
|
||||
albumUsers: { user: true },
|
||||
sharedLinks: true,
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
create(album: Insertable<Albums>, assetIds: string[], albumUsers: AlbumUserCreateDto[]): Promise<AlbumEntity> {
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const newAlbum = await tx.insertInto('albums').values(album).returning('albums.id').executeTakeFirst();
|
||||
|
||||
if (!newAlbum) {
|
||||
throw new Error('Failed to create album');
|
||||
}
|
||||
|
||||
if (assetIds.length > 0) {
|
||||
await this.addAssets(tx, newAlbum.id, assetIds);
|
||||
}
|
||||
|
||||
if (albumUsers.length > 0) {
|
||||
await tx
|
||||
.insertInto('albums_shared_users_users')
|
||||
.values(
|
||||
albumUsers.map((albumUser) => ({ albumsId: newAlbum.id, usersId: albumUser.userId, role: albumUser.role })),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
return tx
|
||||
.selectFrom('albums')
|
||||
.selectAll()
|
||||
.where('id', '=', newAlbum.id)
|
||||
.select(withOwner)
|
||||
.select(withSharedLink)
|
||||
.select(withAssets)
|
||||
.select(withAlbumUsers)
|
||||
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
|
||||
});
|
||||
}
|
||||
|
||||
update(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
|
||||
return this.save(album);
|
||||
update(id: string, album: Updateable<Albums>): Promise<AlbumEntity> {
|
||||
return this.db
|
||||
.updateTable('albums')
|
||||
.set({ ...album, updatedAt: new Date() })
|
||||
.where('id', '=', id)
|
||||
.returningAll('albums')
|
||||
.returning(withOwner)
|
||||
.returning(withSharedLink)
|
||||
.returning(withAlbumUsers)
|
||||
.executeTakeFirst() as unknown as Promise<AlbumEntity>;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.repository.delete({ id });
|
||||
await this.db.deleteFrom('albums').where('id', '=', id).execute();
|
||||
}
|
||||
|
||||
@Chunked({ paramIndex: 2, chunkSize: 30_000 })
|
||||
private async addAssets(manager: EntityManager, albumId: string, assetIds: string[]): Promise<void> {
|
||||
private async addAssets(db: Kysely<DB>, albumId: string, assetIds: string[]): Promise<void> {
|
||||
if (assetIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into('albums_assets_assets', ['albumsId', 'assetsId'])
|
||||
await db
|
||||
.insertInto('albums_assets_assets')
|
||||
.values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId })))
|
||||
.execute();
|
||||
}
|
||||
|
||||
private async save(album: Partial<AlbumEntity>) {
|
||||
const { id } = await this.repository.save(album);
|
||||
return this.repository.findOneOrFail({
|
||||
where: { id },
|
||||
relations: {
|
||||
owner: true,
|
||||
albumUsers: { user: true },
|
||||
sharedLinks: true,
|
||||
assets: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure all thumbnails for albums are updated by:
|
||||
* - Removing thumbnails from albums without assets
|
||||
|
@ -272,28 +331,44 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
async updateThumbnails(): Promise<number | undefined> {
|
||||
// Subquery for getting a new thumbnail.
|
||||
|
||||
const builder = this.dataSource
|
||||
.createQueryBuilder('albums_assets_assets', 'album_assets')
|
||||
.innerJoin('assets', 'assets', '"album_assets"."assetsId" = "assets"."id"')
|
||||
.where('"album_assets"."albumsId" = "albums"."id"');
|
||||
const result = await this.db
|
||||
.updateTable('albums')
|
||||
.set((eb) => ({
|
||||
albumThumbnailAssetId: this.updateThumbnailBuilder(eb)
|
||||
.select('album_assets.assetsId')
|
||||
.orderBy('assets.fileCreatedAt', 'desc')
|
||||
.limit(1),
|
||||
updatedAt: new Date(),
|
||||
}))
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb.and([
|
||||
eb('albumThumbnailAssetId', 'is', null),
|
||||
eb.exists(this.updateThumbnailBuilder(eb).select(sql`1`.as('1'))), // Has assets
|
||||
]),
|
||||
eb.and([
|
||||
eb('albumThumbnailAssetId', 'is not', null),
|
||||
eb.not(
|
||||
eb.exists(
|
||||
this.updateThumbnailBuilder(eb)
|
||||
.select(sql`1`.as('1'))
|
||||
.whereRef('albums.albumThumbnailAssetId', '=', 'album_assets.assetsId'), // Has invalid assets
|
||||
),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
|
||||
const newThumbnail = builder
|
||||
.clone()
|
||||
.select('"album_assets"."assetsId"')
|
||||
.orderBy('"assets"."fileCreatedAt"', 'DESC')
|
||||
.limit(1);
|
||||
const hasAssets = builder.clone().select('1');
|
||||
const hasInvalidAsset = hasAssets.clone().andWhere('"albums"."albumThumbnailAssetId" = "album_assets"."assetsId"');
|
||||
return Number(result[0].numUpdatedRows);
|
||||
}
|
||||
|
||||
const updateAlbums = this.repository
|
||||
.createQueryBuilder('albums')
|
||||
.update(AlbumEntity)
|
||||
.set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` })
|
||||
.where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${hasAssets.getQuery()})`)
|
||||
.orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${hasInvalidAsset.getQuery()})`);
|
||||
|
||||
const result = await updateAlbums.execute();
|
||||
|
||||
return result.affected;
|
||||
private updateThumbnailBuilder(eb: ExpressionBuilder<DB, 'albums'>) {
|
||||
return eb
|
||||
.selectFrom('albums_assets_assets as album_assets')
|
||||
.innerJoin('assets', (join) =>
|
||||
join.onRef('album_assets.assetsId', '=', 'assets.id').on('assets.deletedAt', 'is', null),
|
||||
)
|
||||
.whereRef('album_assets.albumsId', '=', 'albums.id');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,50 +1,36 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { ApiKeys, DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { AuthApiKey } from 'src/types';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
const columns = ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'] as const;
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyRepository implements IKeyRepository {
|
||||
constructor(
|
||||
@InjectRepository(APIKeyEntity) private repository: Repository<APIKeyEntity>,
|
||||
@InjectKysely() private db: Kysely<DB>,
|
||||
) {}
|
||||
export class ApiKeyRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
async create(dto: Insertable<ApiKeys>): Promise<APIKeyEntity> {
|
||||
const { id, name, createdAt, updatedAt, permissions } = await this.db
|
||||
.insertInto('api_keys')
|
||||
.values(dto)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return { id, name, createdAt, updatedAt, permissions } as APIKeyEntity;
|
||||
create(dto: Insertable<ApiKeys>) {
|
||||
return this.db.insertInto('api_keys').values(dto).returningAll().executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async update(userId: string, id: string, dto: Updateable<ApiKeys>): Promise<APIKeyEntity> {
|
||||
async update(userId: string, id: string, dto: Updateable<ApiKeys>) {
|
||||
return this.db
|
||||
.updateTable('api_keys')
|
||||
.set(dto)
|
||||
.where('api_keys.userId', '=', userId)
|
||||
.where('id', '=', asUuid(id))
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as unknown as Promise<APIKeyEntity>;
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
async delete(userId: string, id: string): Promise<void> {
|
||||
async delete(userId: string, id: string) {
|
||||
await this.db.deleteFrom('api_keys').where('userId', '=', userId).where('id', '=', asUuid(id)).execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.STRING] })
|
||||
getKey(hashedToken: string): Promise<AuthApiKey | undefined> {
|
||||
getKey(hashedToken: string) {
|
||||
return this.db
|
||||
.selectFrom('api_keys')
|
||||
.innerJoinLateral(
|
||||
|
@ -72,26 +58,26 @@ export class ApiKeyRepository implements IKeyRepository {
|
|||
eb.fn.toJson('user').as('user'),
|
||||
])
|
||||
.where('api_keys.key', '=', hashedToken)
|
||||
.executeTakeFirst() as Promise<AuthApiKey | undefined>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
getById(userId: string, id: string): Promise<APIKeyEntity | null> {
|
||||
getById(userId: string, id: string) {
|
||||
return this.db
|
||||
.selectFrom('api_keys')
|
||||
.select(columns)
|
||||
.where('id', '=', asUuid(id))
|
||||
.where('userId', '=', userId)
|
||||
.executeTakeFirst() as unknown as Promise<APIKeyEntity | null>;
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getByUserId(userId: string): Promise<APIKeyEntity[]> {
|
||||
getByUserId(userId: string) {
|
||||
return this.db
|
||||
.selectFrom('api_keys')
|
||||
.select(columns)
|
||||
.where('userId', '=', userId)
|
||||
.orderBy('createdAt', 'desc')
|
||||
.execute() as unknown as Promise<APIKeyEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -284,7 +284,23 @@ export class AssetRepository implements IAssetRepository {
|
|||
.$if(!!library, (qb) => qb.select(withLibrary))
|
||||
.$if(!!owner, (qb) => qb.select(withOwner))
|
||||
.$if(!!smartSearch, withSmartSearch)
|
||||
.$if(!!stack, (qb) => withStack(qb, { assets: !!stack!.assets, count: false }))
|
||||
.$if(!!stack, (qb) =>
|
||||
qb
|
||||
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('assets as stacked')
|
||||
.selectAll('asset_stack')
|
||||
.select((eb) => eb.fn('array_agg', [eb.table('stacked')]).as('assets'))
|
||||
.where('stacked.deletedAt', 'is', null)
|
||||
.whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
|
||||
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||
.as('stacked_assets'),
|
||||
(join) => join.on('asset_stack.id', 'is not', null),
|
||||
)
|
||||
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).as('stack')),
|
||||
)
|
||||
.$if(!!files, (qb) => qb.select(withFiles))
|
||||
.$if(!!tags, (qb) => qb.select(withTags))
|
||||
.limit(1)
|
||||
|
|
|
@ -4,10 +4,15 @@ import { InjectKysely } from 'nestjs-kysely';
|
|||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { DatabaseAction, EntityType } from 'src/enum';
|
||||
import { AuditSearch, IAuditRepository } from 'src/interfaces/audit.interface';
|
||||
|
||||
export interface AuditSearch {
|
||||
action?: DatabaseAction;
|
||||
entityType?: EntityType;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditRepository implements IAuditRepository {
|
||||
export class AuditRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({
|
||||
|
|
|
@ -1,19 +1,106 @@
|
|||
import { RegisterQueueOptions } from '@nestjs/bullmq';
|
||||
import { Inject, Injectable, Optional } from '@nestjs/common';
|
||||
import { QueueOptions } from 'bullmq';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateSync } from 'class-validator';
|
||||
import { Request, Response } from 'express';
|
||||
import { RedisOptions } from 'ioredis';
|
||||
import { KyselyConfig } from 'kysely';
|
||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||
import { CLS_ID } from 'nestjs-cls';
|
||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||
import { join, resolve } from 'node:path';
|
||||
import postgres, { Notice } from 'postgres';
|
||||
import { citiesFile, excludePaths, IWorker } from 'src/constants';
|
||||
import { Telemetry } from 'src/decorators';
|
||||
import { EnvDto } from 'src/dtos/env.dto';
|
||||
import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker } from 'src/enum';
|
||||
import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { ImmichEnvironment, ImmichHeader, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
|
||||
import { DatabaseConnectionParams, DatabaseExtension, VectorExtension } from 'src/interfaces/database.interface';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { setDifference } from 'src/utils/set';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
||||
|
||||
export interface EnvData {
|
||||
host?: string;
|
||||
port: number;
|
||||
environment: ImmichEnvironment;
|
||||
configFile?: string;
|
||||
logLevel?: LogLevel;
|
||||
|
||||
buildMetadata: {
|
||||
build?: string;
|
||||
buildUrl?: string;
|
||||
buildImage?: string;
|
||||
buildImageUrl?: string;
|
||||
repository?: string;
|
||||
repositoryUrl?: string;
|
||||
sourceRef?: string;
|
||||
sourceCommit?: string;
|
||||
sourceUrl?: string;
|
||||
thirdPartySourceUrl?: string;
|
||||
thirdPartyBugFeatureUrl?: string;
|
||||
thirdPartyDocumentationUrl?: string;
|
||||
thirdPartySupportUrl?: string;
|
||||
};
|
||||
|
||||
bull: {
|
||||
config: QueueOptions;
|
||||
queues: RegisterQueueOptions[];
|
||||
};
|
||||
|
||||
cls: {
|
||||
config: ClsModuleOptions;
|
||||
};
|
||||
|
||||
database: {
|
||||
config: { typeorm: PostgresConnectionOptions & DatabaseConnectionParams; kysely: KyselyConfig };
|
||||
skipMigrations: boolean;
|
||||
vectorExtension: VectorExtension;
|
||||
};
|
||||
|
||||
licensePublicKey: {
|
||||
client: string;
|
||||
server: string;
|
||||
};
|
||||
|
||||
network: {
|
||||
trustedProxies: string[];
|
||||
};
|
||||
|
||||
otel: OpenTelemetryModuleOptions;
|
||||
|
||||
resourcePaths: {
|
||||
lockFile: string;
|
||||
geodata: {
|
||||
dateFile: string;
|
||||
admin1: string;
|
||||
admin2: string;
|
||||
cities500: string;
|
||||
naturalEarthCountriesPath: string;
|
||||
};
|
||||
web: {
|
||||
root: string;
|
||||
indexHtml: string;
|
||||
};
|
||||
};
|
||||
|
||||
redis: RedisOptions;
|
||||
|
||||
telemetry: {
|
||||
apiPort: number;
|
||||
microservicesPort: number;
|
||||
metrics: Set<ImmichTelemetry>;
|
||||
};
|
||||
|
||||
storage: {
|
||||
ignoreMountCheckErrors: boolean;
|
||||
};
|
||||
|
||||
workers: ImmichWorker[];
|
||||
|
||||
noColor: boolean;
|
||||
nodeVersion?: string;
|
||||
}
|
||||
|
||||
const productionKeys = {
|
||||
client:
|
||||
|
@ -269,10 +356,10 @@ let cached: EnvData | undefined;
|
|||
|
||||
@Injectable()
|
||||
@Telemetry({ enabled: false })
|
||||
export class ConfigRepository implements IConfigRepository {
|
||||
export class ConfigRepository {
|
||||
constructor(@Inject(IWorker) @Optional() private worker?: ImmichWorker) {}
|
||||
|
||||
getEnv(): EnvData {
|
||||
getEnv() {
|
||||
if (!cached) {
|
||||
cached = getEnv();
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import { InjectKysely } from 'nestjs-kysely';
|
|||
import semver from 'semver';
|
||||
import { POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants';
|
||||
import { DB } from 'src/db';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import {
|
||||
DatabaseExtension,
|
||||
DatabaseLock,
|
||||
|
@ -18,6 +17,7 @@ import {
|
|||
VectorUpdateResult,
|
||||
} from 'src/interfaces/database.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { UPSERT_COLUMNS } from 'src/utils/database';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
import { DataSource, EntityManager, EntityMetadata, QueryRunner } from 'typeorm';
|
||||
|
@ -31,7 +31,7 @@ export class DatabaseRepository implements IDatabaseRepository {
|
|||
@InjectKysely() private db: Kysely<DB>,
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
configRepository: ConfigRepository,
|
||||
) {
|
||||
this.vectorExtension = configRepository.getEnv().database.vectorExtension;
|
||||
this.logger.setContext(DatabaseRepository.name);
|
||||
|
|
|
@ -12,7 +12,6 @@ import _ from 'lodash';
|
|||
import { Server, Socket } from 'socket.io';
|
||||
import { EventConfig } from 'src/decorators';
|
||||
import { ImmichWorker, MetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import {
|
||||
ArgsOf,
|
||||
ClientEventMap,
|
||||
|
@ -52,7 +51,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
|
|||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
@Inject(IConfigRepository) private configRepository: ConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(EventRepository.name);
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ICronRepository } from 'src/interfaces/cron.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
|
@ -35,7 +31,6 @@ import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
|||
import { ITrashRepository } from 'src/interfaces/trash.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
|
||||
import { IViewRepository } from 'src/interfaces/view.interface';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||
|
@ -78,22 +73,23 @@ import { ViewRepository } from 'src/repositories/view-repository';
|
|||
|
||||
export const repositories = [
|
||||
//
|
||||
AccessRepository,
|
||||
ActivityRepository,
|
||||
AuditRepository,
|
||||
ApiKeyRepository,
|
||||
ConfigRepository,
|
||||
ViewRepository,
|
||||
];
|
||||
|
||||
export const providers = [
|
||||
{ provide: IAccessRepository, useClass: AccessRepository },
|
||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
|
||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||
{ provide: IAuditRepository, useClass: AuditRepository },
|
||||
{ provide: IConfigRepository, useClass: ConfigRepository },
|
||||
{ provide: ICronRepository, useClass: CronRepository },
|
||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||
{ provide: IEventRepository, useClass: EventRepository },
|
||||
{ provide: IJobRepository, useClass: JobRepository },
|
||||
{ provide: IKeyRepository, useClass: ApiKeyRepository },
|
||||
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
||||
{ provide: ILoggerRepository, useClass: LoggerRepository },
|
||||
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
||||
|
@ -119,5 +115,4 @@ export const providers = [
|
|||
{ provide: ITrashRepository, useClass: TrashRepository },
|
||||
{ provide: IUserRepository, useClass: UserRepository },
|
||||
{ provide: IVersionHistoryRepository, useClass: VersionHistoryRepository },
|
||||
{ provide: IViewRepository, useClass: ViewRepository },
|
||||
];
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { getQueueToken } from '@nestjs/bullmq';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ModuleRef, Reflector } from '@nestjs/core';
|
||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { JobsOptions, Queue, Worker } from 'bullmq';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { JobConfig } from 'src/decorators';
|
||||
import { MetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
IEntityJob,
|
||||
|
@ -22,6 +20,7 @@ import {
|
|||
QueueStatus,
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc';
|
||||
|
||||
type JobMapItem = {
|
||||
|
@ -38,8 +37,7 @@ export class JobRepository implements IJobRepository {
|
|||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
private schedulerRegistry: SchedulerRegistry,
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ClsService } from 'nestjs-cls';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { LoggerRepository } from 'src/repositories/logger.repository';
|
||||
import { IConfigRepository } from 'src/types';
|
||||
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||
import { Mocked } from 'vitest';
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { ConsoleLogger, Inject, Injectable, Scope } from '@nestjs/common';
|
||||
import { ConsoleLogger, Injectable, Scope } from '@nestjs/common';
|
||||
import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import { Telemetry } from 'src/decorators';
|
||||
import { LogLevel } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
|
||||
|
||||
|
@ -25,7 +25,7 @@ export class LoggerRepository extends ConsoleLogger implements ILoggerRepository
|
|||
|
||||
constructor(
|
||||
private cls: ClsService,
|
||||
@Inject(IConfigRepository) configRepository: IConfigRepository,
|
||||
configRepository: ConfigRepository,
|
||||
) {
|
||||
super(LoggerRepository.name);
|
||||
|
||||
|
|
|
@ -11,7 +11,6 @@ import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
|
|||
import { AssetEntity, withExif } from 'src/entities/asset.entity';
|
||||
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
|
||||
import { LogLevel, SystemMetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import {
|
||||
GeoPoint,
|
||||
|
@ -21,6 +20,7 @@ import {
|
|||
ReverseGeocodeResult,
|
||||
} from 'src/interfaces/map.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
interface MapDB extends DB {
|
||||
geodata_places_tmp: GeodataPlaces;
|
||||
|
@ -30,7 +30,7 @@ interface MapDB extends DB {
|
|||
@Injectable()
|
||||
export class MapRepository implements IMapRepository {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
@InjectKysely() private db: Kysely<MapDB>,
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { ExpressionBuilder, Insertable, Kysely, SelectExpression, sql } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import _ from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
|
||||
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { FaceSearchEntity } from 'src/entities/face-search.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { PaginationMode, SourceType } from 'src/enum';
|
||||
import { SourceType } from 'src/enum';
|
||||
import {
|
||||
AssetFaceId,
|
||||
DeleteFacesOptions,
|
||||
|
@ -17,332 +17,418 @@ import {
|
|||
PersonNameSearchOptions,
|
||||
PersonSearchOptions,
|
||||
PersonStatistics,
|
||||
SelectFaceOptions,
|
||||
UnassignFacesOptions,
|
||||
UpdateFacesData,
|
||||
} from 'src/interfaces/person.interface';
|
||||
import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
||||
import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
|
||||
import { mapUpsertColumns } from 'src/utils/database';
|
||||
import { Paginated, PaginationOptions } from 'src/utils/pagination';
|
||||
import { FindOptionsRelations } from 'typeorm';
|
||||
|
||||
const withPerson = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
|
||||
return jsonObjectFrom(
|
||||
eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_faces.personId'),
|
||||
).as('person');
|
||||
};
|
||||
|
||||
const withAsset = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
|
||||
return jsonObjectFrom(
|
||||
eb.selectFrom('assets').selectAll('assets').whereRef('assets.id', '=', 'asset_faces.assetId'),
|
||||
).as('asset');
|
||||
};
|
||||
|
||||
const withFaceSearch = (eb: ExpressionBuilder<DB, 'asset_faces'>) => {
|
||||
return jsonObjectFrom(
|
||||
eb.selectFrom('face_search').selectAll('face_search').whereRef('face_search.faceId', '=', 'asset_faces.id'),
|
||||
).as('faceSearch');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PersonRepository implements IPersonRepository {
|
||||
constructor(
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||
@InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
|
||||
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
|
||||
@InjectRepository(FaceSearchEntity) private faceSearchRepository: Repository<FaceSearchEntity>,
|
||||
@InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
|
||||
) {}
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
|
||||
async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise<number> {
|
||||
const result = await this.assetFaceRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
const result = await this.db
|
||||
.updateTable('asset_faces')
|
||||
.set({ personId: newPersonId })
|
||||
.where(_.omitBy({ personId: oldPersonId, id: faceIds ? In(faceIds) : undefined }, _.isUndefined))
|
||||
.execute();
|
||||
.$if(!!oldPersonId, (qb) => qb.where('asset_faces.personId', '=', oldPersonId!))
|
||||
.$if(!!faceIds, (qb) => qb.where('asset_faces.id', 'in', faceIds!))
|
||||
.executeTakeFirst();
|
||||
|
||||
return result.affected ?? 0;
|
||||
return Number(result.numChangedRows) ?? 0;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
|
||||
async unassignFaces({ sourceType }: UnassignFacesOptions): Promise<void> {
|
||||
await this.assetFaceRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
await this.db
|
||||
.updateTable('asset_faces')
|
||||
.set({ personId: null })
|
||||
.where({ sourceType })
|
||||
.where('asset_faces.sourceType', '=', sourceType)
|
||||
.execute();
|
||||
|
||||
await this.vacuum({ reindexVectors: false });
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[{ id: DummyValue.UUID }]] })
|
||||
async delete(entities: PersonEntity[]): Promise<void> {
|
||||
await this.personRepository.remove(entities);
|
||||
if (entities.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.db
|
||||
.deleteFrom('person')
|
||||
.where(
|
||||
'person.id',
|
||||
'in',
|
||||
entities.map(({ id }) => id),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ sourceType: SourceType.EXIF }] })
|
||||
async deleteFaces({ sourceType }: DeleteFacesOptions): Promise<void> {
|
||||
await this.assetFaceRepository
|
||||
.createQueryBuilder('asset_faces')
|
||||
.delete()
|
||||
.andWhere('sourceType = :sourceType', { sourceType })
|
||||
.execute();
|
||||
await this.db.deleteFrom('asset_faces').where('asset_faces.sourceType', '=', sourceType).execute();
|
||||
|
||||
await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING });
|
||||
}
|
||||
|
||||
getAllFaces(
|
||||
pagination: PaginationOptions,
|
||||
options: FindManyOptions<AssetFaceEntity> = {},
|
||||
): Paginated<AssetFaceEntity> {
|
||||
return paginate(this.assetFaceRepository, pagination, options);
|
||||
getAllFaces(options: Partial<AssetFaceEntity> = {}): AsyncIterableIterator<AssetFaceEntity> {
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.$if(options.personId === null, (qb) => qb.where('asset_faces.personId', 'is', null))
|
||||
.$if(!!options.personId, (qb) => qb.where('asset_faces.personId', '=', options.personId!))
|
||||
.$if(!!options.sourceType, (qb) => qb.where('asset_faces.sourceType', '=', options.sourceType!))
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
|
||||
.$if(!!options.assetId, (qb) => qb.where('asset_faces.assetId', '=', options.assetId!))
|
||||
.stream() as AsyncIterableIterator<AssetFaceEntity>;
|
||||
}
|
||||
|
||||
getAll(pagination: PaginationOptions, options: FindManyOptions<PersonEntity> = {}): Paginated<PersonEntity> {
|
||||
return paginate(this.personRepository, pagination, options);
|
||||
getAll(options: Partial<PersonEntity> = {}): AsyncIterableIterator<PersonEntity> {
|
||||
return this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.$if(!!options.ownerId, (qb) => qb.where('person.ownerId', '=', options.ownerId!))
|
||||
.$if(!!options.thumbnailPath, (qb) => qb.where('person.thumbnailPath', '=', options.thumbnailPath!))
|
||||
.$if(options.faceAssetId === null, (qb) => qb.where('person.faceAssetId', 'is', null))
|
||||
.$if(!!options.faceAssetId, (qb) => qb.where('person.faceAssetId', '=', options.faceAssetId!))
|
||||
.$if(options.isHidden !== undefined, (qb) => qb.where('person.isHidden', '=', options.isHidden!))
|
||||
.stream() as AsyncIterableIterator<PersonEntity>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [{ take: 10, skip: 10 }, DummyValue.UUID] })
|
||||
async getAllForUser(
|
||||
pagination: PaginationOptions,
|
||||
userId: string,
|
||||
options?: PersonSearchOptions,
|
||||
): Paginated<PersonEntity> {
|
||||
const queryBuilder = this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
.innerJoin('person.faces', 'face')
|
||||
.where('person.ownerId = :userId', { userId })
|
||||
.innerJoin('face.asset', 'asset')
|
||||
.andWhere('asset.isArchived = false')
|
||||
.orderBy('person.isHidden', 'ASC')
|
||||
.addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC')
|
||||
.addOrderBy('COUNT(face.assetId)', 'DESC')
|
||||
.addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST')
|
||||
.addOrderBy('person.createdAt')
|
||||
.having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 })
|
||||
.groupBy('person.id');
|
||||
if (options?.closestFaceAssetId) {
|
||||
const innerQueryBuilder = this.faceSearchRepository
|
||||
.createQueryBuilder('face_search')
|
||||
.select('embedding', 'embedding')
|
||||
.where('"face_search"."faceId" = "person"."faceAssetId"');
|
||||
const faceSelectQueryBuilder = this.faceSearchRepository
|
||||
.createQueryBuilder('face_search')
|
||||
.select('embedding', 'embedding')
|
||||
.where('"face_search"."faceId" = :faceId', { faceId: options.closestFaceAssetId });
|
||||
queryBuilder
|
||||
.orderBy('(' + innerQueryBuilder.getQuery() + ') <=> (' + faceSelectQueryBuilder.getQuery() + ')')
|
||||
.setParameters(faceSelectQueryBuilder.getParameters());
|
||||
const items = (await this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.innerJoin('asset_faces', 'asset_faces.personId', 'person.id')
|
||||
.innerJoin('assets', (join) =>
|
||||
join
|
||||
.onRef('asset_faces.assetId', '=', 'assets.id')
|
||||
.on('assets.isArchived', '=', false)
|
||||
.on('assets.deletedAt', 'is', null),
|
||||
)
|
||||
.where('person.ownerId', '=', userId)
|
||||
.orderBy('person.isHidden', 'asc')
|
||||
.orderBy(sql`NULLIF(person.name, '') is null`, 'asc')
|
||||
.orderBy((eb) => eb.fn.count('asset_faces.assetId'), 'desc')
|
||||
.orderBy(sql`NULLIF(person.name, '')`, sql`asc nulls last`)
|
||||
.orderBy('person.createdAt')
|
||||
.having((eb) =>
|
||||
eb.or([
|
||||
eb('person.name', '!=', ''),
|
||||
eb((innerEb) => innerEb.fn.count('asset_faces.assetId'), '>=', options?.minimumFaceCount || 1),
|
||||
]),
|
||||
)
|
||||
.groupBy('person.id')
|
||||
.$if(!!options?.closestFaceAssetId, (qb) =>
|
||||
qb.orderBy((eb) =>
|
||||
eb(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('face_search')
|
||||
.select('face_search.embedding')
|
||||
.whereRef('face_search.faceId', '=', 'person.faceAssetId'),
|
||||
'<=>',
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('face_search')
|
||||
.select('face_search.embedding')
|
||||
.where('face_search.faceId', '=', options!.closestFaceAssetId!),
|
||||
),
|
||||
),
|
||||
)
|
||||
.$if(!options?.withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||
.offset(pagination.skip ?? 0)
|
||||
.limit(pagination.take + 1)
|
||||
.execute()) as PersonEntity[];
|
||||
|
||||
if (items.length > pagination.take) {
|
||||
return { items: items.slice(0, -1), hasNextPage: true };
|
||||
}
|
||||
if (!options?.withHidden) {
|
||||
queryBuilder.andWhere('person.isHidden = false');
|
||||
}
|
||||
return paginatedBuilder(queryBuilder, {
|
||||
mode: PaginationMode.LIMIT_OFFSET,
|
||||
...pagination,
|
||||
});
|
||||
|
||||
return { items, hasNextPage: false };
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
getAllWithoutFaces(): Promise<PersonEntity[]> {
|
||||
return this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
.leftJoin('person.faces', 'face')
|
||||
.having('COUNT(face.assetId) = 0')
|
||||
return this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.leftJoin('asset_faces', 'asset_faces.personId', 'person.id')
|
||||
.having((eb) => eb.fn.count('asset_faces.assetId'), '=', 0)
|
||||
.groupBy('person.id')
|
||||
.withDeleted()
|
||||
.getMany();
|
||||
.execute() as Promise<PersonEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]> {
|
||||
return this.assetFaceRepository.find({
|
||||
where: { assetId },
|
||||
relations: {
|
||||
person: true,
|
||||
},
|
||||
order: {
|
||||
boundingBoxX1: 'ASC',
|
||||
},
|
||||
});
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.select(withPerson)
|
||||
.where('asset_faces.assetId', '=', assetId)
|
||||
.orderBy('asset_faces.boundingBoxX1', 'asc')
|
||||
.execute() as Promise<AssetFaceEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaceById(id: string): Promise<AssetFaceEntity> {
|
||||
// TODO return null instead of find or fail
|
||||
return this.assetFaceRepository.findOneOrFail({
|
||||
where: { id },
|
||||
relations: {
|
||||
person: true,
|
||||
},
|
||||
});
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.select(withPerson)
|
||||
.where('asset_faces.id', '=', id)
|
||||
.executeTakeFirstOrThrow() as Promise<AssetFaceEntity>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaceByIdWithAssets(
|
||||
id: string,
|
||||
relations: FindOptionsRelations<AssetFaceEntity>,
|
||||
select: FindOptionsSelect<AssetFaceEntity>,
|
||||
relations?: FindOptionsRelations<AssetFaceEntity>,
|
||||
select?: SelectFaceOptions,
|
||||
): Promise<AssetFaceEntity | null> {
|
||||
return this.assetFaceRepository.findOne(
|
||||
_.omitBy(
|
||||
{
|
||||
where: { id },
|
||||
relations: {
|
||||
...relations,
|
||||
person: true,
|
||||
asset: true,
|
||||
},
|
||||
select,
|
||||
},
|
||||
_.isUndefined,
|
||||
),
|
||||
);
|
||||
return (this.db
|
||||
.selectFrom('asset_faces')
|
||||
.$if(!!select, (qb) =>
|
||||
qb.select(
|
||||
Object.keys(
|
||||
_.omitBy({ ...select!, faceSearch: undefined, asset: undefined }, _.isUndefined),
|
||||
) as SelectExpression<DB, 'asset_faces'>[],
|
||||
),
|
||||
)
|
||||
.$if(!select, (qb) => qb.selectAll('asset_faces'))
|
||||
.select(withPerson)
|
||||
.select(withAsset)
|
||||
.$if(!!relations?.faceSearch, (qb) => qb.select(withFaceSearch))
|
||||
.where('asset_faces.id', '=', id)
|
||||
.executeTakeFirst() ?? null) as Promise<AssetFaceEntity | null>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async reassignFace(assetFaceId: string, newPersonId: string): Promise<number> {
|
||||
const result = await this.assetFaceRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
const result = await this.db
|
||||
.updateTable('asset_faces')
|
||||
.set({ personId: newPersonId })
|
||||
.where({ id: assetFaceId })
|
||||
.execute();
|
||||
.where('asset_faces.id', '=', assetFaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
return result.affected ?? 0;
|
||||
return Number(result.numChangedRows) ?? 0;
|
||||
}
|
||||
|
||||
getById(personId: string): Promise<PersonEntity | null> {
|
||||
return this.personRepository.findOne({ where: { id: personId } });
|
||||
return (this.db //
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.where('person.id', '=', personId)
|
||||
.executeTakeFirst() ?? null) as Promise<PersonEntity | null>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, { withHidden: true }] })
|
||||
getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions): Promise<PersonEntity[]> {
|
||||
const queryBuilder = this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
.where(
|
||||
'person.ownerId = :userId AND (LOWER(person.name) LIKE :nameStart OR LOWER(person.name) LIKE :nameAnywhere)',
|
||||
{ userId, nameStart: `${personName.toLowerCase()}%`, nameAnywhere: `% ${personName.toLowerCase()}%` },
|
||||
return this.db
|
||||
.selectFrom('person')
|
||||
.selectAll('person')
|
||||
.where((eb) =>
|
||||
eb.and([
|
||||
eb('person.ownerId', '=', userId),
|
||||
eb.or([
|
||||
eb(eb.fn('lower', ['person.name']), 'like', `${personName.toLowerCase()}%`),
|
||||
eb(eb.fn('lower', ['person.name']), 'like', `% ${personName.toLowerCase()}%`),
|
||||
]),
|
||||
]),
|
||||
)
|
||||
.limit(1000);
|
||||
|
||||
if (!withHidden) {
|
||||
queryBuilder.andWhere('person.isHidden = false');
|
||||
}
|
||||
return queryBuilder.getMany();
|
||||
.limit(1000)
|
||||
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||
.execute() as Promise<PersonEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { withHidden: true }] })
|
||||
getDistinctNames(userId: string, { withHidden }: PersonNameSearchOptions): Promise<PersonNameResponse[]> {
|
||||
const queryBuilder = this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
return this.db
|
||||
.selectFrom('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();
|
||||
.distinctOn((eb) => eb.fn('lower', ['person.name']))
|
||||
.where((eb) => eb.and([eb('person.ownerId', '=', userId), eb('person.name', '!=', '')]))
|
||||
.$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getStatistics(personId: string): Promise<PersonStatistics> {
|
||||
const items = await this.assetFaceRepository
|
||||
.createQueryBuilder('face')
|
||||
.leftJoin('face.asset', 'asset')
|
||||
.where('face.personId = :personId', { personId })
|
||||
.andWhere('asset.isArchived = false')
|
||||
.andWhere('asset.deletedAt IS NULL')
|
||||
.andWhere('asset.livePhotoVideoId IS NULL')
|
||||
.select('COUNT(DISTINCT(asset.id))', 'count')
|
||||
.getRawOne();
|
||||
const result = await this.db
|
||||
.selectFrom('asset_faces')
|
||||
.leftJoin('assets', (join) =>
|
||||
join
|
||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.on('asset_faces.personId', '=', personId)
|
||||
.on('assets.isArchived', '=', false)
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.livePhotoVideoId', 'is', null),
|
||||
)
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
|
||||
.executeTakeFirst();
|
||||
|
||||
return {
|
||||
assets: items.count ?? 0,
|
||||
assets: result ? Number(result.count) : 0,
|
||||
};
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getNumberOfPeople(userId: string): Promise<PeopleStatistics> {
|
||||
const items = await this.personRepository
|
||||
.createQueryBuilder('person')
|
||||
.innerJoin('person.faces', 'face')
|
||||
.where('person.ownerId = :userId', { userId })
|
||||
.innerJoin('face.asset', 'asset')
|
||||
.andWhere('asset.isArchived = false')
|
||||
.select('COUNT(DISTINCT(person.id))', 'total')
|
||||
.addSelect('COUNT(DISTINCT(person.id)) FILTER (WHERE person.isHidden = true)', 'hidden')
|
||||
.getRawOne();
|
||||
const items = await this.db
|
||||
.selectFrom('person')
|
||||
.innerJoin('asset_faces', 'asset_faces.personId', 'person.id')
|
||||
.where('person.ownerId', '=', userId)
|
||||
.innerJoin('assets', (join) =>
|
||||
join
|
||||
.onRef('assets.id', '=', 'asset_faces.assetId')
|
||||
.on('assets.deletedAt', 'is', null)
|
||||
.on('assets.isArchived', '=', false),
|
||||
)
|
||||
.select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total'))
|
||||
.select((eb) =>
|
||||
eb.fn
|
||||
.count(eb.fn('distinct', ['person.id']))
|
||||
.filterWhere('person.isHidden', '=', true)
|
||||
.as('hidden'),
|
||||
)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (items == undefined) {
|
||||
return { total: 0, hidden: 0 };
|
||||
}
|
||||
|
||||
const result: PeopleStatistics = {
|
||||
total: items.total ?? 0,
|
||||
hidden: items.hidden ?? 0,
|
||||
return {
|
||||
total: Number(items.total),
|
||||
hidden: Number(items.hidden),
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
create(person: Partial<PersonEntity>): Promise<PersonEntity> {
|
||||
return this.save(person);
|
||||
create(person: Insertable<Person>): Promise<PersonEntity> {
|
||||
return this.db.insertInto('person').values(person).returningAll().executeTakeFirst() as Promise<PersonEntity>;
|
||||
}
|
||||
|
||||
async createAll(people: Partial<PersonEntity>[]): Promise<string[]> {
|
||||
const results = await this.personRepository.save(people);
|
||||
return results.map((person) => person.id);
|
||||
async createAll(people: Insertable<Person>[]): Promise<string[]> {
|
||||
const results = await this.db.insertInto('person').values(people).returningAll().execute();
|
||||
return results.map(({ id }) => id);
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[], [], [{ faceId: DummyValue.UUID, embedding: DummyValue.VECTOR }]] })
|
||||
async refreshFaces(
|
||||
facesToAdd: Partial<AssetFaceEntity>[],
|
||||
facesToAdd: (Insertable<AssetFaces> & { assetId: string })[],
|
||||
faceIdsToRemove: string[],
|
||||
embeddingsToAdd?: FaceSearchEntity[],
|
||||
embeddingsToAdd?: Insertable<FaceSearch>[],
|
||||
): Promise<void> {
|
||||
const query = this.faceSearchRepository.createQueryBuilder().select('1').fromDummy();
|
||||
let query = this.db;
|
||||
if (facesToAdd.length > 0) {
|
||||
const insertCte = this.assetFaceRepository.createQueryBuilder().insert().values(facesToAdd);
|
||||
query.addCommonTableExpression(insertCte, 'added');
|
||||
(query as any) = query.with('added', (db) => db.insertInto('asset_faces').values(facesToAdd));
|
||||
}
|
||||
|
||||
if (faceIdsToRemove.length > 0) {
|
||||
const deleteCte = this.assetFaceRepository
|
||||
.createQueryBuilder()
|
||||
.delete()
|
||||
.where('id = any(:faceIdsToRemove)', { faceIdsToRemove });
|
||||
query.addCommonTableExpression(deleteCte, 'deleted');
|
||||
(query as any) = query.with('removed', (db) =>
|
||||
db.deleteFrom('asset_faces').where('asset_faces.id', '=', (eb) => eb.fn.any(eb.val(faceIdsToRemove))),
|
||||
);
|
||||
}
|
||||
|
||||
if (embeddingsToAdd?.length) {
|
||||
const embeddingCte = this.faceSearchRepository.createQueryBuilder().insert().values(embeddingsToAdd).orIgnore();
|
||||
query.addCommonTableExpression(embeddingCte, 'embeddings');
|
||||
query.getQuery(); // typeorm mixes up parameters without this
|
||||
(query as any) = query.with('added_embeddings', (db) => db.insertInto('face_search').values(embeddingsToAdd));
|
||||
}
|
||||
|
||||
await query.execute();
|
||||
await query.selectFrom(sql`(select 1)`.as('dummy')).execute();
|
||||
}
|
||||
|
||||
async update(person: Partial<PersonEntity>): Promise<PersonEntity> {
|
||||
return this.save(person);
|
||||
async update(person: Partial<PersonEntity> & { id: string }): Promise<PersonEntity> {
|
||||
return this.db
|
||||
.updateTable('person')
|
||||
.set(person)
|
||||
.where('person.id', '=', person.id)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow() as Promise<PersonEntity>;
|
||||
}
|
||||
|
||||
async updateAll(people: Partial<PersonEntity>[]): Promise<void> {
|
||||
await this.personRepository.save(people);
|
||||
async updateAll(people: Insertable<Person>[]): Promise<void> {
|
||||
if (people.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.db
|
||||
.insertInto('person')
|
||||
.values(people)
|
||||
.onConflict((oc) => oc.column('id').doUpdateSet(() => mapUpsertColumns('person', people[0], ['id'])))
|
||||
.execute();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
|
||||
@ChunkedArray()
|
||||
async getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]> {
|
||||
return this.assetFaceRepository.find({ where: ids, relations: { asset: true }, withDeleted: true });
|
||||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]> {
|
||||
const { assetIds, personIds }: { assetIds: string[]; personIds: string[] } = { assetIds: [], personIds: [] };
|
||||
|
||||
for (const { assetId, personId } of ids) {
|
||||
assetIds.push(assetId);
|
||||
personIds.push(personId);
|
||||
}
|
||||
|
||||
return this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.select(withAsset)
|
||||
.select(withPerson)
|
||||
.where('asset_faces.assetId', 'in', assetIds)
|
||||
.where('asset_faces.personId', 'in', personIds)
|
||||
.execute() as Promise<AssetFaceEntity[]>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
|
||||
return this.assetFaceRepository.findOneBy({ personId });
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
|
||||
return (this.db
|
||||
.selectFrom('asset_faces')
|
||||
.selectAll('asset_faces')
|
||||
.where('asset_faces.personId', '=', personId)
|
||||
.executeTakeFirst() ?? null) as Promise<AssetFaceEntity | null>;
|
||||
}
|
||||
|
||||
@GenerateSql()
|
||||
async getLatestFaceDate(): Promise<string | undefined> {
|
||||
const result: { latestDate?: string } | undefined = await this.jobStatusRepository
|
||||
.createQueryBuilder('jobStatus')
|
||||
.select('MAX(jobStatus.facesRecognizedAt)::text', 'latestDate')
|
||||
.getRawOne();
|
||||
const result = (await this.db
|
||||
.selectFrom('asset_job_status')
|
||||
.select((eb) => sql`${eb.fn.max('asset_job_status.facesRecognizedAt')}::text`.as('latestDate'))
|
||||
.executeTakeFirst()) as { latestDate: string } | undefined;
|
||||
|
||||
return result?.latestDate;
|
||||
}
|
||||
|
||||
private async save(person: Partial<PersonEntity>): Promise<PersonEntity> {
|
||||
const { id } = await this.personRepository.save(person);
|
||||
return this.personRepository.findOneByOrFail({ id });
|
||||
}
|
||||
|
||||
private async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise<void> {
|
||||
await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search, person');
|
||||
await this.assetFaceRepository.query('REINDEX TABLE asset_faces');
|
||||
await this.assetFaceRepository.query('REINDEX TABLE person');
|
||||
await sql`VACUUM ANALYZE asset_faces, face_search, person`.execute(this.db);
|
||||
await sql`REINDEX TABLE asset_faces`.execute(this.db);
|
||||
await sql`REINDEX TABLE person`.execute(this.db);
|
||||
if (reindexVectors) {
|
||||
await this.assetFaceRepository.query('REINDEX TABLE face_search');
|
||||
await sql`REINDEX TABLE face_search`.execute(this.db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
SearchPaginationOptions,
|
||||
SmartSearchOptions,
|
||||
} from 'src/interfaces/search.interface';
|
||||
import { anyUuid, asUuid, asVector } from 'src/utils/database';
|
||||
import { anyUuid, asUuid } from 'src/utils/database';
|
||||
import { Paginated } from 'src/utils/pagination';
|
||||
import { isValidInteger } from 'src/validation';
|
||||
|
||||
|
@ -82,7 +82,7 @@ export class SearchRepository implements ISearchRepository {
|
|||
{ page: 1, size: 200 },
|
||||
{
|
||||
takenAfter: DummyValue.DATE,
|
||||
embedding: Array.from({ length: 512 }, Math.random),
|
||||
embedding: DummyValue.VECTOR,
|
||||
lensModel: DummyValue.STRING,
|
||||
withStacked: true,
|
||||
isFavorite: true,
|
||||
|
@ -97,7 +97,7 @@ export class SearchRepository implements ISearchRepository {
|
|||
|
||||
const items = (await searchAssetBuilder(this.db, options)
|
||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.orderBy(sql`smart_search.embedding <=> ${asVector(options.embedding)}`)
|
||||
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
|
||||
.limit(pagination.size + 1)
|
||||
.offset((pagination.page - 1) * pagination.size)
|
||||
.execute()) as any as AssetEntity[];
|
||||
|
@ -111,7 +111,7 @@ export class SearchRepository implements ISearchRepository {
|
|||
params: [
|
||||
{
|
||||
assetId: DummyValue.UUID,
|
||||
embedding: Array.from({ length: 512 }, Math.random),
|
||||
embedding: DummyValue.VECTOR,
|
||||
maxDistance: 0.6,
|
||||
type: AssetType.IMAGE,
|
||||
userIds: [DummyValue.UUID],
|
||||
|
@ -119,7 +119,6 @@ export class SearchRepository implements ISearchRepository {
|
|||
],
|
||||
})
|
||||
searchDuplicates({ assetId, embedding, maxDistance, type, userIds }: AssetDuplicateSearch) {
|
||||
const vector = asVector(embedding);
|
||||
return this.db
|
||||
.with('cte', (qb) =>
|
||||
qb
|
||||
|
@ -127,7 +126,7 @@ export class SearchRepository implements ISearchRepository {
|
|||
.select([
|
||||
'assets.id as assetId',
|
||||
'assets.duplicateId',
|
||||
sql<number>`smart_search.embedding <=> ${vector}`.as('distance'),
|
||||
sql<number>`smart_search.embedding <=> ${embedding}`.as('distance'),
|
||||
])
|
||||
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
|
||||
.where('assets.ownerId', '=', anyUuid(userIds))
|
||||
|
@ -135,7 +134,7 @@ export class SearchRepository implements ISearchRepository {
|
|||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.type', '=', type)
|
||||
.where('assets.id', '!=', asUuid(assetId))
|
||||
.orderBy(sql`smart_search.embedding <=> ${vector}`)
|
||||
.orderBy(sql`smart_search.embedding <=> ${embedding}`)
|
||||
.limit(64),
|
||||
)
|
||||
.selectFrom('cte')
|
||||
|
@ -148,7 +147,7 @@ export class SearchRepository implements ISearchRepository {
|
|||
params: [
|
||||
{
|
||||
userIds: [DummyValue.UUID],
|
||||
embedding: Array.from({ length: 512 }, Math.random),
|
||||
embedding: DummyValue.VECTOR,
|
||||
numResults: 10,
|
||||
maxDistance: 0.6,
|
||||
},
|
||||
|
@ -159,7 +158,6 @@ export class SearchRepository implements ISearchRepository {
|
|||
throw new Error(`Invalid value for 'numResults': ${numResults}`);
|
||||
}
|
||||
|
||||
const vector = asVector(embedding);
|
||||
return this.db
|
||||
.with('cte', (qb) =>
|
||||
qb
|
||||
|
@ -167,14 +165,14 @@ export class SearchRepository implements ISearchRepository {
|
|||
.select([
|
||||
'asset_faces.id',
|
||||
'asset_faces.personId',
|
||||
sql<number>`face_search.embedding <=> ${vector}`.as('distance'),
|
||||
sql<number>`face_search.embedding <=> ${embedding}`.as('distance'),
|
||||
])
|
||||
.innerJoin('assets', 'assets.id', 'asset_faces.assetId')
|
||||
.innerJoin('face_search', 'face_search.faceId', 'asset_faces.id')
|
||||
.where('assets.ownerId', '=', anyUuid(userIds))
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.$if(!!hasPerson, (qb) => qb.where('asset_faces.personId', 'is not', null))
|
||||
.orderBy(sql`face_search.embedding <=> ${vector}`)
|
||||
.orderBy(sql`face_search.embedding <=> ${embedding}`)
|
||||
.limit(numResults),
|
||||
)
|
||||
.selectFrom('cte')
|
||||
|
@ -258,12 +256,11 @@ export class SearchRepository implements ISearchRepository {
|
|||
.execute() as any as Promise<AssetEntity[]>;
|
||||
}
|
||||
|
||||
async upsert(assetId: string, embedding: number[]): Promise<void> {
|
||||
const vector = asVector(embedding);
|
||||
async upsert(assetId: string, embedding: string): Promise<void> {
|
||||
await this.db
|
||||
.insertInto('smart_search')
|
||||
.values({ assetId: asUuid(assetId), embedding: vector } as any)
|
||||
.onConflict((oc) => oc.column('assetId').doUpdateSet({ embedding: vector } as any))
|
||||
.values({ assetId: asUuid(assetId), embedding } as any)
|
||||
.onConflict((oc) => oc.column('assetId').doUpdateSet({ embedding } as any))
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import { exec as execCallback } from 'node:child_process';
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
const maybeFirstLine = async (command: string): Promise<string> => {
|
||||
|
@ -36,7 +36,7 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => {
|
|||
@Injectable()
|
||||
export class ServerInfoRepository implements IServerInfoRepository {
|
||||
constructor(
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(ServerInfoRepository.name);
|
||||
|
|
|
@ -1,84 +1,113 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { ExpressionBuilder, Kysely, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { StackEntity } from 'src/entities/stack.entity';
|
||||
import { IStackRepository, StackSearch } from 'src/interfaces/stack.interface';
|
||||
import { DataSource, In, Repository } from 'typeorm';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
const withAssets = (eb: ExpressionBuilder<DB, 'asset_stack'>, withTags = false) => {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.selectAll()
|
||||
.$if(withTags, (eb) =>
|
||||
eb.select((eb) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('tags')
|
||||
.selectAll('tags')
|
||||
.innerJoin('tag_asset', 'tags.id', 'tag_asset.tagsId')
|
||||
.whereRef('tag_asset.assetsId', '=', 'assets.id'),
|
||||
).as('tags'),
|
||||
),
|
||||
)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id'),
|
||||
).as('assets');
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class StackRepository implements IStackRepository {
|
||||
constructor(
|
||||
@InjectDataSource() private dataSource: DataSource,
|
||||
@InjectRepository(StackEntity) private repository: Repository<StackEntity>,
|
||||
) {}
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [{ ownerId: DummyValue.UUID }] })
|
||||
search(query: StackSearch): Promise<StackEntity[]> {
|
||||
return this.repository.find({
|
||||
where: {
|
||||
ownerId: query.ownerId,
|
||||
primaryAssetId: query.primaryAssetId,
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
return this.db
|
||||
.selectFrom('asset_stack')
|
||||
.selectAll('asset_stack')
|
||||
.select(withAssets)
|
||||
.where('asset_stack.ownerId', '=', query.ownerId)
|
||||
.$if(!!query.primaryAssetId, (eb) => eb.where('asset_stack.primaryAssetId', '=', query.primaryAssetId!))
|
||||
.execute() as unknown as Promise<StackEntity[]>;
|
||||
}
|
||||
|
||||
async create(entity: { ownerId: string; assetIds: string[] }): Promise<StackEntity> {
|
||||
return this.dataSource.manager.transaction(async (manager) => {
|
||||
const stackRepository = manager.getRepository(StackEntity);
|
||||
|
||||
const stacks = await stackRepository.find({
|
||||
where: {
|
||||
ownerId: entity.ownerId,
|
||||
primaryAssetId: In(entity.assetIds),
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
assets: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
return this.db.transaction().execute(async (tx) => {
|
||||
const stacks = await tx
|
||||
.selectFrom('asset_stack')
|
||||
.where('asset_stack.ownerId', '=', entity.ownerId)
|
||||
.where('asset_stack.primaryAssetId', 'in', entity.assetIds)
|
||||
.select('asset_stack.id')
|
||||
.select((eb) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('assets')
|
||||
.select('assets.id')
|
||||
.whereRef('assets.stackId', '=', 'asset_stack.id')
|
||||
.where('assets.deletedAt', 'is', null),
|
||||
).as('assets'),
|
||||
)
|
||||
.execute();
|
||||
|
||||
const assetIds = new Set<string>(entity.assetIds);
|
||||
|
||||
// children
|
||||
for (const stack of stacks) {
|
||||
for (const asset of stack.assets) {
|
||||
assetIds.add(asset.id);
|
||||
if (stack.assets && stack.assets.length > 0) {
|
||||
for (const asset of stack.assets) {
|
||||
assetIds.add(asset.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stacks.length > 0) {
|
||||
await stackRepository.delete({ id: In(stacks.map((stack) => stack.id)) });
|
||||
await tx
|
||||
.deleteFrom('asset_stack')
|
||||
.where(
|
||||
'id',
|
||||
'in',
|
||||
stacks.map((stack) => stack.id),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
|
||||
const { id } = await stackRepository.save({
|
||||
ownerId: entity.ownerId,
|
||||
primaryAssetId: entity.assetIds[0],
|
||||
assets: [...assetIds].map((id) => ({ id }) as AssetEntity),
|
||||
});
|
||||
const newRecord = await tx
|
||||
.insertInto('asset_stack')
|
||||
.values({
|
||||
ownerId: entity.ownerId,
|
||||
primaryAssetId: entity.assetIds[0],
|
||||
})
|
||||
.returning('id')
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return stackRepository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
await tx
|
||||
.updateTable('assets')
|
||||
.set({
|
||||
stackId: newRecord.id,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where('id', 'in', [...assetIds])
|
||||
.execute();
|
||||
|
||||
return tx
|
||||
.selectFrom('asset_stack')
|
||||
.selectAll('asset_stack')
|
||||
.select(withAssets)
|
||||
.where('id', '=', newRecord.id)
|
||||
.executeTakeFirst() as unknown as Promise<StackEntity>;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -91,12 +120,12 @@ export class StackRepository implements IStackRepository {
|
|||
|
||||
const assetIds = stack.assets.map(({ id }) => id);
|
||||
|
||||
await this.repository.delete(id);
|
||||
|
||||
// Update assets updatedAt
|
||||
await this.dataSource.manager.update(AssetEntity, assetIds, {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
await this.db.deleteFrom('asset_stack').where('id', '=', asUuid(id)).execute();
|
||||
await this.db
|
||||
.updateTable('assets')
|
||||
.set({ stackId: null, updatedAt: new Date() })
|
||||
.where('id', 'in', assetIds)
|
||||
.execute();
|
||||
}
|
||||
|
||||
async deleteAll(ids: string[]): Promise<void> {
|
||||
|
@ -110,54 +139,31 @@ export class StackRepository implements IStackRepository {
|
|||
assetIds.push(...stack.assets.map(({ id }) => id));
|
||||
}
|
||||
|
||||
await this.repository.delete(ids);
|
||||
|
||||
// Update assets updatedAt
|
||||
await this.dataSource.manager.update(AssetEntity, assetIds, {
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
await this.db
|
||||
.updateTable('assets')
|
||||
.set({ updatedAt: new Date(), stackId: null })
|
||||
.where('id', 'in', assetIds)
|
||||
.where('stackId', 'in', ids)
|
||||
.execute();
|
||||
}
|
||||
|
||||
update(entity: Partial<StackEntity>) {
|
||||
return this.save(entity);
|
||||
update(id: string, entity: Updateable<StackEntity>): Promise<StackEntity> {
|
||||
return this.db
|
||||
.updateTable('asset_stack')
|
||||
.set(entity)
|
||||
.where('id', '=', asUuid(id))
|
||||
.returningAll('asset_stack')
|
||||
.returning((eb) => withAssets(eb, true))
|
||||
.executeTakeFirstOrThrow() as unknown as Promise<StackEntity>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getById(id: string): Promise<StackEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
tags: true,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
assets: {
|
||||
fileCreatedAt: 'ASC',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private async save(entity: Partial<StackEntity>) {
|
||||
const { id } = await this.repository.save(entity);
|
||||
return this.repository.findOneOrFail({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
relations: {
|
||||
assets: {
|
||||
exifInfo: true,
|
||||
},
|
||||
},
|
||||
order: {
|
||||
assets: {
|
||||
fileCreatedAt: 'ASC',
|
||||
},
|
||||
},
|
||||
});
|
||||
getById(id: string): Promise<StackEntity | undefined> {
|
||||
return this.db
|
||||
.selectFrom('asset_stack')
|
||||
.selectAll()
|
||||
.select((eb) => withAssets(eb, true))
|
||||
.where('id', '=', asUuid(id))
|
||||
.executeTakeFirst() as Promise<StackEntity | undefined>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,9 @@ import { MetricService } from 'nestjs-otel';
|
|||
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { ImmichTelemetry, MetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMetricGroupRepository, ITelemetryRepository, MetricGroupOptions } from 'src/interfaces/telemetry.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
class MetricGroupRepository implements IMetricGroupRepository {
|
||||
private enabled = false;
|
||||
|
@ -95,7 +95,7 @@ export class TelemetryRepository implements ITelemetryRepository {
|
|||
constructor(
|
||||
private metricService: MetricService,
|
||||
private reflect: Reflector,
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
const { telemetry } = this.configRepository.getEnv();
|
||||
|
|
|
@ -2,15 +2,14 @@ import { Kysely } from 'kysely';
|
|||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DB } from 'src/db';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetEntity, withExif } from 'src/entities/asset.entity';
|
||||
import { IViewRepository } from 'src/interfaces/view.interface';
|
||||
import { withExif } from 'src/entities/asset.entity';
|
||||
import { asUuid } from 'src/utils/database';
|
||||
|
||||
export class ViewRepository implements IViewRepository {
|
||||
export class ViewRepository {
|
||||
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getUniqueOriginalPaths(userId: string): Promise<string[]> {
|
||||
async getUniqueOriginalPaths(userId: string) {
|
||||
const results = await this.db
|
||||
.selectFrom('assets')
|
||||
.select((eb) => eb.fn<string>('substring', ['assets.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath'))
|
||||
|
@ -25,7 +24,7 @@ export class ViewRepository implements IViewRepository {
|
|||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||
async getAssetsByOriginalPath(userId: string, partialPath: string): Promise<AssetEntity[]> {
|
||||
async getAssetsByOriginalPath(userId: string, partialPath: string) {
|
||||
const normalizedPath = partialPath.replaceAll(/^\/|\/$/g, '');
|
||||
|
||||
return this.db
|
||||
|
@ -42,6 +41,6 @@ export class ViewRepository implements IViewRepository {
|
|||
(eb) => eb.fn('regexp_replace', ['assets.originalPath', eb.val('.*/(.+)'), eb.val(String.raw`\1`)]),
|
||||
'asc',
|
||||
)
|
||||
.execute() as any as Promise<AssetEntity[]>;
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,14 +135,17 @@ describe(AlbumService.name, () => {
|
|||
assetIds: ['123'],
|
||||
});
|
||||
|
||||
expect(albumMock.create).toHaveBeenCalledWith({
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: albumStub.empty.albumName,
|
||||
description: albumStub.empty.description,
|
||||
albumUsers: [{ userId: 'user-id', role: AlbumUserRole.EDITOR }],
|
||||
assets: [{ id: '123' }],
|
||||
albumThumbnailAssetId: '123',
|
||||
});
|
||||
expect(albumMock.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: albumStub.empty.albumName,
|
||||
description: albumStub.empty.description,
|
||||
|
||||
albumThumbnailAssetId: '123',
|
||||
},
|
||||
['123'],
|
||||
[{ userId: 'user-id', role: AlbumUserRole.EDITOR }],
|
||||
);
|
||||
|
||||
expect(userMock.get).toHaveBeenCalledWith('user-id', {});
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']));
|
||||
|
@ -175,14 +178,17 @@ describe(AlbumService.name, () => {
|
|||
assetIds: ['asset-1', 'asset-2'],
|
||||
});
|
||||
|
||||
expect(albumMock.create).toHaveBeenCalledWith({
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: 'Test album',
|
||||
description: '',
|
||||
albumUsers: [],
|
||||
assets: [{ id: 'asset-1' }],
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(albumMock.create).toHaveBeenCalledWith(
|
||||
{
|
||||
ownerId: authStub.admin.user.id,
|
||||
albumName: 'Test album',
|
||||
description: '',
|
||||
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
},
|
||||
['asset-1'],
|
||||
[],
|
||||
);
|
||||
expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(
|
||||
authStub.admin.user.id,
|
||||
new Set(['asset-1', 'asset-2']),
|
||||
|
@ -192,7 +198,7 @@ describe(AlbumService.name, () => {
|
|||
|
||||
describe('update', () => {
|
||||
it('should prevent updating an album that does not exist', async () => {
|
||||
albumMock.getById.mockResolvedValue(null);
|
||||
albumMock.getById.mockResolvedValue(void 0);
|
||||
|
||||
await expect(
|
||||
sut.update(authStub.user1, 'invalid-id', {
|
||||
|
@ -238,7 +244,7 @@ describe(AlbumService.name, () => {
|
|||
});
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledTimes(1);
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-4', {
|
||||
id: 'album-4',
|
||||
albumName: 'new album name',
|
||||
});
|
||||
|
@ -344,7 +350,7 @@ describe(AlbumService.name, () => {
|
|||
describe('removeUser', () => {
|
||||
it('should require a valid album id', async () => {
|
||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1']));
|
||||
albumMock.getById.mockResolvedValue(null);
|
||||
albumMock.getById.mockResolvedValue(void 0);
|
||||
await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(albumMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -529,7 +535,7 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-3' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
|
@ -547,7 +553,7 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-1' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-id',
|
||||
|
@ -569,7 +575,7 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-3' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
|
@ -606,7 +612,7 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-3' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
|
@ -629,7 +635,7 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-1' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
expect(albumMock.update).toHaveBeenCalledWith('album-123', {
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
|
@ -696,7 +702,6 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-id' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) });
|
||||
expect(albumMock.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']);
|
||||
});
|
||||
|
||||
|
@ -720,8 +725,6 @@ describe(AlbumService.name, () => {
|
|||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||
{ success: true, id: 'asset-id' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) });
|
||||
});
|
||||
|
||||
it('should reset the thumbnail if it is removed', async () => {
|
||||
|
@ -734,10 +737,6 @@ describe(AlbumService.name, () => {
|
|||
{ success: true, id: 'asset-id' },
|
||||
]);
|
||||
|
||||
expect(albumMock.update).toHaveBeenCalledWith({
|
||||
id: 'album-123',
|
||||
updatedAt: expect.any(Date),
|
||||
});
|
||||
expect(albumMock.updateThumbnails).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,6 @@ import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
|||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AlbumUserEntity } from 'src/entities/album-user.entity';
|
||||
import { AlbumEntity } from 'src/entities/album.entity';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/interfaces/album.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
|
@ -112,16 +111,18 @@ export class AlbumService extends BaseService {
|
|||
permission: Permission.ASSET_SHARE,
|
||||
ids: dto.assetIds || [],
|
||||
});
|
||||
const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity);
|
||||
const assetIds = [...allowedAssetIdsSet].map((id) => id);
|
||||
|
||||
const album = await this.albumRepository.create({
|
||||
ownerId: auth.user.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
albumUsers: albumUsers.map((albumUser) => albumUser as AlbumUserEntity) ?? [],
|
||||
assets,
|
||||
albumThumbnailAssetId: assets[0]?.id || null,
|
||||
});
|
||||
const album = await this.albumRepository.create(
|
||||
{
|
||||
ownerId: auth.user.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
albumThumbnailAssetId: assetIds[0] || null,
|
||||
},
|
||||
assetIds,
|
||||
albumUsers,
|
||||
);
|
||||
|
||||
for (const { userId } of albumUsers) {
|
||||
await this.eventRepository.emit('album.invite', { id: album.id, userId });
|
||||
|
@ -141,7 +142,7 @@ export class AlbumService extends BaseService {
|
|||
throw new BadRequestException('Invalid album thumbnail');
|
||||
}
|
||||
}
|
||||
const updatedAlbum = await this.albumRepository.update({
|
||||
const updatedAlbum = await this.albumRepository.update(album.id, {
|
||||
id: album.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
|
@ -170,7 +171,7 @@ export class AlbumService extends BaseService {
|
|||
|
||||
const { id: firstNewAssetId } = results.find(({ success }) => success) || {};
|
||||
if (firstNewAssetId) {
|
||||
await this.albumRepository.update({
|
||||
await this.albumRepository.update(id, {
|
||||
id,
|
||||
updatedAt: new Date(),
|
||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
|
||||
|
@ -199,11 +200,8 @@ export class AlbumService extends BaseService {
|
|||
);
|
||||
|
||||
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||
if (removedIds.length > 0) {
|
||||
await this.albumRepository.update({ id, updatedAt: new Date() });
|
||||
if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
||||
await this.albumRepository.updateThumbnails();
|
||||
}
|
||||
if (removedIds.length > 0 && album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
||||
await this.albumRepository.updateThumbnails();
|
||||
}
|
||||
|
||||
return results;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { BadRequestException } from '@nestjs/common';
|
||||
import { Permission } from 'src/enum';
|
||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { APIKeyService } from 'src/services/api-key.service';
|
||||
import { IApiKeyRepository } from 'src/types';
|
||||
import { keyStub } from 'test/fixtures/api-key.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
|
@ -12,7 +12,7 @@ describe(APIKeyService.name, () => {
|
|||
let sut: APIKeyService;
|
||||
|
||||
let cryptoMock: Mocked<ICryptoRepository>;
|
||||
let keyMock: Mocked<IKeyRepository>;
|
||||
let keyMock: Mocked<IApiKeyRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
({ sut, cryptoMock, keyMock } = newTestService(APIKeyService));
|
||||
|
@ -56,8 +56,6 @@ describe(APIKeyService.name, () => {
|
|||
|
||||
describe('update', () => {
|
||||
it('should throw an error if the key is not found', async () => {
|
||||
keyMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.update(authStub.admin, 'random-guid', { name: 'New Name' })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
@ -77,8 +75,6 @@ describe(APIKeyService.name, () => {
|
|||
|
||||
describe('delete', () => {
|
||||
it('should throw an error if the key is not found', async () => {
|
||||
keyMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.delete(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(keyMock.delete).not.toHaveBeenCalledWith('random-guid');
|
||||
|
@ -95,8 +91,6 @@ describe(APIKeyService.name, () => {
|
|||
|
||||
describe('getById', () => {
|
||||
it('should throw an error if the key is not found', async () => {
|
||||
keyMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.getById(authStub.admin, 'random-guid')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(keyMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'random-guid');
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { APIKeyEntity } from 'src/entities/api-key.entity';
|
||||
import { Permission } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { ApiKeyItem } from 'src/types';
|
||||
import { isGranted } from 'src/utils/access';
|
||||
|
||||
@Injectable()
|
||||
|
@ -57,13 +58,13 @@ export class APIKeyService extends BaseService {
|
|||
return keys.map((key) => this.map(key));
|
||||
}
|
||||
|
||||
private map(entity: APIKeyEntity): APIKeyResponseDto {
|
||||
private map(entity: ApiKeyItem): APIKeyResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
name: entity.name,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
permissions: entity.permissions,
|
||||
permissions: entity.permissions as Permission[],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ import { Cron, CronExpression, Interval } from '@nestjs/schedule';
|
|||
import { NextFunction, Request, Response } from 'express';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { ONE_HOUR } from 'src/constants';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
|
@ -38,7 +38,7 @@ export class ApiService {
|
|||
private jobService: JobService,
|
||||
private sharedLinkService: SharedLinkService,
|
||||
private versionService: VersionService,
|
||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||
private configRepository: ConfigRepository,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
this.logger.setContext(ApiService.name);
|
||||
|
|
|
@ -520,7 +520,7 @@ describe(AssetService.name, () => {
|
|||
|
||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||
|
||||
expect(stackMock.update).toHaveBeenCalledWith({
|
||||
expect(stackMock.update).toHaveBeenCalledWith('stack-1', {
|
||||
id: 'stack-1',
|
||||
primaryAssetId: 'stack-child-asset-1',
|
||||
});
|
||||
|
|
|
@ -192,7 +192,7 @@ export class AssetService extends BaseService {
|
|||
const stackAssetIds = asset.stack.assets.map((a) => a.id);
|
||||
if (stackAssetIds.length > 2) {
|
||||
const newPrimaryAssetId = stackAssetIds.find((a) => a !== id)!;
|
||||
await this.stackRepository.update({
|
||||
await this.stackRepository.update(asset.stack.id, {
|
||||
id: asset.stack.id,
|
||||
primaryAssetId: newPrimaryAssetId,
|
||||
});
|
||||
|
|
|
@ -2,12 +2,12 @@ import { BadRequestException } from '@nestjs/common';
|
|||
import { FileReportItemDto } from 'src/dtos/audit.dto';
|
||||
import { AssetFileType, AssetPathType, DatabaseAction, EntityType, PersonPathType, UserPathType } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { JobStatus } from 'src/interfaces/job.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AuditService } from 'src/services/audit.service';
|
||||
import { IAuditRepository } from 'src/types';
|
||||
import { auditStub } from 'test/fixtures/audit.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { newTestService } from 'test/utils';
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue