1
0
forked from Cutlery/immich

Compare commits

..

46 Commits

Author SHA1 Message Date
Alex Tran c3c692aad8 Revert "Revert "fix(server): on_asset_update event sends varying data types (#7179)""
This reverts commit 26c3635291.
2024-02-22 09:24:41 -06:00
Alex Tran 2e99ce994b Merge branch 'fix-stacking' of github.com:immich-app/immich into fix-stacking 2024-02-22 09:21:55 -06:00
Alex Tran 26c3635291 Revert "fix(server): on_asset_update event sends varying data types (#7179)"
This reverts commit 4b46bb49d7.
2024-02-22 09:20:16 -06:00
Jason Rasmussen b3131dfe14 refactor(web): sidebar settings (#7344) 2024-02-22 10:14:11 -05:00
Ravid Yael b4e924b0c0 web: improve storage template onboarding message (#7339)
* Style: modifing onboard message for using storage template engine

* Style: modifying onboard message for using storage template engine

* style: Fix Prettier formatting issues

* chore: cleanup message

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-02-22 15:14:02 +00:00
Alex Tran 484f1256ea Merge branch 'fix-stacking' of github.com:immich-app/immich into fix-stacking 2024-02-22 09:11:06 -06:00
Alex Tran d51a666692 revert 2024-02-22 09:10:58 -06:00
Alex Tran 73f20ef4e7 Merge branch 'main' of github.com:immich-app/immich into fix-stacking 2024-02-22 08:40:45 -06:00
martin 01d6707b59 feat(web): add an option to change the date formats (#7174)
* feat: add an option to change the date formats

* pr feedback

* fix: change title

* fix: show list supported by the browser

* fix: tests

* fix: dates

* fix: check only if locale is set

* fix: better fallback value

* fix: fallback

* fix: fallback

* feat: add default locale option

* refactor: shared components

* refactor: shared components

* prepare for svelte 5

* don't use relative paths

* refactor: fallback value

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* fix: parsing store

* fix: lint

* refactor: locales

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-02-22 14:36:14 +00:00
Alex Tran e1f66ac4da Merge branch 'main' of github.com:immich-app/immich into fix-stacking 2024-02-22 08:34:00 -06:00
荣顶 a224bb23d0 docs: add star history for README (#7328) 2024-02-22 09:05:13 -05:00
martin 75947ab6c2 feat(web): search albums (#7322)
* feat: search albums

* pr feedback

* fix: comparison

* pr feedback

* simplify

* chore: more compact album padding

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-02-22 09:04:43 -05:00
Michel Heusschen e3cccba78c fix(server): out of memory when unstacking assets (#7332) 2024-02-22 08:50:46 -05:00
Michel Heusschen ec55acc98c perf(server): optimize mapAsset (#7331) 2024-02-22 08:48:27 -05:00
renovate[bot] 869e9f1399 chore(deps): update base-image to v20240222 (major) (#7338)
chore(deps): update base-image to v20240222

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-22 08:16:56 -05:00
Michel Heusschen 46f85618db feat(web): rework combobox and add clear button (#7317)
* feat(web): rework combobox

* simplify statement and use transition-all
2024-02-22 08:12:33 -05:00
Alex Tran 2d95715ae8 revert 2024-02-21 22:18:53 -06:00
Alex Tran 692b8b189a fixed 2024-02-21 22:12:50 -06:00
Mert 749b182f97 fix(server): fix log for setting other vector extension (#7325)
fix log
2024-02-21 18:47:43 -05:00
Jason Rasmussen 2ebb57cbd4 refactor: e2e client (#7324) 2024-02-21 17:34:11 -05:00
Alex 7d51fba1c0 log to track 2024-02-21 16:27:09 -06:00
martin 5c0c98473d fix(server, web): people page (#7319)
* fix: people page

* fix: use locale

* fix: e2e

* fix: remove useless w-full

* fix: don't count people without thumbnail

* fix: es6 template string

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-02-21 17:03:45 -05:00
Jason Rasmussen 546edc2e91 refactor: album e2e (#7320)
* refactor: album e2e

* refactor: user e2e
2024-02-21 16:52:13 -05:00
Alex Tran 4c9ac82fdc fix(server): stack info in asset response for mobile 2024-02-21 12:38:31 -06:00
Michel Heusschen 173b47033a fix(server): search with same face multiple times (#7306) 2024-02-21 09:52:38 -06:00
Michel Heusschen d3e14fd662 feat(web): search improvements and refactor (#7291) 2024-02-21 09:50:50 -06:00
Marcel Eeken 06c134950a Localize the output of the library count to make it more readable (#7305) 2024-02-21 14:35:24 +01:00
martin 8f57bfb496 fix(web): small issues everywhere (#7207)
* multiple fix

* fix: album re-render

* fix: revert re-render album

* fix: linter

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-02-21 08:29:22 -05:00
Michel Heusschen 855aa8e30a fix(web): back button for gallery viewer (#7250) 2024-02-21 08:28:16 -05:00
Jason Rasmussen f798e037d5 refactor(server): e2e (#7265)
* refactor: activity e2e

* refactor: person e2e

* refactor: shared link e2e
2024-02-21 08:28:03 -05:00
renovate[bot] a1bc74cdd6 fix(deps): update exiftool (#7230)
* fix(deps): update exiftool

* documenting such changes would have been too easy

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-02-21 08:26:13 -05:00
renovate[bot] aeb7081af1 chore(deps): update @immich/cli (#7235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 08:25:59 -05:00
renovate[bot] c5da317033 chore(deps): update dependency @types/node to v20.11.19 (#7236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 08:25:45 -05:00
renovate[bot] 01f682134a chore(deps): update dependency @types/node to v20.11.19 (#7238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 08:25:37 -05:00
renovate[bot] 43f887e5f2 chore(deps): update dependency @types/node to v20.11.19 (#7239)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 08:25:28 -05:00
renovate[bot] ee3b3ca115 chore(deps): update dependency vite to v5.1.3 (#7247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-21 08:24:50 -05:00
Michel Heusschen e7995014f9 fix(web): search filter form events (#7285) 2024-02-21 08:21:43 -05:00
renovate[bot] a771f33fa3 chore(deps): update machine-learning (#7225) 2024-02-21 01:12:38 -05:00
Mert 397570ad1a chore(server): change transcode default to accept all supported audio codecs (#7283)
* change transcode defaults

* don't untick accepted audio codecs

* no need to change the transcode policy

* fix tests

* remove log
2024-02-21 00:25:30 -05:00
Alex The Bot 7c34d0595e Version v1.95.1 2024-02-21 04:02:43 +00:00
Mert eb73f6605b fix(server): don't return archived assets by default (#7278)
* don't show archived results by default

* fix e2e

* generate sql

* set default in dto

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-02-21 03:59:26 +00:00
Alex bb5236ae65 fix(server): not in album filter with context search (#7275) 2024-02-20 20:44:34 -06:00
Mert e33fd40b4c fix(server): quote database name in migration (#7277)
quote database name
2024-02-21 02:13:43 +00:00
Alex 73825918c0 fix(web): presenting modal removes the browser's scroll ability (#7257)
* fix(web): presenting modal removes the browser's scroll ability

* removed unsued code

* eslint
2024-02-20 14:02:01 -06:00
martyfuhry a22bf99206 fix(mobile): Uses immich thumbnail for background of memory picture (#7254)
unused imports
2024-02-20 13:58:41 -06:00
Alex 578b71b961 chore: post release tasks 2024-02-20 11:05:24 -06:00
147 changed files with 2283 additions and 1822 deletions
+6
View File
@@ -128,3 +128,9 @@ If you feel like this is the right cause and the app is something you are seeing
<a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a>
## Star History
<a href="https://star-history.com/#immich-app/immich">
<img src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" alt="Star History Chart" width="100%" />
</a>
+12 -12
View File
@@ -1325,9 +1325,9 @@
}
},
"node_modules/@types/node": {
"version": "20.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
"integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==",
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -5240,9 +5240,9 @@
}
},
"node_modules/vite": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.2.tgz",
"integrity": "sha512-uwiFebQbTWRIGbCaTEBVAfKqgqKNKMJ2uPXsXeLIZxM8MVMjoS3j0cG8NrPxdDIadaWnPSjrkLWffLSC+uiP3Q==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
"integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
"dev": true,
"dependencies": {
"esbuild": "^0.19.3",
@@ -6481,9 +6481,9 @@
}
},
"@types/node": {
"version": "20.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
"integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==",
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
@@ -9364,9 +9364,9 @@
}
},
"vite": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.2.tgz",
"integrity": "sha512-uwiFebQbTWRIGbCaTEBVAfKqgqKNKMJ2uPXsXeLIZxM8MVMjoS3j0cG8NrPxdDIadaWnPSjrkLWffLSC+uiP3Q==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
"integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
"dev": true,
"requires": {
"esbuild": "^0.19.3",
+2 -2
View File
@@ -17,7 +17,7 @@ x-server-build: &server-common
services:
immich-server:
container_name: immich_server
command: [ "./start-server.sh" ]
command: [ "start.sh", "immich" ]
<<: *server-common
ports:
- 2283:3001
@@ -27,7 +27,7 @@ services:
immich-microservices:
container_name: immich_microservices
command: [ "./start-microservices.sh" ]
command: [ "start.sh", "microservices" ]
<<: *server-common
# extends:
# file: hwaccel.transcoding.yml
+8 -3
View File
@@ -23,9 +23,13 @@
}
},
"../cli": {
"name": "@immich/cli",
"version": "2.0.8",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"lodash-es": "^4.17.21"
},
"bin": {
"immich": "dist/index.js"
},
@@ -34,6 +38,7 @@
"@testcontainers/postgresql": "^10.7.1",
"@types/byte-size": "^8.1.0",
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.3.1",
"@typescript-eslint/eslint-plugin": "^7.0.0",
@@ -801,9 +806,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
"integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==",
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -1,79 +1,94 @@
import { AlbumResponseDto, LoginResponseDto, ReactionType } from '@app/domain';
import { ActivityController } from '@app/immich';
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
import { ActivityEntity } from '@app/infra/entities';
import { errorStub, userDto, uuidStub } from '@test/fixtures';
import {
ActivityCreateDto,
AlbumResponseDto,
AssetResponseDto,
LoginResponseDto,
ReactionType,
createActivity as create,
createAlbum,
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import request from 'supertest';
import { api } from '../../client';
import { testApp } from '../utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe(`${ActivityController.name} (e2e)`, () => {
let server: any;
describe('/activity', () => {
let admin: LoginResponseDto;
let asset: AssetFileUploadResponseDto;
let album: AlbumResponseDto;
let nonOwner: LoginResponseDto;
let asset: AssetResponseDto;
let album: AlbumResponseDto;
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
create(
{ activityCreateDto: dto },
{ headers: asBearerAuth(accessToken || admin.accessToken) }
);
beforeAll(async () => {
server = (await testApp.create()).getHttpServer();
await testApp.reset();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);
asset = await api.assetApi.upload(server, admin.accessToken, 'example');
apiUtils.setup();
await dbUtils.reset();
await api.userApi.create(server, admin.accessToken, userDto.user1);
nonOwner = await api.authApi.login(server, userDto.user1);
album = await api.albumApi.create(server, admin.accessToken, {
albumName: 'Album 1',
assetIds: [asset.id],
sharedWithUserIds: [nonOwner.userId],
});
});
afterAll(async () => {
await testApp.teardown();
admin = await apiUtils.adminSetup();
nonOwner = await apiUtils.userSetup(admin.accessToken, createUserDto.user1);
asset = await apiUtils.createAsset(admin.accessToken);
album = await createAlbum(
{
createAlbumDto: {
albumName: 'Album 1',
assetIds: [asset.id],
sharedWithUserIds: [nonOwner.userId],
},
},
{ headers: asBearerAuth(admin.accessToken) }
);
});
beforeEach(async () => {
await testApp.reset({ entities: [ActivityEntity] });
await dbUtils.reset(['activity']);
});
describe('GET /activity', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/activity');
const { status, body } = await request(app).get('/activity');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
expect(body).toEqual(
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
);
});
it('should reject an invalid albumId', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/activity')
.query({ albumId: uuidStub.invalid })
.query({ albumId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
expect(body).toEqual(
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
);
});
it('should reject an invalid assetId', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/activity')
.query({ albumId: uuidStub.notFound, assetId: uuidStub.invalid })
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
expect(body).toEqual(
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID']))
);
});
it('should start off empty', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/activity')
.query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -82,22 +97,22 @@ describe(`${ActivityController.name} (e2e)`, () => {
});
it('should filter by album id', async () => {
const album2 = await api.albumApi.create(server, admin.accessToken, {
albumName: 'Album 2',
assetIds: [asset.id],
});
const album2 = await createAlbum(
{
createAlbumDto: {
albumName: 'Album 2',
assetIds: [asset.id],
},
},
{ headers: asBearerAuth(admin.accessToken) }
);
const [reaction] = await Promise.all([
api.activityApi.create(server, admin.accessToken, {
albumId: album.id,
type: ReactionType.LIKE,
}),
api.activityApi.create(server, admin.accessToken, {
albumId: album2.id,
type: ReactionType.LIKE,
}),
createActivity({ albumId: album.id, type: ReactionType.Like }),
createActivity({ albumId: album2.id, type: ReactionType.Like }),
]);
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/activity')
.query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -108,15 +123,15 @@ describe(`${ActivityController.name} (e2e)`, () => {
it('should filter by type=comment', async () => {
const [reaction] = await Promise.all([
api.activityApi.create(server, admin.accessToken, {
createActivity({
albumId: album.id,
type: ReactionType.COMMENT,
type: ReactionType.Comment,
comment: 'comment',
}),
api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
createActivity({ albumId: album.id, type: ReactionType.Like }),
]);
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/activity')
.query({ albumId: album.id, type: 'comment' })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -127,15 +142,15 @@ describe(`${ActivityController.name} (e2e)`, () => {
it('should filter by type=like', async () => {
const [reaction] = await Promise.all([
api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
api.activityApi.create(server, admin.accessToken, {
createActivity({ albumId: album.id, type: ReactionType.Like }),
createActivity({
albumId: album.id,
type: ReactionType.COMMENT,
type: ReactionType.Comment,
comment: 'comment',
}),
]);
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/activity')
.query({ albumId: album.id, type: 'like' })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -146,18 +161,18 @@ describe(`${ActivityController.name} (e2e)`, () => {
it('should filter by userId', async () => {
const [reaction] = await Promise.all([
api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
createActivity({ albumId: album.id, type: ReactionType.Like }),
]);
const response1 = await request(server)
const response1 = await request(app)
.get('/activity')
.query({ albumId: album.id, userId: uuidStub.notFound })
.query({ albumId: album.id, userId: uuidDto.notFound })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(response1.status).toEqual(200);
expect(response1.body.length).toBe(0);
const response2 = await request(server)
const response2 = await request(app)
.get('/activity')
.query({ albumId: album.id, userId: admin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -169,15 +184,15 @@ describe(`${ActivityController.name} (e2e)`, () => {
it('should filter by assetId', async () => {
const [reaction] = await Promise.all([
api.activityApi.create(server, admin.accessToken, {
createActivity({
albumId: album.id,
assetId: asset.id,
type: ReactionType.LIKE,
type: ReactionType.Like,
}),
api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }),
createActivity({ albumId: album.id, type: ReactionType.Like }),
]);
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/activity')
.query({ albumId: album.id, assetId: asset.id })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -189,34 +204,45 @@ describe(`${ActivityController.name} (e2e)`, () => {
describe('POST /activity', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).post('/activity');
const { status, body } = await request(app).post('/activity');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require an albumId', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidStub.invalid });
.send({ albumId: uuidDto.invalid });
expect(status).toEqual(400);
expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
expect(body).toEqual(
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))
);
});
it('should require a comment when type is comment', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidStub.notFound, type: 'comment', comment: null });
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
expect(status).toEqual(400);
expect(body).toEqual(errorStub.badRequest(['comment must be a string', 'comment should not be empty']));
expect(body).toEqual(
errorDto.badRequest([
'comment must be a string',
'comment should not be empty',
])
);
});
it('should add a comment to an album', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'comment', comment: 'This is my first comment' });
.send({
albumId: album.id,
type: 'comment',
comment: 'This is my first comment',
});
expect(status).toEqual(201);
expect(body).toEqual({
id: expect.any(String),
@@ -229,7 +255,7 @@ describe(`${ActivityController.name} (e2e)`, () => {
});
it('should add a like to an album', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
@@ -245,11 +271,11 @@ describe(`${ActivityController.name} (e2e)`, () => {
});
it('should return a 200 for a duplicate like on the album', async () => {
const reaction = await api.activityApi.create(server, admin.accessToken, {
albumId: album.id,
type: ReactionType.LIKE,
});
const { status, body } = await request(server)
const [reaction] = await Promise.all([
createActivity({ albumId: album.id, type: ReactionType.Like }),
]);
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
@@ -258,12 +284,14 @@ describe(`${ActivityController.name} (e2e)`, () => {
});
it('should not confuse an album like with an asset like', async () => {
const reaction = await api.activityApi.create(server, admin.accessToken, {
albumId: album.id,
assetId: asset.id,
type: ReactionType.LIKE,
});
const { status, body } = await request(server)
const [reaction] = await Promise.all([
createActivity({
albumId: album.id,
assetId: asset.id,
type: ReactionType.Like,
}),
]);
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' });
@@ -272,10 +300,15 @@ describe(`${ActivityController.name} (e2e)`, () => {
});
it('should add a comment to an asset', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'comment', comment: 'This is my first comment' });
.send({
albumId: album.id,
assetId: asset.id,
type: 'comment',
comment: 'This is my first comment',
});
expect(status).toEqual(201);
expect(body).toEqual({
id: expect.any(String),
@@ -288,7 +321,7 @@ describe(`${ActivityController.name} (e2e)`, () => {
});
it('should add a like to an asset', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' });
@@ -304,12 +337,15 @@ describe(`${ActivityController.name} (e2e)`, () => {
});
it('should return a 200 for a duplicate like on an asset', async () => {
const reaction = await api.activityApi.create(server, admin.accessToken, {
albumId: album.id,
assetId: asset.id,
type: ReactionType.LIKE,
});
const { status, body } = await request(server)
const [reaction] = await Promise.all([
createActivity({
albumId: album.id,
assetId: asset.id,
type: ReactionType.Like,
}),
]);
const { status, body } = await request(app)
.post('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' });
@@ -320,50 +356,52 @@ describe(`${ActivityController.name} (e2e)`, () => {
describe('DELETE /activity/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).delete(`/activity/${uuidStub.notFound}`);
const { status, body } = await request(app).delete(
`/activity/${uuidDto.notFound}`
);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid uuid', async () => {
const { status, body } = await request(server)
.delete(`/activity/${uuidStub.invalid}`)
const { status, body } = await request(app)
.delete(`/activity/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should remove a comment from an album', async () => {
const reaction = await api.activityApi.create(server, admin.accessToken, {
const reaction = await createActivity({
albumId: album.id,
type: ReactionType.COMMENT,
type: ReactionType.Comment,
comment: 'This is a test comment',
});
const { status } = await request(server)
const { status } = await request(app)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204);
});
it('should remove a like from an album', async () => {
const reaction = await api.activityApi.create(server, admin.accessToken, {
const reaction = await createActivity({
albumId: album.id,
type: ReactionType.LIKE,
type: ReactionType.Like,
});
const { status } = await request(server)
const { status } = await request(app)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204);
});
it('should let the owner remove a comment by another user', async () => {
const reaction = await api.activityApi.create(server, nonOwner.accessToken, {
const reaction = await createActivity({
albumId: album.id,
type: ReactionType.COMMENT,
type: ReactionType.Comment,
comment: 'This is a test comment',
});
const { status } = await request(server)
const { status } = await request(app)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -371,28 +409,33 @@ describe(`${ActivityController.name} (e2e)`, () => {
});
it('should not let a user remove a comment by another user', async () => {
const reaction = await api.activityApi.create(server, admin.accessToken, {
const reaction = await createActivity({
albumId: album.id,
type: ReactionType.COMMENT,
type: ReactionType.Comment,
comment: 'This is a test comment',
});
const { status, body } = await request(server)
const { status, body } = await request(app)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest('Not found or no activity.delete access'));
expect(body).toEqual(
errorDto.badRequest('Not found or no activity.delete access')
);
});
it('should let a non-owner remove their own comment', async () => {
const reaction = await api.activityApi.create(server, nonOwner.accessToken, {
albumId: album.id,
type: ReactionType.COMMENT,
comment: 'This is a test comment',
});
const reaction = await createActivity(
{
albumId: album.id,
type: ReactionType.Comment,
comment: 'This is a test comment',
},
nonOwner.accessToken
);
const { status } = await request(server)
const { status } = await request(app)
.delete(`/activity/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
@@ -1,11 +1,15 @@
import { AlbumResponseDto, LoginResponseDto } from '@app/domain';
import { AlbumController } from '@app/immich';
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
import { SharedLinkType } from '@app/infra/entities';
import { errorStub, userDto, uuidStub } from '@test/fixtures';
import {
AlbumResponseDto,
AssetResponseDto,
LoginResponseDto,
SharedLinkType,
deleteUser,
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import request from 'supertest';
import { api } from '../../client';
import { testApp } from '../utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
const user1SharedUser = 'user1SharedUser';
const user1SharedLink = 'user1SharedLink';
@@ -14,193 +18,327 @@ const user2SharedUser = 'user2SharedUser';
const user2SharedLink = 'user2SharedLink';
const user2NotShared = 'user2NotShared';
describe(`${AlbumController.name} (e2e)`, () => {
let server: any;
describe('/album', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user1Asset: AssetFileUploadResponseDto;
let user1Asset1: AssetResponseDto;
let user1Asset2: AssetResponseDto;
let user1Albums: AlbumResponseDto[];
let user2: LoginResponseDto;
let user2Albums: AlbumResponseDto[];
let user3: LoginResponseDto; // deleted
beforeAll(async () => {
server = (await testApp.create()).getHttpServer();
});
apiUtils.setup();
await dbUtils.reset();
afterAll(async () => {
await testApp.teardown();
});
admin = await apiUtils.adminSetup();
beforeEach(async () => {
await testApp.reset();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);
await Promise.all([
api.userApi.create(server, admin.accessToken, userDto.user1),
api.userApi.create(server, admin.accessToken, userDto.user2),
[user1, user2, user3] = await Promise.all([
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
]);
[user1, user2] = await Promise.all([
api.authApi.login(server, userDto.user1),
api.authApi.login(server, userDto.user2),
[user1Asset1, user1Asset2] = await Promise.all([
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
]);
user1Asset = await api.assetApi.upload(server, user1.accessToken, 'example');
const albums = await Promise.all([
// user 1
api.albumApi.create(server, user1.accessToken, {
apiUtils.createAlbum(user1.accessToken, {
albumName: user1SharedUser,
sharedWithUserIds: [user2.userId],
assetIds: [user1Asset.id],
assetIds: [user1Asset1.id],
}),
apiUtils.createAlbum(user1.accessToken, {
albumName: user1SharedLink,
assetIds: [user1Asset1.id],
}),
apiUtils.createAlbum(user1.accessToken, {
albumName: user1NotShared,
assetIds: [user1Asset1.id, user1Asset2.id],
}),
api.albumApi.create(server, user1.accessToken, { albumName: user1SharedLink, assetIds: [user1Asset.id] }),
api.albumApi.create(server, user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset.id] }),
// user 2
api.albumApi.create(server, user2.accessToken, {
apiUtils.createAlbum(user2.accessToken, {
albumName: user2SharedUser,
sharedWithUserIds: [user1.userId],
assetIds: [user1Asset.id],
assetIds: [user1Asset1.id],
}),
apiUtils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
apiUtils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
// user 3
apiUtils.createAlbum(user3.accessToken, {
albumName: 'Deleted',
sharedWithUserIds: [user1.userId],
}),
api.albumApi.create(server, user2.accessToken, { albumName: user2SharedLink }),
api.albumApi.create(server, user2.accessToken, { albumName: user2NotShared }),
]);
user1Albums = albums.slice(0, 3);
user2Albums = albums.slice(3);
user2Albums = albums.slice(3, 6);
await Promise.all([
// add shared link to user1SharedLink album
api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.ALBUM,
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: user1Albums[1].id,
}),
// add shared link to user2SharedLink album
api.sharedLinkApi.create(server, user2.accessToken, {
type: SharedLinkType.ALBUM,
apiUtils.createSharedLink(user2.accessToken, {
type: SharedLinkType.Album,
albumId: user2Albums[1].id,
}),
]);
await deleteUser(
{ id: user3.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
});
describe('GET /album', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/album');
const { status, body } = await request(app).get('/album');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should reject an invalid shared param', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/album?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorStub.badRequest(['shared must be a boolean value']));
expect(body).toEqual(
errorDto.badRequest(['shared must be a boolean value'])
);
});
it('should reject an invalid assetId param', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/album?assetId=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(errorStub.badRequest(['assetId must be a UUID']));
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
});
it('should not return shared albums with a deleted owner', async () => {
await api.userApi.delete(server, admin.accessToken, user1.userId);
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/album?shared=true')
.set('Authorization', `Bearer ${user2.accessToken}`);
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body).toHaveLength(3);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ ownerId: user2.userId, albumName: user2SharedLink, shared: true }),
]),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedUser,
shared: true,
}),
expect.objectContaining({
ownerId: user2.userId,
albumName: user2SharedUser,
shared: true,
}),
])
);
});
it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(server).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
const { status, body } = await request(app)
.get('/album')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(3);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedUser, shared: true }),
expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedLink, shared: true }),
expect.objectContaining({ ownerId: user1.userId, albumName: user1NotShared, shared: false }),
]),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedUser,
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1NotShared,
shared: false,
}),
])
);
});
it('should return the album collection filtered by shared', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/album?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(3);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedUser, shared: true }),
expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedLink, shared: true }),
expect.objectContaining({ ownerId: user2.userId, albumName: user2SharedUser, shared: true }),
]),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedUser,
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
shared: true,
}),
expect.objectContaining({
ownerId: user2.userId,
albumName: user2SharedUser,
shared: true,
}),
])
);
});
it('should return the album collection filtered by NOT shared', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/album?shared=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ ownerId: user1.userId, albumName: user1NotShared, shared: false }),
]),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1NotShared,
shared: false,
}),
])
);
});
it('should return the album collection filtered by assetId', async () => {
const asset = await api.assetApi.upload(server, user1.accessToken, 'example2');
await api.albumApi.addAssets(server, user1.accessToken, user1Albums[0].id, { ids: [asset.id] });
const { status, body } = await request(server)
.get(`/album?assetId=${asset.id}`)
const { status, body } = await request(app)
.get(`/album?assetId=${user1Asset2.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
});
it('should return the album collection filtered by assetId and ignores shared=true', async () => {
const { status, body } = await request(server)
.get(`/album?shared=true&assetId=${user1Asset.id}`)
const { status, body } = await request(app)
.get(`/album?shared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
});
it('should return the album collection filtered by assetId and ignores shared=false', async () => {
const { status, body } = await request(server)
.get(`/album?shared=false&assetId=${user1Asset.id}`)
const { status, body } = await request(app)
.get(`/album?shared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
});
});
describe('GET /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/album/${user1Albums[0].id}`
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return album info for own album', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining(user1Albums[0].assets[0])],
});
});
it('should return album info for shared album', async () => {
const { status, body } = await request(app)
.get(`/album/${user2Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user2Albums[0],
assets: [expect.objectContaining(user2Albums[0].assets[0])],
});
});
it('should return album info with assets when withoutAssets is undefined', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining(user1Albums[0].assets[0])],
});
});
it('should return album info without assets when withoutAssets is true', async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [],
assetCount: 1,
});
});
});
describe('GET /album/count', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/album/count');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app)
.get('/album/count')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ owned: 3, shared: 3, notShared: 1 });
});
});
describe('POST /album', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).post('/album').send({ albumName: 'New album' });
const { status, body } = await request(app)
.post('/album')
.send({ albumName: 'New album' });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should create an album', async () => {
const body = await api.albumApi.create(server, user1.accessToken, { albumName: 'New album' });
const { status, body } = await request(app)
.post('/album')
.send({ albumName: 'New album' })
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
createdAt: expect.any(String),
@@ -220,113 +358,56 @@ describe(`${AlbumController.name} (e2e)`, () => {
});
});
describe('GET /album/count', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/album/count');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(server)
.get('/album/count')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ owned: 3, shared: 3, notShared: 1 });
});
});
describe('GET /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get(`/album/${user1Albums[0].id}`);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should return album info for own album', async () => {
const { status, body } = await request(server)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] });
});
it('should return album info for shared album', async () => {
const { status, body } = await request(server)
.get(`/album/${user2Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ ...user2Albums[0], assets: [expect.objectContaining(user2Albums[0].assets[0])] });
});
it('should return album info with assets when withoutAssets is undefined', async () => {
const { status, body } = await request(server)
.get(`/album/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] });
});
it('should return album info without assets when withoutAssets is true', async () => {
const { status, body } = await request(server)
.get(`/album/${user1Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [],
assetCount: 1,
});
});
});
describe('PUT /album/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).put(`/album/${user1Albums[0].id}/assets`);
const { status, body } = await request(app).put(
`/album/${user1Albums[0].id}/assets`
);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should be able to add own asset to own album', async () => {
const asset = await api.assetApi.upload(server, user1.accessToken, 'example1');
const { status, body } = await request(server)
const asset = await apiUtils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
]);
});
it('should be able to add own asset to shared album', async () => {
const asset = await api.assetApi.upload(server, user1.accessToken, 'example1');
const { status, body } = await request(server)
const asset = await apiUtils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
]);
});
});
describe('PATCH /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
.patch(`/album/${uuidStub.notFound}`)
const { status, body } = await request(app)
.patch(`/album/${uuidDto.notFound}`)
.send({ albumName: 'New album name' });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should update an album', async () => {
const album = await api.albumApi.create(server, user1.accessToken, { albumName: 'New album' });
const { status, body } = await request(server)
const album = await apiUtils.createAlbum(user1.accessToken, {
albumName: 'New album',
});
const { status, body } = await request(app)
.patch(`/album/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({
@@ -345,52 +426,68 @@ describe(`${AlbumController.name} (e2e)`, () => {
describe('DELETE /album/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.send({ ids: [user1Asset.id] });
.send({ ids: [user1Asset1.id] });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should be able to remove own asset from own album', async () => {
const { status, body } = await request(server)
.delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: true })]);
});
it('should be able to remove own asset from shared album', async () => {
const { status, body } = await request(server)
.delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: true })]);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not be able to remove foreign asset from own album', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset.id] });
.send({ ids: [user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: false, error: 'no_permission' })]);
expect(body).toEqual([
expect.objectContaining({
id: user1Asset1.id,
success: false,
error: 'no_permission',
}),
]);
});
it('should not be able to remove foreign asset from foreign album', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset.id] });
.send({ ids: [user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: false, error: 'no_permission' })]);
expect(body).toEqual([
expect.objectContaining({
id: user1Asset1.id,
success: false,
error: 'no_permission',
}),
]);
});
it('should be able to remove own asset from own album', async () => {
const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
]);
});
it('should be able to remove own asset from shared album', async () => {
const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
]);
});
});
@@ -398,51 +495,57 @@ describe(`${AlbumController.name} (e2e)`, () => {
let album: AlbumResponseDto;
beforeEach(async () => {
album = await api.albumApi.create(server, user1.accessToken, { albumName: 'testAlbum' });
album = await apiUtils.createAlbum(user1.accessToken, {
albumName: 'testAlbum',
});
});
it('should require authentication', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/users`)
.send({ sharedUserIds: [] });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should be able to add user to own album', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ sharedUserIds: [user2.userId] });
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ sharedUsers: [expect.objectContaining({ id: user2.userId })] }));
expect(body).toEqual(
expect.objectContaining({
sharedUsers: [expect.objectContaining({ id: user2.userId })],
})
);
});
it('should not be able to share album with owner', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ sharedUserIds: [user1.userId] });
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest('Cannot be shared with owner'));
expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner'));
});
it('should not be able to add existing user to shared album', async () => {
await request(server)
await request(app)
.put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ sharedUserIds: [user2.userId] });
const { status, body } = await request(server)
const { status, body } = await request(app)
.put(`/album/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ sharedUserIds: [user2.userId] });
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest('User already added'));
expect(body).toEqual(errorDto.badRequest('User already added'));
});
});
});
+178
View File
@@ -0,0 +1,178 @@
import { LoginResponseDto, PersonResponseDto } from '@immich/sdk';
import { uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, dbUtils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activity', () => {
let admin: LoginResponseDto;
let visiblePerson: PersonResponseDto;
let hiddenPerson: PersonResponseDto;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup();
});
beforeEach(async () => {
await dbUtils.reset(['person']);
[visiblePerson, hiddenPerson] = await Promise.all([
apiUtils.createPerson(admin.accessToken, {
name: 'visible_person',
}),
apiUtils.createPerson(admin.accessToken, {
name: 'hidden_person',
isHidden: true,
}),
]);
const asset = await apiUtils.createAsset(admin.accessToken);
await Promise.all([
dbUtils.createFace({ assetId: asset.id, personId: visiblePerson.id }),
dbUtils.createFace({ assetId: asset.id, personId: hiddenPerson.id }),
]);
});
describe('GET /person', () => {
beforeEach(async () => {});
it('should require authentication', async () => {
const { status, body } = await request(app).get('/person');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return all people (including hidden)', async () => {
const { status, body } = await request(app)
.get('/person')
.set('Authorization', `Bearer ${admin.accessToken}`)
.query({ withHidden: true });
expect(status).toBe(200);
expect(body).toEqual({
total: 2,
hidden: 1,
people: [
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ name: 'hidden_person' }),
],
});
});
it('should return only visible people', async () => {
const { status, body } = await request(app)
.get('/person')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
total: 2,
hidden: 1,
people: [expect.objectContaining({ name: 'visible_person' })],
});
});
});
describe('GET /person/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/person/${uuidDto.notFound}`
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app)
.get(`/person/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should return person information', async () => {
const { status, body } = await request(app)
.get(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: visiblePerson.id }));
});
});
describe('PUT /person/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(
`/person/${uuidDto.notFound}`
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const { key, type } of [
{ key: 'name', type: 'string' },
{ key: 'featureFaceAssetId', type: 'string' },
{ key: 'isHidden', type: 'boolean value' },
]) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest([`${key} must be a ${type}`]));
});
}
it('should not accept invalid birth dates', async () => {
for (const { birthDate, response } of [
{ birthDate: false, response: 'Not found or no person.write access' },
{ birthDate: 'false', response: ['birthDate must be a Date instance'] },
{
birthDate: '123567',
response: 'Not found or no person.write access',
},
{ birthDate: 123567, response: 'Not found or no person.write access' },
]) {
const { status, body } = await request(app)
.put(`/person/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(response));
}
});
it('should update a date of birth', async () => {
const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: '1990-01-01T05:00:00.000Z' });
expect(status).toBe(200);
expect(body).toMatchObject({ birthDate: '1990-01-01' });
});
it('should clear a date of birth', async () => {
// TODO ironically this uses the update endpoint to create the person
const person = await apiUtils.createPerson(admin.accessToken, {
birthDate: new Date('1990-01-01').toISOString(),
});
expect(person.birthDate).toBeDefined();
const { status, body } = await request(app)
.put(`/person/${person.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: null });
expect(status).toBe(200);
expect(body).toMatchObject({ birthDate: null });
});
});
});
@@ -1,21 +1,21 @@
import {
AlbumResponseDto,
AssetResponseDto,
IAssetRepository,
LoginResponseDto,
SharedLinkCreateDto,
SharedLinkResponseDto,
} from '@app/domain';
import { SharedLinkController } from '@app/immich';
import { SharedLinkType } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { errorStub, userDto, uuidStub } from '@test/fixtures';
import { DateTime } from 'luxon';
SharedLinkType,
createSharedLink as create,
createAlbum,
deleteUser,
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import request from 'supertest';
import { api } from '../../client';
import { testApp } from '../utils';
import { beforeAll, describe, expect, it } from 'vitest';
describe(`${SharedLinkController.name} (e2e)`, () => {
let server: any;
describe('/shared-link', () => {
let admin: LoginResponseDto;
let asset1: AssetResponseDto;
let asset2: AssetResponseDto;
@@ -30,97 +30,96 @@ describe(`${SharedLinkController.name} (e2e)`, () => {
let linkWithAssets: SharedLinkResponseDto;
let linkWithMetadata: SharedLinkResponseDto;
let linkWithoutMetadata: SharedLinkResponseDto;
let app: INestApplication<any>;
beforeAll(async () => {
app = await testApp.create();
server = app.getHttpServer();
const assetRepository = app.get<IAssetRepository>(IAssetRepository);
apiUtils.setup();
await dbUtils.reset();
await testApp.reset();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);
await Promise.all([
api.userApi.create(server, admin.accessToken, userDto.user1),
api.userApi.create(server, admin.accessToken, userDto.user2),
]);
admin = await apiUtils.adminSetup();
[user1, user2] = await Promise.all([
api.authApi.login(server, userDto.user1),
api.authApi.login(server, userDto.user2),
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
]);
[asset1, asset2] = await Promise.all([
api.assetApi.create(server, user1.accessToken),
api.assetApi.create(server, user1.accessToken),
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
]);
await assetRepository.upsertExif({
assetId: asset1.id,
longitude: -108.400968333333,
latitude: 39.115,
orientation: '1',
dateTimeOriginal: DateTime.fromISO('2022-01-10T19:15:44.310Z').toJSDate(),
timeZone: 'UTC-4',
state: 'Mesa County, Colorado',
country: 'United States of America',
});
[album, deletedAlbum, metadataAlbum] = await Promise.all([
api.albumApi.create(server, user1.accessToken, { albumName: 'album' }),
api.albumApi.create(server, user2.accessToken, { albumName: 'deleted album' }),
api.albumApi.create(server, user1.accessToken, { albumName: 'metadata album', assetIds: [asset1.id] }),
createAlbum(
{ createAlbumDto: { albumName: 'album' } },
{ headers: asBearerAuth(user1.accessToken) }
),
createAlbum(
{ createAlbumDto: { albumName: 'deleted album' } },
{ headers: asBearerAuth(user2.accessToken) }
),
createAlbum(
{
createAlbumDto: {
albumName: 'metadata album',
assetIds: [asset1.id],
},
},
{ headers: asBearerAuth(user1.accessToken) }
),
]);
[linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] =
await Promise.all([
api.sharedLinkApi.create(server, user2.accessToken, {
type: SharedLinkType.ALBUM,
albumId: deletedAlbum.id,
}),
api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.ALBUM,
albumId: album.id,
}),
api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.INDIVIDUAL,
assetIds: [asset1.id],
}),
api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.ALBUM,
albumId: album.id,
password: 'foo',
}),
api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.ALBUM,
albumId: metadataAlbum.id,
showMetadata: true,
}),
api.sharedLinkApi.create(server, user1.accessToken, {
type: SharedLinkType.ALBUM,
albumId: metadataAlbum.id,
showMetadata: false,
}),
]);
[
linkWithDeletedAlbum,
linkWithAlbum,
linkWithAssets,
linkWithPassword,
linkWithMetadata,
linkWithoutMetadata,
] = await Promise.all([
apiUtils.createSharedLink(user2.accessToken, {
type: SharedLinkType.Album,
albumId: deletedAlbum.id,
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset1.id],
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
password: 'foo',
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: metadataAlbum.id,
showMetadata: true,
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: metadataAlbum.id,
showMetadata: false,
}),
]);
await api.userApi.delete(server, admin.accessToken, user2.userId);
});
afterAll(async () => {
await testApp.teardown();
await deleteUser(
{ id: user2.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
});
describe('GET /shared-link', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/shared-link');
const { status, body } = await request(app).get('/shared-link');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get all shared links created by user', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`);
@@ -133,12 +132,12 @@ describe(`${SharedLinkController.name} (e2e)`, () => {
expect.objectContaining({ id: linkWithPassword.id }),
expect.objectContaining({ id: linkWithMetadata.id }),
expect.objectContaining({ id: linkWithoutMetadata.id }),
]),
])
);
});
it('should not get shared links created by other users', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/shared-link')
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -149,7 +148,7 @@ describe(`${SharedLinkController.name} (e2e)`, () => {
describe('GET /shared-link/me', () => {
it('should not require admin authentication', async () => {
const { status } = await request(server)
const { status } = await request(app)
.get('/shared-link/me')
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -157,52 +156,66 @@ describe(`${SharedLinkController.name} (e2e)`, () => {
});
it('should get data for correct shared link', async () => {
const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithAlbum.key });
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithAlbum.key });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
album,
userId: user1.userId,
type: SharedLinkType.ALBUM,
}),
type: SharedLinkType.Album,
})
);
});
it('should return unauthorized for incorrect shared link', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithAlbum.key + 'foo' });
expect(status).toBe(401);
expect(body).toEqual(errorStub.invalidShareKey);
expect(body).toEqual(errorDto.invalidShareKey);
});
it('should return unauthorized if target has been soft deleted', async () => {
const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key });
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithDeletedAlbum.key });
expect(status).toBe(401);
expect(body).toEqual(errorStub.invalidShareKey);
expect(body).toEqual(errorDto.invalidShareKey);
});
it('should return unauthorized for password protected link', async () => {
const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithPassword.key });
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithPassword.key });
expect(status).toBe(401);
expect(body).toEqual(errorStub.invalidSharePassword);
expect(body).toEqual(errorDto.invalidSharePassword);
});
it('should get data for correct password protected link', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithPassword.key, password: 'foo' });
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM }));
expect(body).toEqual(
expect.objectContaining({
album,
userId: user1.userId,
type: SharedLinkType.Album,
})
);
});
it('should return metadata for album shared link', async () => {
const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithMetadata.key });
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithMetadata.key });
expect(status).toBe(200);
expect(body.assets).toHaveLength(1);
@@ -211,22 +224,16 @@ describe(`${SharedLinkController.name} (e2e)`, () => {
originalFileName: 'example',
localDateTime: expect.any(String),
fileCreatedAt: expect.any(String),
exifInfo: expect.objectContaining({
longitude: -108.400968333333,
latitude: 39.115,
orientation: '1',
dateTimeOriginal: expect.any(String),
timeZone: 'UTC-4',
state: 'Mesa County, Colorado',
country: 'United States of America',
}),
}),
exifInfo: expect.any(Object),
})
);
expect(body.album).toBeDefined();
});
it('should not return metadata for album shared link without metadata', async () => {
const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithoutMetadata.key });
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithoutMetadata.key });
expect(status).toBe(200);
expect(body.assets).toHaveLength(1);
@@ -242,127 +249,150 @@ describe(`${SharedLinkController.name} (e2e)`, () => {
describe('GET /shared-link/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get(`/shared-link/${linkWithAlbum.id}`);
const { status, body } = await request(app).get(
`/shared-link/${linkWithAlbum.id}`
);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get shared link by id', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get(`/shared-link/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM }));
expect(body).toEqual(
expect.objectContaining({
album,
userId: user1.userId,
type: SharedLinkType.Album,
})
);
});
it('should not get shared link by id if user has not created the link or it does not exist', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.get(`/shared-link/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' }));
expect(body).toEqual(
expect.objectContaining({ message: 'Shared link not found' })
);
});
});
describe('POST /shared-link', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/shared-link')
.send({ type: SharedLinkType.ALBUM, albumId: uuidStub.notFound });
.send({ type: SharedLinkType.Album, albumId: uuidDto.notFound });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a type and the correspondent asset/album id', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest());
expect(body).toEqual(errorDto.badRequest());
});
it('should require an asset/album id', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.ALBUM });
.send({ type: SharedLinkType.Album });
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' }));
expect(body).toEqual(
expect.objectContaining({ message: 'Invalid albumId' })
);
});
it('should require a valid asset id', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.INDIVIDUAL, assetId: uuidStub.notFound });
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
expect(status).toBe(400);
expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' }));
expect(body).toEqual(
expect.objectContaining({ message: 'Invalid assetIds' })
);
});
it('should create a shared link', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.post('/shared-link')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.ALBUM, albumId: album.id });
.send({ type: SharedLinkType.Album, albumId: album.id });
expect(status).toBe(201);
expect(body).toEqual(expect.objectContaining({ type: SharedLinkType.ALBUM, userId: user1.userId }));
expect(body).toEqual(
expect.objectContaining({
type: SharedLinkType.Album,
userId: user1.userId,
})
);
});
});
describe('PATCH /shared-link/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.patch(`/shared-link/${linkWithAlbum.id}`)
.send({ description: 'foo' });
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should fail if invalid link', async () => {
const { status, body } = await request(server)
.patch(`/shared-link/${uuidStub.notFound}`)
const { status, body } = await request(app)
.patch(`/shared-link/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'foo' });
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest());
expect(body).toEqual(errorDto.badRequest());
});
it('should update shared link', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.patch(`/shared-link/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'foo' });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({ type: SharedLinkType.ALBUM, userId: user1.userId, description: 'foo' }),
expect.objectContaining({
type: SharedLinkType.Album,
userId: user1.userId,
description: 'foo',
})
);
});
});
describe('PUT /shared-link/:id/assets', () => {
it('should not add assets to shared link (album)', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.put(`/shared-link/${linkWithAlbum.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] });
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest('Invalid shared link type'));
expect(body).toEqual(errorDto.badRequest('Invalid shared link type'));
});
it('should add an assets to a shared link (individual)', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.put(`/shared-link/${linkWithAssets.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] });
@@ -374,17 +404,17 @@ describe(`${SharedLinkController.name} (e2e)`, () => {
describe('DELETE /shared-link/:id/assets', () => {
it('should not remove assets from a shared link (album)', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.delete(`/shared-link/${linkWithAlbum.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] });
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest('Invalid shared link type'));
expect(body).toEqual(errorDto.badRequest('Invalid shared link type'));
});
it('should remove assets from a shared link (individual)', async () => {
const { status, body } = await request(server)
const { status, body } = await request(app)
.delete(`/shared-link/${linkWithAssets.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] });
@@ -396,23 +426,25 @@ describe(`${SharedLinkController.name} (e2e)`, () => {
describe('DELETE /shared-link/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).delete(`/shared-link/${linkWithAlbum.id}`);
const { status, body } = await request(app).delete(
`/shared-link/${linkWithAlbum.id}`
);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
expect(body).toEqual(errorDto.unauthorized);
});
it('should fail if invalid link', async () => {
const { status, body } = await request(server)
.delete(`/shared-link/${uuidStub.notFound}`)
const { status, body } = await request(app)
.delete(`/shared-link/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest());
expect(body).toEqual(errorDto.badRequest());
});
it('should delete a shared link', async () => {
const { status } = await request(server)
const { status } = await request(app)
.delete(`/shared-link/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
+51 -66
View File
@@ -1,26 +1,31 @@
import {
LoginResponseDto,
UserResponseDto,
createUser,
deleteUser,
getUserById,
} from '@immich/sdk';
import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
import { createUserDto, userDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/server-info', () => {
let admin: LoginResponseDto;
let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto;
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset();
admin = await apiUtils.adminSetup({ onboarding: false });
[deletedUser, nonAdmin, userToDelete] = await Promise.all([
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
]);
await deleteUser(
{ id: deletedUser.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
});
describe('GET /user', () => {
@@ -30,60 +35,54 @@ describe('/server-info', () => {
expect(body).toEqual(errorDto.unauthorized);
});
it('should start with the admin', async () => {
it('should get users', async () => {
const { status, body } = await request(app)
.get('/user')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
expect(body).toHaveLength(1);
expect(body[0]).toMatchObject({ email: 'admin@immich.cloud' });
expect(body).toHaveLength(4);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
])
);
});
it('should hide deleted users', async () => {
const user1 = await apiUtils.userSetup(
admin.accessToken,
createUserDto.user1
);
await deleteUser(
{ id: user1.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const { status, body } = await request(app)
.get(`/user`)
.query({ isAll: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body[0]).toMatchObject({ email: 'admin@immich.cloud' });
expect(body).toHaveLength(3);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
])
);
});
it('should include deleted users', async () => {
const user1 = await apiUtils.userSetup(
admin.accessToken,
createUserDto.user1
);
await deleteUser(
{ id: user1.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const { status, body } = await request(app)
.get(`/user`)
.query({ isAll: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body[0]).toMatchObject({
id: user1.userId,
email: 'user1@immich.cloud',
deletedAt: expect.any(String),
});
expect(body[1]).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
});
expect(body).toHaveLength(4);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
])
);
});
});
@@ -149,13 +148,13 @@ describe('/server-info', () => {
.post(`/user`)
.send({
isAdmin: true,
email: 'user1@immich.cloud',
password: 'Password123',
email: 'user4@immich.cloud',
password: 'password123',
name: 'Immich',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'user1@immich.cloud',
email: 'user4@immich.cloud',
isAdmin: false,
shouldChangePassword: true,
});
@@ -181,18 +180,9 @@ describe('/server-info', () => {
});
describe('DELETE /user/:id', () => {
let userToDelete: UserResponseDto;
beforeEach(async () => {
userToDelete = await createUser(
{ createUserDto: createUserDto.user1 },
{ headers: asBearerAuth(admin.accessToken) }
);
});
it('should require authentication', async () => {
const { status, body } = await request(app).delete(
`/user/${userToDelete.id}`
`/user/${userToDelete.userId}`
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -200,12 +190,12 @@ describe('/server-info', () => {
it('should delete user', async () => {
const { status, body } = await request(app)
.delete(`/user/${userToDelete.id}`)
.delete(`/user/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...userToDelete,
expect(body).toMatchObject({
id: userToDelete.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
@@ -231,14 +221,9 @@ describe('/server-info', () => {
}
it('should not allow a non-admin to become an admin', async () => {
const user = await apiUtils.userSetup(
admin.accessToken,
createUserDto.user1
);
const { status, body } = await request(app)
.put(`/user`)
.send({ isAdmin: true, id: user.userId })
.send({ isAdmin: true, id: nonAdmin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
+64 -4
View File
@@ -1,14 +1,20 @@
import {
AssetResponseDto,
CreateAlbumDto,
CreateAssetDto,
CreateUserDto,
LoginResponseDto,
PersonUpdateDto,
SharedLinkCreateDto,
createAlbum,
createApiKey,
createPerson,
createSharedLink,
createUser,
defaults,
login,
setAdminOnboarding,
signUpAdmin,
updatePerson,
} from '@immich/sdk';
import { BrowserContext } from '@playwright/test';
import { spawn } from 'child_process';
@@ -45,7 +51,36 @@ export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
let client: pg.Client | null = null;
export const dbUtils = {
reset: async () => {
createFace: async ({
assetId,
personId,
}: {
assetId: string;
personId: string;
}) => {
if (!client) {
return;
}
const vector = Array.from({ length: 512 }, Math.random);
const embedding = `[${vector.join(',')}]`;
await client.query(
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
[assetId, personId, embedding]
);
},
setPersonThumbnail: async (personId: string) => {
if (!client) {
return;
}
await client.query(
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
[personId]
);
},
reset: async (tables?: string[]) => {
try {
if (!client) {
client = new pg.Client(
@@ -54,14 +89,20 @@ export const dbUtils = {
await client.connect();
}
for (const table of [
tables = tables || [
'shared_links',
'person',
'albums',
'assets',
'asset_faces',
'activity',
'api_keys',
'user_token',
'users',
'system_metadata',
]) {
];
for (const table of tables) {
await client.query(`DELETE FROM ${table} CASCADE;`);
}
} catch (error) {
@@ -144,6 +185,11 @@ export const apiUtils = {
{ headers: asBearerAuth(accessToken) }
);
},
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
createAlbum(
{ createAlbumDto: dto },
{ headers: asBearerAuth(accessToken) }
),
createAsset: async (
accessToken: string,
dto?: Omit<CreateAssetDto, 'assetData'>
@@ -165,6 +211,20 @@ export const apiUtils = {
return body as AssetResponseDto;
},
createPerson: async (accessToken: string, dto: PersonUpdateDto) => {
// TODO fix createPerson to accept a body
const { id } = await createPerson({ headers: asBearerAuth(accessToken) });
await dbUtils.setPersonThumbnail(id);
return updatePerson(
{ id, personUpdateDto: dto },
{ headers: asBearerAuth(accessToken) }
);
},
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
createSharedLink(
{ sharedLinkCreateDto: dto },
{ headers: asBearerAuth(accessToken) }
),
};
export const cliUtils = {
+188 -188
View File
@@ -64,33 +64,33 @@ trio = ["trio (>=0.23)"]
[[package]]
name = "black"
version = "24.1.1"
version = "24.2.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"},
{file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"},
{file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"},
{file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"},
{file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"},
{file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"},
{file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"},
{file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"},
{file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"},
{file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"},
{file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"},
{file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"},
{file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"},
{file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"},
{file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"},
{file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"},
{file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"},
{file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"},
{file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"},
{file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"},
{file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"},
{file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"},
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
]
[package.dependencies]
@@ -2101,61 +2101,61 @@ numpy = [
[[package]]
name = "orjson"
version = "3.9.13"
version = "3.9.14"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
{file = "orjson-3.9.13-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fa6b67f8bef277c2a4aadd548d58796854e7d760964126c3209b19bccc6a74f1"},
{file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b812417199eeb169c25f67815cfb66fd8de7ff098bf57d065e8c1943a7ba5c8f"},
{file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ccd5bd222e5041069ad9d9868ab59e6dbc53ecde8d8c82b919954fbba43b46b"},
{file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaaf80957c38e9d3f796f355a80fad945e72cd745e6b64c210e635b7043b673e"},
{file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60da7316131185d0110a1848e9ad15311e6c8938ee0b5be8cbd7261e1d80ee8f"},
{file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b98cd948372f0eb219bc309dee4633db1278687161e3280d9e693b6076951d2"},
{file = "orjson-3.9.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3869d65561f10071d3e7f35ae58fd377056f67d7aaed5222f318390c3ad30339"},
{file = "orjson-3.9.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43fd6036b16bb6742d03dae62f7bdf8214d06dea47e4353cde7e2bd1358d186f"},
{file = "orjson-3.9.13-cp310-none-win32.whl", hash = "sha256:0d3ba9d88e20765335260d7b25547d7c571eee2b698200f97afa7d8c7cd668fc"},
{file = "orjson-3.9.13-cp310-none-win_amd64.whl", hash = "sha256:6e47153db080f5e87e8ba638f1a8b18995eede6b0abb93964d58cf11bcea362f"},
{file = "orjson-3.9.13-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4584e8eb727bc431baaf1bf97e35a1d8a0109c924ec847395673dfd5f4ef6d6f"},
{file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f37f0cdd026ef777a4336e599d8194c8357fc14760c2a5ddcfdf1965d45504b"},
{file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d714595d81efab11b42bccd119977d94b25d12d3a806851ff6bfd286a4bce960"},
{file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9171e8e1a1f221953e38e84ae0abffe8759002fd8968106ee379febbb5358b33"},
{file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ab9dbdec3f13f3ea6f937564ce21651844cfbf2725099f2f490426acf683c23"},
{file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811ac076855e33e931549340288e0761873baf29276ad00f221709933c644330"},
{file = "orjson-3.9.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:860d0f5b42d0c0afd73fa4177709f6e1b966ba691fcd72175affa902052a81d6"},
{file = "orjson-3.9.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:838b898e8c1f26eb6b8d81b180981273f6f5110c76c22c384979aca854194f1b"},
{file = "orjson-3.9.13-cp311-none-win32.whl", hash = "sha256:d3222db9df629ef3c3673124f2e05fb72bc4a320c117e953fec0d69dde82e36d"},
{file = "orjson-3.9.13-cp311-none-win_amd64.whl", hash = "sha256:978117122ca4cc59b28af5322253017f6c5fc03dbdda78c7f4b94ae984c8dd43"},
{file = "orjson-3.9.13-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:031df1026c7ea8303332d78711f180231e3ae8b564271fb748a03926587c5546"},
{file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fd9a2101d04e85086ea6198786a3f016e45475f800712e6833e14bf9ce2832f"},
{file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:446d9ad04204e79229ae19502daeea56479e55cbc32634655d886f5a39e91b44"},
{file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b57c0954a9fdd2b05b9cec0f5a12a0bdce5bf021a5b3b09323041613972481ab"},
{file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:266e55c83f81248f63cc93d11c5e3a53df49a5d2598fa9e9db5f99837a802d5d"},
{file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31372ba3a9fe8ad118e7d22fba46bbc18e89039e3bfa89db7bc8c18ee722dca8"},
{file = "orjson-3.9.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3b0c4da61f39899561e08e571f54472a09fa71717d9797928af558175ae5243"},
{file = "orjson-3.9.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cc03a35bfc71c8ebf96ce49b82c2a7be6af4b3cd3ac34166fdb42ac510bbfff"},
{file = "orjson-3.9.13-cp312-none-win_amd64.whl", hash = "sha256:49b7e3fe861cb246361825d1a238f2584ed8ea21e714bf6bb17cebb86772e61c"},
{file = "orjson-3.9.13-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:62e9a99879c4d5a04926ac2518a992134bfa00d546ea5a4cae4b9be454d35a22"},
{file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d92a3e835a5100f1d5b566fff79217eab92223ca31900dba733902a182a35ab0"},
{file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23f21faf072ed3b60b5954686f98157e073f6a8068eaa58dbde83e87212eda84"},
{file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:828c502bb261588f7de897e06cb23c4b122997cb039d2014cb78e7dabe92ef0c"},
{file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16946d095212a3dec552572c5d9bca7afa40f3116ad49695a397be07d529f1fa"},
{file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3deadd8dc0e9ff844b5b656fa30a48dbee1c3b332d8278302dd9637f6b09f627"},
{file = "orjson-3.9.13-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9b1b5adc5adf596c59dca57156b71ad301d73956f5bab4039b0e34dbf50b9fa0"},
{file = "orjson-3.9.13-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ddc089315d030c54f0f03fb38286e2667c05009a78d659f108a8efcfbdf2e585"},
{file = "orjson-3.9.13-cp38-none-win32.whl", hash = "sha256:ae77275a28667d9c82d4522b681504642055efa0368d73108511647c6499b31c"},
{file = "orjson-3.9.13-cp38-none-win_amd64.whl", hash = "sha256:730385fdb99a21fce9bb84bb7fcbda72c88626facd74956bda712834b480729d"},
{file = "orjson-3.9.13-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7e8e4a571d958910272af8d53a9cbe6599f9f5fd496a1bc51211183bb2072cbd"},
{file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfad553a36548262e7da0f3a7464270e13900b898800fb571a5d4b298c3f8356"},
{file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d691c44604941945b00e0a13b19a7d9c1a19511abadf0080f373e98fdeb6b31"},
{file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8c83718346de08d68b3cb1105c5d91e5fc39885d8610fdda16613d4e3941459"},
{file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ef57a53bfc2091a7cd50a640d9ae866bd7d92a5225a1bab6baa60ef62583f2"},
{file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9156b96afa38db71344522f5517077eaedf62fcd2c9148392ff93d801128809c"},
{file = "orjson-3.9.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31fb66b41fb2c4c817d9610f0bc7d31345728d7b5295ac78b63603407432a2b2"},
{file = "orjson-3.9.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8a730bf07feacb0863974e67b206b7c503a62199de1cece2eb0d4c233ec29c11"},
{file = "orjson-3.9.13-cp39-none-win32.whl", hash = "sha256:5ef58869f3399acbbe013518d8b374ee9558659eef14bca0984f67cb1fbd3c37"},
{file = "orjson-3.9.13-cp39-none-win_amd64.whl", hash = "sha256:9bcf56efdb83244cde070e82a69c0f03c47c235f0a5cb6c81d9da23af7fbaae4"},
{file = "orjson-3.9.13.tar.gz", hash = "sha256:fc6bc65b0cf524ee042e0bc2912b9206ef242edfba7426cf95763e4af01f527a"},
{file = "orjson-3.9.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:793f6c9448ab6eb7d4974b4dde3f230345c08ca6c7995330fbceeb43a5c8aa5e"},
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bc7928d161840096adc956703494b5c0193ede887346f028216cac0af87500"},
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b36f54da759602d8e2f7dad958752d453dfe2c7122767bc7f765e17dc59959"},
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abcda41ecdc950399c05eff761c3de91485d9a70d8227cb599ad3a66afe93bcc"},
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df76ecd17b1b3627bddfd689faaf206380a1a38cc9f6c4075bd884eaedcf46c2"},
{file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d450a8e0656efb5d0fcb062157b918ab02dcca73278975b4ee9ea49e2fcf5bd5"},
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95c03137b0cf66517c8baa65770507a756d3a89489d8ecf864ea92348e1beabe"},
{file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20837e10835c98973673406d6798e10f821e7744520633811a5a3d809762d8cc"},
{file = "orjson-3.9.14-cp310-none-win32.whl", hash = "sha256:1f7b6f3ef10ae8e3558abb729873d033dbb5843507c66b1c0767e32502ba96bb"},
{file = "orjson-3.9.14-cp310-none-win_amd64.whl", hash = "sha256:ea890e6dc1711aeec0a33b8520e395c2f3d59ead5b4351a788e06bf95fc7ba81"},
{file = "orjson-3.9.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c19009ff37f033c70acd04b636380379499dac2cba27ae7dfc24f304deabbc81"},
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19cdea0664aec0b7f385be84986d4defd3334e9c3c799407686ee1c26f7b8251"},
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135d518f73787ce323b1a5e21fb854fe22258d7a8ae562b81a49d6c7f826f2a3"},
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2cf1d0557c61c75e18cf7d69fb689b77896e95553e212c0cc64cf2087944b84"},
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7c11667421df2d8b18b021223505dcc3ee51be518d54e4dc49161ac88ac2b87"},
{file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eefc41ba42e75ed88bc396d8fe997beb20477f3e7efa000cd7a47eda452fbb2"},
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:917311d6a64d1c327c0dfda1e41f3966a7fb72b11ca7aa2e7a68fcccc7db35d9"},
{file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4dc1c132259b38d12c6587d190cd09cd76e3b5273ce71fe1372437b4cbc65f6f"},
{file = "orjson-3.9.14-cp311-none-win32.whl", hash = "sha256:6f39a10408478f4c05736a74da63727a1ae0e83e3533d07b19443400fe8591ca"},
{file = "orjson-3.9.14-cp311-none-win_amd64.whl", hash = "sha256:26280a7fcb62d8257f634c16acebc3bec626454f9ab13558bbf7883b9140760e"},
{file = "orjson-3.9.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:08e722a8d06b13b67a51f247a24938d1a94b4b3862e40e0eef3b2e98c99cd04c"},
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2591faa0c031cf3f57e5bce1461cfbd6160f3f66b5a72609a130924917cb07d"},
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2450d87dd7b4f277f4c5598faa8b49a0c197b91186c47a2c0b88e15531e4e3e"},
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90903d2908158a2c9077a06f11e27545de610af690fb178fd3ba6b32492d4d1c"},
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6f095eef0026eae76fc212f20f786011ecf482fc7df2f4c272a8ae6dd7b1ef"},
{file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751250a31fef2bac05a2da2449aae7142075ea26139271f169af60456d8ad27a"},
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a1af21160a38ee8be3f4fcf24ee4b99e6184cadc7f915d599f073f478a94d2c"},
{file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:449bf090b2aa4e019371d7511a6ea8a5a248139205c27d1834bb4b1e3c44d936"},
{file = "orjson-3.9.14-cp312-none-win_amd64.whl", hash = "sha256:a603161318ff699784943e71f53899983b7dee571b4dd07c336437c9c5a272b0"},
{file = "orjson-3.9.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:814f288c011efdf8f115c5ebcc1ab94b11da64b207722917e0ceb42f52ef30a3"},
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88cafb100af68af3b9b29b5ccd09fdf7a48c63327916c8c923a94c336d38dd3"},
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba3518b999f88882ade6686f1b71e207b52e23546e180499be5bbb63a2f9c6e6"},
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978f416bbff9da8d2091e3cf011c92da68b13f2c453dcc2e8109099b2a19d234"},
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fc593cf836f631153d0e21beaeb8d26e144445c73645889335c2247fcd71a0"},
{file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d1528db3c7554f9d6eeb09df23cb80dd5177ec56eeb55cc5318826928de506"},
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7183cc68ee2113b19b0b8714221e5e3b07b3ba10ca2bb108d78fd49cefaae101"},
{file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df3266d54246cb56b8bb17fa908660d2a0f2e3f63fbc32451ffc1b1505051d07"},
{file = "orjson-3.9.14-cp38-none-win32.whl", hash = "sha256:7913079b029e1b3501854c9a78ad938ed40d61fe09bebab3c93e60ff1301b189"},
{file = "orjson-3.9.14-cp38-none-win_amd64.whl", hash = "sha256:29512eb925b620e5da2fd7585814485c67cc6ba4fe739a0a700c50467a8a8065"},
{file = "orjson-3.9.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5bf597530544db27a8d76aced49cfc817ee9503e0a4ebf0109cd70331e7bbe0c"},
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac650d49366fa41fe702e054cb560171a8634e2865537e91f09a8d05ea5b1d37"},
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:236230433a9a4968ab895140514c308fdf9f607cb8bee178a04372b771123860"},
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3014ccbda9be0b1b5f8ea895121df7e6524496b3908f4397ff02e923bcd8f6dd"},
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac0c7eae7ad3a223bde690565442f8a3d620056bd01196f191af8be58a5248e1"},
{file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca33fdd0b38839b01912c57546d4f412ba7bfa0faf9bf7453432219aec2df07"},
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f75823cc1674a840a151e999a7dfa0d86c911150dd6f951d0736ee9d383bf415"},
{file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f52ac2eb49e99e7373f62e2a68428c6946cda52ce89aa8fe9f890c7278e2d3a"},
{file = "orjson-3.9.14-cp39-none-win32.whl", hash = "sha256:0572f174f50b673b7df78680fb52cd0087a8585a6d06d295a5f790568e1064c6"},
{file = "orjson-3.9.14-cp39-none-win_amd64.whl", hash = "sha256:ab90c02cb264250b8a58cedcc72ed78a4a257d956c8d3c8bebe9751b818dfad8"},
{file = "orjson-3.9.14.tar.gz", hash = "sha256:06fb40f8e49088ecaa02f1162581d39e2cf3fd9dbbfe411eb2284147c99bad79"},
]
[[package]]
@@ -3096,121 +3096,121 @@ all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib"
[[package]]
name = "tokenizers"
version = "0.15.1"
version = "0.15.2"
description = ""
optional = false
python-versions = ">=3.7"
files = [
{file = "tokenizers-0.15.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:32c9491dd1bcb33172c26b454dbd607276af959b9e78fa766e2694cafab3103c"},
{file = "tokenizers-0.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29a1b784b870a097e7768f8c20c2dd851e2c75dad3efdae69a79d3e7f1d614d5"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0049fbe648af04148b08cb211994ce8365ee628ce49724b56aaefd09a3007a78"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e84b3c235219e75e24de6b71e6073cd2c8d740b14d88e4c6d131b90134e3a338"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8cc575769ea11d074308c6d71cb10b036cdaec941562c07fc7431d956c502f0e"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bf28f299c4158e6d0b5eaebddfd500c4973d947ffeaca8bcbe2e8c137dff0b"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:506555f98361db9c74e1323a862d77dcd7d64c2058829a368bf4159d986e339f"},
{file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7061b0a28ade15906f5b2ec8c48d3bdd6e24eca6b427979af34954fbe31d5cef"},
{file = "tokenizers-0.15.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ed5e35507b7a0e2aac3285c4f5e37d4ec5cfc0e5825b862b68a0aaf2757af52"},
{file = "tokenizers-0.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9df9247df0de6509dd751b1c086e5f124b220133b5c883bb691cb6fb3d786f"},
{file = "tokenizers-0.15.1-cp310-none-win32.whl", hash = "sha256:dd999af1b4848bef1b11d289f04edaf189c269d5e6afa7a95fa1058644c3f021"},
{file = "tokenizers-0.15.1-cp310-none-win_amd64.whl", hash = "sha256:39d06a57f7c06940d602fad98702cf7024c4eee7f6b9fe76b9f2197d5a4cc7e2"},
{file = "tokenizers-0.15.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8ad034eb48bf728af06915e9294871f72fcc5254911eddec81d6df8dba1ce055"},
{file = "tokenizers-0.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea9ede7c42f8fa90f31bfc40376fd91a7d83a4aa6ad38e6076de961d48585b26"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b85d6fe1a20d903877aa0ef32ef6b96e81e0e48b71c206d6046ce16094de6970"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a7d44f656320137c7d643b9c7dcc1814763385de737fb98fd2643880910f597"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd244bd0793cdacf27ee65ec3db88c21f5815460e8872bbeb32b040469d6774e"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3f4a36e371b3cb1123adac8aeeeeab207ad32f15ed686d9d71686a093bb140"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2921a53966afb29444da98d56a6ccbef23feb3b0c0f294b4e502370a0a64f25"},
{file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f49068cf51f49c231067f1a8c9fc075ff960573f6b2a956e8e1b0154fb638ea5"},
{file = "tokenizers-0.15.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0ab1a22f20eaaab832ab3b00a0709ca44a0eb04721e580277579411b622c741c"},
{file = "tokenizers-0.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:671268f24b607c4adc6fa2b5b580fd4211b9f84b16bd7f46d62f8e5be0aa7ba4"},
{file = "tokenizers-0.15.1-cp311-none-win32.whl", hash = "sha256:a4f03e33d2bf7df39c8894032aba599bf90f6f6378e683a19d28871f09bb07fc"},
{file = "tokenizers-0.15.1-cp311-none-win_amd64.whl", hash = "sha256:30f689537bcc7576d8bd4daeeaa2cb8f36446ba2f13f421b173e88f2d8289c4e"},
{file = "tokenizers-0.15.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f3a379dd0898a82ea3125e8f9c481373f73bffce6430d4315f0b6cd5547e409"},
{file = "tokenizers-0.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d870ae58bba347d38ac3fc8b1f662f51e9c95272d776dd89f30035c83ee0a4f"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d6d28e0143ec2e253a8a39e94bf1d24776dbe73804fa748675dbffff4a5cd6d8"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61ae9ac9f44e2da128ee35db69489883b522f7abe033733fa54eb2de30dac23d"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8e322a47e29128300b3f7749a03c0ec2bce0a3dc8539ebff738d3f59e233542"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:760334f475443bc13907b1a8e1cb0aeaf88aae489062546f9704dce6c498bfe2"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b173753d4aca1e7d0d4cb52b5e3ffecfb0ca014e070e40391b6bb4c1d6af3f2"},
{file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1f13d457c8f0ab17e32e787d03470067fe8a3b4d012e7cc57cb3264529f4a"},
{file = "tokenizers-0.15.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:425b46ceff4505f20191df54b50ac818055d9d55023d58ae32a5d895b6f15bb0"},
{file = "tokenizers-0.15.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:681ac6ba3b4fdaf868ead8971221a061f580961c386e9732ea54d46c7b72f286"},
{file = "tokenizers-0.15.1-cp312-none-win32.whl", hash = "sha256:f2272656063ccfba2044df2115095223960d80525d208e7a32f6c01c351a6f4a"},
{file = "tokenizers-0.15.1-cp312-none-win_amd64.whl", hash = "sha256:9abe103203b1c6a2435d248d5ff4cceebcf46771bfbc4957a98a74da6ed37674"},
{file = "tokenizers-0.15.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2ce9ed5c8ef26b026a66110e3c7b73d93ec2d26a0b1d0ea55ddce61c0e5f446f"},
{file = "tokenizers-0.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:89b24d366137986c3647baac29ef902d2d5445003d11c30df52f1bd304689aeb"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0faebedd01b413ab777ca0ee85914ed8b031ea5762ab0ea60b707ce8b9be6842"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbd9dfcdad4f3b95d801f768e143165165055c18e44ca79a8a26de889cd8e85"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:97194324c12565b07e9993ca9aa813b939541185682e859fb45bb8d7d99b3193"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485e43e2cc159580e0d83fc919ec3a45ae279097f634b1ffe371869ffda5802c"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:191d084d60e3589d6420caeb3f9966168269315f8ec7fbc3883122dc9d99759d"},
{file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01c28cc8d7220634a75b14c53f4fc9d1b485f99a5a29306a999c115921de2897"},
{file = "tokenizers-0.15.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:325212027745d3f8d5d5006bb9e5409d674eb80a184f19873f4f83494e1fdd26"},
{file = "tokenizers-0.15.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3c5573603c36ce12dbe318bcfb490a94cad2d250f34deb2f06cb6937957bbb71"},
{file = "tokenizers-0.15.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:1441161adb6d71a15a630d5c1d8659d5ebe41b6b209586fbeea64738e58fcbb2"},
{file = "tokenizers-0.15.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:382a8d0c31afcfb86571afbfefa37186df90865ce3f5b731842dab4460e53a38"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e76959783e3f4ec73b3f3d24d4eec5aa9225f0bee565c48e77f806ed1e048f12"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:401df223e5eb927c5961a0fc6b171818a2bba01fb36ef18c3e1b69b8cd80e591"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52606c233c759561a16e81b2290a7738c3affac7a0b1f0a16fe58dc22e04c7d"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72c658bbe5a05ed8bc2ac5ad782385bfd743ffa4bc87d9b5026341e709c6f44"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25f5643a2f005c42f0737a326c6c6bdfedfdc9a994b10a1923d9c3e792e4d6a6"},
{file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c5b6f633999d6b42466bbfe21be2e26ad1760b6f106967a591a41d8cbca980e"},
{file = "tokenizers-0.15.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ceb5c9ad11a015150b545c1a11210966a45b8c3d68a942e57cf8938c578a77ca"},
{file = "tokenizers-0.15.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bedd4ce0c4872db193444c395b11c7697260ce86a635ab6d48102d76be07d324"},
{file = "tokenizers-0.15.1-cp37-none-win32.whl", hash = "sha256:cd6caef6c14f5ed6d35f0ddb78eab8ca6306d0cd9870330bccff72ad014a6f42"},
{file = "tokenizers-0.15.1-cp37-none-win_amd64.whl", hash = "sha256:d2bd7af78f58d75a55e5df61efae164ab9200c04b76025f9cc6eeb7aff3219c2"},
{file = "tokenizers-0.15.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:59b3ca6c02e0bd5704caee274978bd055de2dff2e2f39dadf536c21032dfd432"},
{file = "tokenizers-0.15.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:48fe21b67c22583bed71933a025fd66b1f5cfae1baefa423c3d40379b5a6e74e"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3d190254c66a20fb1efbdf035e6333c5e1f1c73b1f7bfad88f9c31908ac2c2c4"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fef90c8f5abf17d48d6635f5fd92ad258acd1d0c2d920935c8bf261782cfe7c8"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fac011ef7da3357aa7eb19efeecf3d201ede9618f37ddedddc5eb809ea0963ca"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:574ec5b3e71d1feda6b0ecac0e0445875729b4899806efbe2b329909ec75cb50"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aca16c3c0637c051a59ea99c4253f16fbb43034fac849076a7e7913b2b9afd2d"},
{file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6f238fc2bbfd3e12e8529980ec1624c7e5b69d4e959edb3d902f36974f725a"},
{file = "tokenizers-0.15.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:587e11a26835b73c31867a728f32ca8a93c9ded4a6cd746516e68b9d51418431"},
{file = "tokenizers-0.15.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6456e7ad397352775e2efdf68a9ec5d6524bbc4543e926eef428d36de627aed4"},
{file = "tokenizers-0.15.1-cp38-none-win32.whl", hash = "sha256:614f0da7dd73293214bd143e6221cafd3f7790d06b799f33a987e29d057ca658"},
{file = "tokenizers-0.15.1-cp38-none-win_amd64.whl", hash = "sha256:a4fa0a20d9f69cc2bf1cfce41aa40588598e77ec1d6f56bf0eb99769969d1ede"},
{file = "tokenizers-0.15.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8d3f18a45e0cf03ce193d5900460dc2430eec4e14c786e5d79bddba7ea19034f"},
{file = "tokenizers-0.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:38dbd6c38f88ad7d5dc5d70c764415d38fe3bcd99dc81638b572d093abc54170"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:777286b1f7e52de92aa4af49fe31046cfd32885d1bbaae918fab3bba52794c33"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58d4d550a3862a47dd249892d03a025e32286eb73cbd6bc887fb8fb64bc97165"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eda68ce0344f35042ae89220b40a0007f721776b727806b5c95497b35714bb7"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cd33d15f7a3a784c3b665cfe807b8de3c6779e060349bd5005bb4ae5bdcb437"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a1aa370f978ac0bfb50374c3a40daa93fd56d47c0c70f0c79607fdac2ccbb42"},
{file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:241482b940340fff26a2708cb9ba383a5bb8a2996d67a0ff2c4367bf4b86cc3a"},
{file = "tokenizers-0.15.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:68f30b05f46a4d9aba88489eadd021904afe90e10a7950e28370d6e71b9db021"},
{file = "tokenizers-0.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5a3c5d8025529670462b881b7b2527aacb6257398c9ec8e170070432c3ae3a82"},
{file = "tokenizers-0.15.1-cp39-none-win32.whl", hash = "sha256:74d1827830f60a9d78da8f6d49a1fbea5422ce0eea42e2617877d23380a7efbc"},
{file = "tokenizers-0.15.1-cp39-none-win_amd64.whl", hash = "sha256:9ff499923e4d6876d6b6a63ea84a56805eb35e91dd89b933a7aee0c56a3838c6"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b3aa007a0f4408f62a8471bdaa3faccad644cbf2622639f2906b4f9b5339e8b8"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f3d4176fa93d8b2070db8f3c70dc21106ae6624fcaaa334be6bdd3a0251e729e"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d0e463655ef8b2064df07bd4a445ed7f76f6da3b286b4590812587d42f80e89"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:089138fd0351b62215c462a501bd68b8df0e213edcf99ab9efd5dba7b4cb733e"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e563ac628f5175ed08e950430e2580e544b3e4b606a0995bb6b52b3a3165728"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:244dcc28c5fde221cb4373961b20da30097669005b122384d7f9f22752487a46"},
{file = "tokenizers-0.15.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d82951d46052dddae1369e68ff799a0e6e29befa9a0b46e387ae710fd4daefb0"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b14296bc9059849246ceb256ffbe97f8806a9b5d707e0095c22db312f4fc014"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0309357bb9b6c8d86cdf456053479d7112074b470651a997a058cd7ad1c4ea57"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083f06e9d8d01b70b67bcbcb7751b38b6005512cce95808be6bf34803534a7e7"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85288aea86ada579789447f0dcec108ebef8da4b450037eb4813d83e4da9371e"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:385e6fcb01e8de90c1d157ae2a5338b23368d0b1c4cc25088cdca90147e35d17"},
{file = "tokenizers-0.15.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:60067edfcbf7d6cd448ac47af41ec6e84377efbef7be0c06f15a7c1dd069e044"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f7e37f89acfe237d4eaf93c3b69b0f01f407a7a5d0b5a8f06ba91943ea3cf10"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:6a63a15b523d42ebc1f4028e5a568013388c2aefa4053a263e511cb10aaa02f1"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2417d9e4958a6c2fbecc34c27269e74561c55d8823bf914b422e261a11fdd5fd"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8550974bace6210e41ab04231e06408cf99ea4279e0862c02b8d47e7c2b2828"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:194ba82129b171bcd29235a969e5859a93e491e9b0f8b2581f500f200c85cfdd"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1bfd95eef8b01e6c0805dbccc8eaf41d8c5a84f0cce72c0ab149fe76aae0bce6"},
{file = "tokenizers-0.15.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b87a15dd72f8216b03c151e3dace00c75c3fe7b0ee9643c25943f31e582f1a34"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6ac22f358a0c2a6c685be49136ce7ea7054108986ad444f567712cf274b34cd8"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e9d1f046a9b9d9a95faa103f07db5921d2c1c50f0329ebba4359350ee02b18b"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a0fd30a4b74485f6a7af89fffb5fb84d6d5f649b3e74f8d37f624cc9e9e97cf"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e45dc206b9447fa48795a1247c69a1732d890b53e2cc51ba42bc2fefa22407"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eaff56ef3e218017fa1d72007184401f04cb3a289990d2b6a0a76ce71c95f96"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b41dc107e4a4e9c95934e79b025228bbdda37d9b153d8b084160e88d5e48ad6f"},
{file = "tokenizers-0.15.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1922b8582d0c33488764bcf32e80ef6054f515369e70092729c928aae2284bc2"},
{file = "tokenizers-0.15.1.tar.gz", hash = "sha256:c0a331d6d5a3d6e97b7f99f562cee8d56797180797bc55f12070e495e717c980"},
{file = "tokenizers-0.15.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:52f6130c9cbf70544287575a985bf44ae1bda2da7e8c24e97716080593638012"},
{file = "tokenizers-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:054c1cc9c6d68f7ffa4e810b3d5131e0ba511b6e4be34157aa08ee54c2f8d9ee"},
{file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9b9b070fdad06e347563b88c278995735292ded1132f8657084989a4c84a6d5"},
{file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea621a7eef4b70e1f7a4e84dd989ae3f0eeb50fc8690254eacc08acb623e82f1"},
{file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf7fd9a5141634fa3aa8d6b7be362e6ae1b4cda60da81388fa533e0b552c98fd"},
{file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44f2a832cd0825295f7179eaf173381dc45230f9227ec4b44378322d900447c9"},
{file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b9ec69247a23747669ec4b0ca10f8e3dfb3545d550258129bd62291aabe8605"},
{file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b6a4c78da863ff26dbd5ad9a8ecc33d8a8d97b535172601cf00aee9d7ce9ce"},
{file = "tokenizers-0.15.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5ab2a4d21dcf76af60e05af8063138849eb1d6553a0d059f6534357bce8ba364"},
{file = "tokenizers-0.15.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a47acfac7e511f6bbfcf2d3fb8c26979c780a91e06fb5b9a43831b2c0153d024"},
{file = "tokenizers-0.15.2-cp310-none-win32.whl", hash = "sha256:064ff87bb6acdbd693666de9a4b692add41308a2c0ec0770d6385737117215f2"},
{file = "tokenizers-0.15.2-cp310-none-win_amd64.whl", hash = "sha256:3b919afe4df7eb6ac7cafd2bd14fb507d3f408db7a68c43117f579c984a73843"},
{file = "tokenizers-0.15.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:89cd1cb93e4b12ff39bb2d626ad77e35209de9309a71e4d3d4672667b4b256e7"},
{file = "tokenizers-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfed5c64e5be23d7ee0f0e98081a25c2a46b0b77ce99a4f0605b1ec43dd481fa"},
{file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a907d76dcfda37023ba203ab4ceeb21bc5683436ebefbd895a0841fd52f6f6f2"},
{file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20ea60479de6fc7b8ae756b4b097572372d7e4032e2521c1bbf3d90c90a99ff0"},
{file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48e2b9335be2bc0171df9281385c2ed06a15f5cf121c44094338306ab7b33f2c"},
{file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:112a1dd436d2cc06e6ffdc0b06d55ac019a35a63afd26475205cb4b1bf0bfbff"},
{file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4620cca5c2817177ee8706f860364cc3a8845bc1e291aaf661fb899e5d1c45b0"},
{file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccd73a82751c523b3fc31ff8194702e4af4db21dc20e55b30ecc2079c5d43cb7"},
{file = "tokenizers-0.15.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:107089f135b4ae7817affe6264f8c7a5c5b4fd9a90f9439ed495f54fcea56fb4"},
{file = "tokenizers-0.15.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0ff110ecc57b7aa4a594396525a3451ad70988e517237fe91c540997c4e50e29"},
{file = "tokenizers-0.15.2-cp311-none-win32.whl", hash = "sha256:6d76f00f5c32da36c61f41c58346a4fa7f0a61be02f4301fd30ad59834977cc3"},
{file = "tokenizers-0.15.2-cp311-none-win_amd64.whl", hash = "sha256:cc90102ed17271cf0a1262babe5939e0134b3890345d11a19c3145184b706055"},
{file = "tokenizers-0.15.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f86593c18d2e6248e72fb91c77d413a815153b8ea4e31f7cd443bdf28e467670"},
{file = "tokenizers-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0774bccc6608eca23eb9d620196687c8b2360624619623cf4ba9dc9bd53e8b51"},
{file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0222c5b7c9b26c0b4822a82f6a7011de0a9d3060e1da176f66274b70f846b98"},
{file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3835738be1de66624fff2f4f6f6684775da4e9c00bde053be7564cbf3545cc66"},
{file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0143e7d9dcd811855c1ce1ab9bf5d96d29bf5e528fd6c7824d0465741e8c10fd"},
{file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db35825f6d54215f6b6009a7ff3eedee0848c99a6271c870d2826fbbedf31a38"},
{file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f5e64b0389a2be47091d8cc53c87859783b837ea1a06edd9d8e04004df55a5c"},
{file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e0480c452217edd35eca56fafe2029fb4d368b7c0475f8dfa3c5c9c400a7456"},
{file = "tokenizers-0.15.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a33ab881c8fe70474980577e033d0bc9a27b7ab8272896e500708b212995d834"},
{file = "tokenizers-0.15.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a308a607ca9de2c64c1b9ba79ec9a403969715a1b8ba5f998a676826f1a7039d"},
{file = "tokenizers-0.15.2-cp312-none-win32.whl", hash = "sha256:b8fcfa81bcb9447df582c5bc96a031e6df4da2a774b8080d4f02c0c16b42be0b"},
{file = "tokenizers-0.15.2-cp312-none-win_amd64.whl", hash = "sha256:38d7ab43c6825abfc0b661d95f39c7f8af2449364f01d331f3b51c94dcff7221"},
{file = "tokenizers-0.15.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:38bfb0204ff3246ca4d5e726e8cc8403bfc931090151e6eede54d0e0cf162ef0"},
{file = "tokenizers-0.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c861d35e8286a53e06e9e28d030b5a05bcbf5ac9d7229e561e53c352a85b1fc"},
{file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:936bf3842db5b2048eaa53dade907b1160f318e7c90c74bfab86f1e47720bdd6"},
{file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:620beacc3373277700d0e27718aa8b25f7b383eb8001fba94ee00aeea1459d89"},
{file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2735ecbbf37e52db4ea970e539fd2d450d213517b77745114f92867f3fc246eb"},
{file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:473c83c5e2359bb81b0b6fde870b41b2764fcdd36d997485e07e72cc3a62264a"},
{file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968fa1fb3c27398b28a4eca1cbd1e19355c4d3a6007f7398d48826bbe3a0f728"},
{file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:865c60ae6eaebdde7da66191ee9b7db52e542ed8ee9d2c653b6d190a9351b980"},
{file = "tokenizers-0.15.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7c0d8b52664ab2d4a8d6686eb5effc68b78608a9008f086a122a7b2996befbab"},
{file = "tokenizers-0.15.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f33dfbdec3784093a9aebb3680d1f91336c56d86cc70ddf88708251da1fe9064"},
{file = "tokenizers-0.15.2-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d44ba80988ff9424e33e0a49445072ac7029d8c0e1601ad25a0ca5f41ed0c1d6"},
{file = "tokenizers-0.15.2-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:dce74266919b892f82b1b86025a613956ea0ea62a4843d4c4237be2c5498ed3a"},
{file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0ef06b9707baeb98b316577acb04f4852239d856b93e9ec3a299622f6084e4be"},
{file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73e2e74bbb07910da0d37c326869f34113137b23eadad3fc00856e6b3d9930c"},
{file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eeb12daf02a59e29f578a865f55d87cd103ce62bd8a3a5874f8fdeaa82e336b"},
{file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ba9f6895af58487ca4f54e8a664a322f16c26bbb442effd01087eba391a719e"},
{file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccec77aa7150e38eec6878a493bf8c263ff1fa8a62404e16c6203c64c1f16a26"},
{file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f40604f5042ff210ba82743dda2b6aa3e55aa12df4e9f2378ee01a17e2855e"},
{file = "tokenizers-0.15.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5645938a42d78c4885086767c70923abad047163d809c16da75d6b290cb30bbe"},
{file = "tokenizers-0.15.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:05a77cbfebe28a61ab5c3891f9939cc24798b63fa236d84e5f29f3a85a200c00"},
{file = "tokenizers-0.15.2-cp37-none-win32.whl", hash = "sha256:361abdc068e8afe9c5b818769a48624687fb6aaed49636ee39bec4e95e1a215b"},
{file = "tokenizers-0.15.2-cp37-none-win_amd64.whl", hash = "sha256:7ef789f83eb0f9baeb4d09a86cd639c0a5518528f9992f38b28e819df397eb06"},
{file = "tokenizers-0.15.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4fe1f74a902bee74a3b25aff180fbfbf4f8b444ab37c4d496af7afd13a784ed2"},
{file = "tokenizers-0.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c4b89038a684f40a6b15d6b09f49650ac64d951ad0f2a3ea9169687bbf2a8ba"},
{file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d05a1b06f986d41aed5f2de464c003004b2df8aaf66f2b7628254bcbfb72a438"},
{file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508711a108684111ec8af89d3a9e9e08755247eda27d0ba5e3c50e9da1600f6d"},
{file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:daa348f02d15160cb35439098ac96e3a53bacf35885072611cd9e5be7d333daa"},
{file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494fdbe5932d3416de2a85fc2470b797e6f3226c12845cadf054dd906afd0442"},
{file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2d60f5246f4da9373f75ff18d64c69cbf60c3bca597290cea01059c336d2470"},
{file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93268e788825f52de4c7bdcb6ebc1fcd4a5442c02e730faa9b6b08f23ead0e24"},
{file = "tokenizers-0.15.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6fc7083ab404019fc9acafe78662c192673c1e696bd598d16dc005bd663a5cf9"},
{file = "tokenizers-0.15.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41e39b41e5531d6b2122a77532dbea60e171ef87a3820b5a3888daa847df4153"},
{file = "tokenizers-0.15.2-cp38-none-win32.whl", hash = "sha256:06cd0487b1cbfabefb2cc52fbd6b1f8d4c37799bd6c6e1641281adaa6b2504a7"},
{file = "tokenizers-0.15.2-cp38-none-win_amd64.whl", hash = "sha256:5179c271aa5de9c71712e31cb5a79e436ecd0d7532a408fa42a8dbfa4bc23fd9"},
{file = "tokenizers-0.15.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82f8652a74cc107052328b87ea8b34291c0f55b96d8fb261b3880216a9f9e48e"},
{file = "tokenizers-0.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:02458bee6f5f3139f1ebbb6d042b283af712c0981f5bc50edf771d6b762d5e4f"},
{file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c9a09cd26cca2e1c349f91aa665309ddb48d71636370749414fbf67bc83c5343"},
{file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:158be8ea8554e5ed69acc1ce3fbb23a06060bd4bbb09029431ad6b9a466a7121"},
{file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ddba9a2b0c8c81633eca0bb2e1aa5b3a15362b1277f1ae64176d0f6eba78ab1"},
{file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ef5dd1d39797044642dbe53eb2bc56435308432e9c7907728da74c69ee2adca"},
{file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:454c203164e07a860dbeb3b1f4a733be52b0edbb4dd2e5bd75023ffa8b49403a"},
{file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cf6b7f1d4dc59af960e6ffdc4faffe6460bbfa8dce27a58bf75755ffdb2526d"},
{file = "tokenizers-0.15.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2ef09bbc16519f6c25d0c7fc0c6a33a6f62923e263c9d7cca4e58b8c61572afb"},
{file = "tokenizers-0.15.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c9a2ebdd2ad4ec7a68e7615086e633857c85e2f18025bd05d2a4399e6c5f7169"},
{file = "tokenizers-0.15.2-cp39-none-win32.whl", hash = "sha256:918fbb0eab96fe08e72a8c2b5461e9cce95585d82a58688e7f01c2bd546c79d0"},
{file = "tokenizers-0.15.2-cp39-none-win_amd64.whl", hash = "sha256:524e60da0135e106b254bd71f0659be9f89d83f006ea9093ce4d1fab498c6d0d"},
{file = "tokenizers-0.15.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9b648a58281c4672212fab04e60648fde574877d0139cd4b4f93fe28ca8944"},
{file = "tokenizers-0.15.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7c7d18b733be6bbca8a55084027f7be428c947ddf871c500ee603e375013ffba"},
{file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:13ca3611de8d9ddfbc4dc39ef54ab1d2d4aaa114ac8727dfdc6a6ec4be017378"},
{file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:237d1bf3361cf2e6463e6c140628e6406766e8b27274f5fcc62c747ae3c6f094"},
{file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67a0fe1e49e60c664915e9fb6b0cb19bac082ab1f309188230e4b2920230edb3"},
{file = "tokenizers-0.15.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4e022fe65e99230b8fd89ebdfea138c24421f91c1a4f4781a8f5016fd5cdfb4d"},
{file = "tokenizers-0.15.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d857be2df69763362ac699f8b251a8cd3fac9d21893de129bc788f8baaef2693"},
{file = "tokenizers-0.15.2-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:708bb3e4283177236309e698da5fcd0879ce8fd37457d7c266d16b550bcbbd18"},
{file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64c35e09e9899b72a76e762f9854e8750213f67567787d45f37ce06daf57ca78"},
{file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1257f4394be0d3b00de8c9e840ca5601d0a4a8438361ce9c2b05c7d25f6057b"},
{file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02272fe48280e0293a04245ca5d919b2c94a48b408b55e858feae9618138aeda"},
{file = "tokenizers-0.15.2-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dc3ad9ebc76eabe8b1d7c04d38be884b8f9d60c0cdc09b0aa4e3bcf746de0388"},
{file = "tokenizers-0.15.2-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:32e16bdeffa7c4f46bf2152172ca511808b952701d13e7c18833c0b73cb5c23f"},
{file = "tokenizers-0.15.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fb16ba563d59003028b678d2361a27f7e4ae0ab29c7a80690efa20d829c81fdb"},
{file = "tokenizers-0.15.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:2277c36d2d6cdb7876c274547921a42425b6810d38354327dd65a8009acf870c"},
{file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1cf75d32e8d250781940d07f7eece253f2fe9ecdb1dc7ba6e3833fa17b82fcbc"},
{file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1b3b31884dc8e9b21508bb76da80ebf7308fdb947a17affce815665d5c4d028"},
{file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10122d8d8e30afb43bb1fe21a3619f62c3e2574bff2699cf8af8b0b6c5dc4a3"},
{file = "tokenizers-0.15.2-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d88b96ff0fe8e91f6ef01ba50b0d71db5017fa4e3b1d99681cec89a85faf7bf7"},
{file = "tokenizers-0.15.2-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:37aaec5a52e959892870a7c47cef80c53797c0db9149d458460f4f31e2fb250e"},
{file = "tokenizers-0.15.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2ea752f2b0fe96eb6e2f3adbbf4d72aaa1272079b0dfa1145507bd6a5d537e6"},
{file = "tokenizers-0.15.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b19a808d8799fda23504a5cd31d2f58e6f52f140380082b352f877017d6342b"},
{file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64c86e5e068ac8b19204419ed8ca90f9d25db20578f5881e337d203b314f4104"},
{file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de19c4dc503c612847edf833c82e9f73cd79926a384af9d801dcf93f110cea4e"},
{file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea09acd2fe3324174063d61ad620dec3bcf042b495515f27f638270a7d466e8b"},
{file = "tokenizers-0.15.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cf27fd43472e07b57cf420eee1e814549203d56de00b5af8659cb99885472f1f"},
{file = "tokenizers-0.15.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7ca22bd897537a0080521445d91a58886c8c04084a6a19e6c78c586e0cfa92a5"},
{file = "tokenizers-0.15.2.tar.gz", hash = "sha256:e6e9c6e019dd5484be5beafc775ae6c925f4c69a3487040ed09b45e13df2cb91"},
]
[package.dependencies]
@@ -3281,13 +3281,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
version = "0.27.0.post1"
version = "0.27.1"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
{file = "uvicorn-0.27.0.post1-py3-none-any.whl", hash = "sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f"},
{file = "uvicorn-0.27.0.post1.tar.gz", hash = "sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907"},
{file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"},
{file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"},
]
[package.dependencies]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.95.0"
version = "1.95.1"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"
+1
View File
@@ -0,0 +1 @@
3.13.6
+1
View File
@@ -0,0 +1 @@
3.13.6
+1
View File
@@ -0,0 +1 @@
C:/Users/alext/fvm/versions/3.13.6
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 122,
"android.injected.version.name" => "1.95.0",
"android.injected.version.code" => 123,
"android.injected.version.name" => "1.95.1",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
+3 -3
View File
@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000276">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000232">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="22.651725">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="78.881681">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="31.263395">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.080999">
</testcase>
+1 -1
View File
@@ -180,4 +180,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
COCOAPODS: 1.12.1
COCOAPODS: 1.11.3
+3 -3
View File
@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 137;
CURRENT_PROJECT_VERSION = 139;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 137;
CURRENT_PROJECT_VERSION = 139;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 137;
CURRENT_PROJECT_VERSION = 139;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
+2 -2
View File
@@ -55,11 +55,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.94.1</string>
<string>1.95.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>137</string>
<string>139</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>
+1 -1
View File
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.95.0"
version_number: "1.95.1"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
+6 -6
View File
@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000247">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000255">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.238661">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.157832">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="2.965603">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.825919">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.228067">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.18815">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="108.628243">
<testcase classname="fastlane.lanes" name="4: build_app" time="110.912709">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="71.752027">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.396901">
</testcase>
@@ -1,13 +1,11 @@
import 'dart:ui';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
class MemoryCard extends StatelessWidget {
final Asset asset;
@@ -44,14 +42,9 @@ class MemoryCard extends StatelessWidget {
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: CachedNetworkImageProvider(
getThumbnailUrl(
asset,
),
cacheKey: getThumbnailCacheKey(
asset,
),
headers: {"x-immich-user-token": accessToken},
image: ImmichImage.imageProvider(
asset: asset,
isThumbnail: true,
),
fit: BoxFit.cover,
),
@@ -58,6 +58,18 @@ class AssetService {
final assetDto = await _apiService.assetApi
.getAllAssets(userId: user.id, updatedAfter: since);
if (assetDto == null) return (null, null);
print("AssetDto length: ${assetDto.length} ");
for (final e in assetDto) {
print("AssetDto: ${e.stackParentId}");
var b = Asset.remote(e);
print("e.stackParentId ${e.stackParentId}");
print("e.id ${e.id}");
print(
"e.stackParentId == e.id ? null : e.stackParentId, ${e.stackParentId == e.id ? null : e.stackParentId}",
);
print("Mapped asset ${b.stackParentId}");
}
return (assetDto.map(Asset.remote).toList(), deleted.ids);
}
@@ -82,6 +94,7 @@ class AssetService {
if (assets == null) {
return null;
}
allAssets.addAll(assets.map(Asset.remote));
if (assets.length < chunkSize) {
break;
+1 -1
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.95.0
- API version: 1.95.1
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
+1 -1
View File
@@ -1153,7 +1153,7 @@ Name | Type | Description | Notes
**updatedAfter** | **DateTime**| | [optional]
**updatedBefore** | **DateTime**| | [optional]
**webpPath** | **String**| | [optional]
**withArchived** | **bool**| | [optional]
**withArchived** | **bool**| | [optional] [default to false]
**withDeleted** | **bool**| | [optional]
**withExif** | **bool**| | [optional]
**withPeople** | **bool**| | [optional]
+1 -1
View File
@@ -46,7 +46,7 @@ Name | Type | Description | Notes
**updatedAfter** | [**DateTime**](DateTime.md) | | [optional]
**updatedBefore** | [**DateTime**](DateTime.md) | | [optional]
**webpPath** | **String** | | [optional]
**withArchived** | **bool** | | [optional]
**withArchived** | **bool** | | [optional] [default to false]
**withDeleted** | **bool** | | [optional]
**withExif** | **bool** | | [optional]
**withPeople** | **bool** | | [optional]
+1
View File
@@ -8,6 +8,7 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**hidden** | **int** | |
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [default to const []]
**total** | **int** | |
+2 -1
View File
@@ -18,6 +18,7 @@ Name | Type | Description | Notes
**isExternal** | **bool** | | [optional]
**isFavorite** | **bool** | | [optional]
**isMotion** | **bool** | | [optional]
**isNotInAlbum** | **bool** | | [optional]
**isOffline** | **bool** | | [optional]
**isReadOnly** | **bool** | | [optional]
**isVisible** | **bool** | | [optional]
@@ -36,7 +37,7 @@ Name | Type | Description | Notes
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | [optional]
**updatedAfter** | [**DateTime**](DateTime.md) | | [optional]
**updatedBefore** | [**DateTime**](DateTime.md) | | [optional]
**withArchived** | **bool** | | [optional]
**withArchived** | **bool** | | [optional] [default to false]
**withDeleted** | **bool** | | [optional]
**withExif** | **bool** | | [optional]
+4 -14
View File
@@ -51,7 +51,7 @@ class MetadataSearchDto {
this.updatedAfter,
this.updatedBefore,
this.webpPath,
this.withArchived,
this.withArchived = false,
this.withDeleted,
this.withExif,
this.withPeople,
@@ -356,13 +356,7 @@ class MetadataSearchDto {
///
String? webpPath;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? withArchived;
bool withArchived;
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -483,7 +477,7 @@ class MetadataSearchDto {
(updatedAfter == null ? 0 : updatedAfter!.hashCode) +
(updatedBefore == null ? 0 : updatedBefore!.hashCode) +
(webpPath == null ? 0 : webpPath!.hashCode) +
(withArchived == null ? 0 : withArchived!.hashCode) +
(withArchived.hashCode) +
(withDeleted == null ? 0 : withDeleted!.hashCode) +
(withExif == null ? 0 : withExif!.hashCode) +
(withPeople == null ? 0 : withPeople!.hashCode) +
@@ -680,11 +674,7 @@ class MetadataSearchDto {
} else {
// json[r'webpPath'] = null;
}
if (this.withArchived != null) {
json[r'withArchived'] = this.withArchived;
} else {
// json[r'withArchived'] = null;
}
if (this.withDeleted != null) {
json[r'withDeleted'] = this.withDeleted;
} else {
@@ -756,7 +746,7 @@ class MetadataSearchDto {
updatedAfter: mapDateTime(json, r'updatedAfter', r''),
updatedBefore: mapDateTime(json, r'updatedBefore', r''),
webpPath: mapValueOfType<String>(json, r'webpPath'),
withArchived: mapValueOfType<bool>(json, r'withArchived'),
withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
withExif: mapValueOfType<bool>(json, r'withExif'),
withPeople: mapValueOfType<bool>(json, r'withPeople'),
+9 -1
View File
@@ -13,30 +13,36 @@ part of openapi.api;
class PeopleResponseDto {
/// Returns a new [PeopleResponseDto] instance.
PeopleResponseDto({
required this.hidden,
this.people = const [],
required this.total,
});
int hidden;
List<PersonResponseDto> people;
int total;
@override
bool operator ==(Object other) => identical(this, other) || other is PeopleResponseDto &&
other.hidden == hidden &&
_deepEquality.equals(other.people, people) &&
other.total == total;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(hidden.hashCode) +
(people.hashCode) +
(total.hashCode);
@override
String toString() => 'PeopleResponseDto[people=$people, total=$total]';
String toString() => 'PeopleResponseDto[hidden=$hidden, people=$people, total=$total]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'hidden'] = this.hidden;
json[r'people'] = this.people;
json[r'total'] = this.total;
return json;
@@ -50,6 +56,7 @@ class PeopleResponseDto {
final json = value.cast<String, dynamic>();
return PeopleResponseDto(
hidden: mapValueOfType<int>(json, r'hidden')!,
people: PersonResponseDto.listFromJson(json[r'people']),
total: mapValueOfType<int>(json, r'total')!,
);
@@ -99,6 +106,7 @@ class PeopleResponseDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'hidden',
'people',
'total',
};
+22 -15
View File
@@ -23,6 +23,7 @@ class SmartSearchDto {
this.isExternal,
this.isFavorite,
this.isMotion,
this.isNotInAlbum,
this.isOffline,
this.isReadOnly,
this.isVisible,
@@ -41,7 +42,7 @@ class SmartSearchDto {
this.type,
this.updatedAfter,
this.updatedBefore,
this.withArchived,
this.withArchived = false,
this.withDeleted,
this.withExif,
});
@@ -126,6 +127,14 @@ class SmartSearchDto {
///
bool? isMotion;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? isNotInAlbum;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@@ -264,13 +273,7 @@ class SmartSearchDto {
///
DateTime? updatedBefore;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? withArchived;
bool withArchived;
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -300,6 +303,7 @@ class SmartSearchDto {
other.isExternal == isExternal &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
other.isNotInAlbum == isNotInAlbum &&
other.isOffline == isOffline &&
other.isReadOnly == isReadOnly &&
other.isVisible == isVisible &&
@@ -335,6 +339,7 @@ class SmartSearchDto {
(isExternal == null ? 0 : isExternal!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
(isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
(isOffline == null ? 0 : isOffline!.hashCode) +
(isReadOnly == null ? 0 : isReadOnly!.hashCode) +
(isVisible == null ? 0 : isVisible!.hashCode) +
@@ -353,12 +358,12 @@ class SmartSearchDto {
(type == null ? 0 : type!.hashCode) +
(updatedAfter == null ? 0 : updatedAfter!.hashCode) +
(updatedBefore == null ? 0 : updatedBefore!.hashCode) +
(withArchived == null ? 0 : withArchived!.hashCode) +
(withArchived.hashCode) +
(withDeleted == null ? 0 : withDeleted!.hashCode) +
(withExif == null ? 0 : withExif!.hashCode);
@override
String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]';
String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -412,6 +417,11 @@ class SmartSearchDto {
} else {
// json[r'isMotion'] = null;
}
if (this.isNotInAlbum != null) {
json[r'isNotInAlbum'] = this.isNotInAlbum;
} else {
// json[r'isNotInAlbum'] = null;
}
if (this.isOffline != null) {
json[r'isOffline'] = this.isOffline;
} else {
@@ -498,11 +508,7 @@ class SmartSearchDto {
} else {
// json[r'updatedBefore'] = null;
}
if (this.withArchived != null) {
json[r'withArchived'] = this.withArchived;
} else {
// json[r'withArchived'] = null;
}
if (this.withDeleted != null) {
json[r'withDeleted'] = this.withDeleted;
} else {
@@ -534,6 +540,7 @@ class SmartSearchDto {
isExternal: mapValueOfType<bool>(json, r'isExternal'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isMotion: mapValueOfType<bool>(json, r'isMotion'),
isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
isOffline: mapValueOfType<bool>(json, r'isOffline'),
isReadOnly: mapValueOfType<bool>(json, r'isReadOnly'),
isVisible: mapValueOfType<bool>(json, r'isVisible'),
@@ -552,7 +559,7 @@ class SmartSearchDto {
type: AssetTypeEnum.fromJson(json[r'type']),
updatedAfter: mapDateTime(json, r'updatedAfter', r''),
updatedBefore: mapDateTime(json, r'updatedBefore', r''),
withArchived: mapValueOfType<bool>(json, r'withArchived'),
withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
withExif: mapValueOfType<bool>(json, r'withExif'),
);
+1 -1
View File
@@ -206,7 +206,7 @@ void main() {
// TODO
});
// bool withArchived
// bool withArchived (default value: false)
test('to test the property `withArchived`', () async {
// TODO
});
+5
View File
@@ -16,6 +16,11 @@ void main() {
// final instance = PeopleResponseDto();
group('test PeopleResponseDto', () {
// int hidden
test('to test the property `hidden`', () async {
// TODO
});
// List<PersonResponseDto> people (default value: const [])
test('to test the property `people`', () async {
// TODO
+6 -1
View File
@@ -66,6 +66,11 @@ void main() {
// TODO
});
// bool isNotInAlbum
test('to test the property `isNotInAlbum`', () async {
// TODO
});
// bool isOffline
test('to test the property `isOffline`', () async {
// TODO
@@ -156,7 +161,7 @@ void main() {
// TODO
});
// bool withArchived
// bool withArchived (default value: false)
test('to test the property `withArchived`', () async {
// TODO
});
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.95.0+122
version: 1.95.1+123
isar_version: &isar_version 3.1.0+1
environment:
+11 -1
View File
@@ -2463,6 +2463,7 @@
"required": false,
"in": "query",
"schema": {
"default": false,
"type": "boolean"
}
},
@@ -6413,7 +6414,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.95.0",
"version": "1.95.1",
"contact": {}
},
"tags": [],
@@ -8429,6 +8430,7 @@
"type": "string"
},
"withArchived": {
"default": false,
"type": "boolean"
},
"withDeleted": {
@@ -8591,6 +8593,9 @@
},
"PeopleResponseDto": {
"properties": {
"hidden": {
"type": "integer"
},
"people": {
"items": {
"$ref": "#/components/schemas/PersonResponseDto"
@@ -8602,6 +8607,7 @@
}
},
"required": [
"hidden",
"people",
"total"
],
@@ -9435,6 +9441,9 @@
"isMotion": {
"type": "boolean"
},
"isNotInAlbum": {
"type": "boolean"
},
"isOffline": {
"type": "boolean"
},
@@ -9497,6 +9506,7 @@
"type": "string"
},
"withArchived": {
"default": false,
"type": "boolean"
},
"withDeleted": {
+13 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.0
* The version of the OpenAPI document: 1.95.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -2801,6 +2801,12 @@ export type PathType = typeof PathType[keyof typeof PathType];
* @interface PeopleResponseDto
*/
export interface PeopleResponseDto {
/**
*
* @type {number}
* @memberof PeopleResponseDto
*/
'hidden': number;
/**
*
* @type {Array<PersonResponseDto>}
@@ -3880,6 +3886,12 @@ export interface SmartSearchDto {
* @memberof SmartSearchDto
*/
'isMotion'?: boolean;
/**
*
* @type {boolean}
* @memberof SmartSearchDto
*/
'isNotInAlbum'?: boolean;
/**
*
* @type {boolean}
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.0
* The version of the OpenAPI document: 1.95.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.0
* The version of the OpenAPI document: 1.95.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.0
* The version of the OpenAPI document: 1.95.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.95.0
* The version of the OpenAPI document: 1.95.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+3 -1
View File
@@ -1,6 +1,6 @@
/**
* Immich
* 1.95.0
* 1.95.1
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
@@ -524,6 +524,7 @@ export type UpdatePartnerDto = {
inTimeline: boolean;
};
export type PeopleResponseDto = {
hidden: number;
people: PersonResponseDto[];
total: number;
};
@@ -656,6 +657,7 @@ export type SmartSearchDto = {
isExternal?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isNotInAlbum?: boolean;
isOffline?: boolean;
isReadOnly?: boolean;
isVisible?: boolean;
+3 -3
View File
@@ -29,9 +29,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
"integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==",
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
+2 -2
View File
@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20240213@sha256:16646a37bae065b51e68cb2ba7a63027b29504d43a30644625382afbe326114a as dev
FROM ghcr.io/immich-app/base-server-dev:20240222@sha256:2ff467d6ae5c00a2317eb7b13cb40ba5be0fd33c160175dba621b1bf72bc1cd1 as dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@@ -40,7 +40,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20240213@sha256:61d159d069c5b522f16de9733fb79feb0e82c0b099d16f026196f344d12a1e5e
FROM ghcr.io/immich-app/base-server-prod:20240222@sha256:9ae5eebf95cf7759eec9dcfbd9e48a722701075ac855209f2e0b01c631b76f5c
WORKDIR /usr/src/app
ENV NODE_ENV=production \
+42 -15
View File
@@ -50,6 +50,7 @@ describe(`${AssetController.name} (e2e)`, () => {
let asset3: AssetResponseDto;
let asset4: AssetResponseDto;
let asset5: AssetResponseDto;
let asset6: AssetResponseDto;
const createAsset = async (
loginResponse: LoginResponseDto,
@@ -96,12 +97,11 @@ describe(`${AssetController.name} (e2e)`, () => {
beforeEach(async () => {
await testApp.reset({ entities: [AssetEntity, AssetStackEntity] });
[asset1, asset2, asset3, asset4, asset5] = await Promise.all([
[asset1, asset2, asset3, asset4, asset5, asset6] = await Promise.all([
createAsset(user1, new Date('1970-01-01')),
createAsset(user1, new Date('1970-02-10')),
createAsset(user1, new Date('1970-02-11'), {
isFavorite: true,
isArchived: true,
isExternal: true,
isReadOnly: true,
type: AssetType.VIDEO,
@@ -118,6 +118,9 @@ describe(`${AssetController.name} (e2e)`, () => {
createAsset(user1, new Date('1970-01-01'), {
deletedAt: yesterday.toJSDate(),
}),
createAsset(user1, new Date('1970-02-11'), {
isArchived: true,
}),
]);
await assetRepository.upsertExif({
@@ -275,14 +278,14 @@ describe(`${AssetController.name} (e2e)`, () => {
should: 'should search by isArchived (true)',
deferred: () => ({
query: { isArchived: true },
assets: [asset3],
assets: [asset6],
}),
},
{
should: 'should search by isArchived (false)',
deferred: () => ({
query: { isArchived: false },
assets: [asset2, asset1],
assets: [asset3, asset2, asset1],
}),
},
{
@@ -313,6 +316,20 @@ describe(`${AssetController.name} (e2e)`, () => {
assets: [asset3],
}),
},
{
should: 'should search by withArchived (true)',
deferred: () => ({
query: { withArchived: true },
assets: [asset3, asset6, asset2, asset1],
}),
},
{
should: 'should search by withArchived (false)',
deferred: () => ({
query: { withArchived: false },
assets: [asset3, asset2, asset1],
}),
},
{
should: 'should search by createdBefore',
deferred: () => ({
@@ -514,8 +531,8 @@ describe(`${AssetController.name} (e2e)`, () => {
expect(status).toBe(200);
expect(body.length).toBe(assets.length);
for (let i = 0; i < assets.length; i++) {
expect(body[i]).toEqual(expect.objectContaining({ id: assets[i].id }));
for (const [i, asset] of assets.entries()) {
expect(body[i]).toEqual(expect.objectContaining({ id: asset.id }));
}
});
}
@@ -682,7 +699,7 @@ describe(`${AssetController.name} (e2e)`, () => {
it("should not upload to another user's library", async () => {
const content = randomBytes(32);
const library = (await api.libraryApi.getAll(server, user2.accessToken))[0];
const [library] = await api.libraryApi.getAll(server, user2.accessToken);
await api.assetApi.upload(server, user1.accessToken, 'example-image', { content });
const { body, status } = await request(server)
@@ -902,7 +919,7 @@ describe(`${AssetController.name} (e2e)`, () => {
.get('/asset/statistics')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(body).toEqual({ images: 5, videos: 1, total: 6 });
expect(body).toEqual({ images: 6, videos: 1, total: 7 });
expect(status).toBe(200);
});
@@ -923,7 +940,7 @@ describe(`${AssetController.name} (e2e)`, () => {
.query({ isArchived: true });
expect(status).toBe(200);
expect(body).toEqual({ images: 2, videos: 1, total: 3 });
expect(body).toEqual({ images: 3, videos: 0, total: 3 });
});
it('should return stats of all favored and archived assets', async () => {
@@ -933,7 +950,7 @@ describe(`${AssetController.name} (e2e)`, () => {
.query({ isFavorite: true, isArchived: true });
expect(status).toBe(200);
expect(body).toEqual({ images: 1, videos: 1, total: 2 });
expect(body).toEqual({ images: 1, videos: 0, total: 1 });
});
it('should return stats of all assets neither favored nor archived', async () => {
@@ -1041,7 +1058,7 @@ describe(`${AssetController.name} (e2e)`, () => {
expect.arrayContaining([
{ count: 1, timeBucket: '2023-11-01T00:00:00.000Z' },
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
{ count: 1, timeBucket: '1970-02-01T00:00:00.000Z' },
{ count: 2, timeBucket: '1970-02-01T00:00:00.000Z' },
]),
);
});
@@ -1198,8 +1215,13 @@ describe(`${AssetController.name} (e2e)`, () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset2.id })]));
expect(body).toHaveLength(2);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: asset2.id }),
expect.objectContaining({ id: asset3.id }),
]),
);
});
it('should get all map markers', async () => {
@@ -1209,8 +1231,13 @@ describe(`${AssetController.name} (e2e)`, () => {
.query({ isArchived: false });
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body).toEqual([expect.objectContaining({ id: asset2.id })]);
expect(body).toHaveLength(2);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: asset2.id }),
expect.objectContaining({ id: asset3.id }),
]),
);
});
});
-191
View File
@@ -1,191 +0,0 @@
import { IPersonRepository, LoginResponseDto } from '@app/domain';
import { PersonController } from '@app/immich';
import { PersonEntity } from '@app/infra/entities';
import { INestApplication } from '@nestjs/common';
import { errorStub, uuidStub } from '@test/fixtures';
import request from 'supertest';
import { api } from '../../client';
import { testApp } from '../utils';
describe(`${PersonController.name}`, () => {
let app: INestApplication;
let server: any;
let loginResponse: LoginResponseDto;
let accessToken: string;
let personRepository: IPersonRepository;
let visiblePerson: PersonEntity;
let hiddenPerson: PersonEntity;
beforeAll(async () => {
app = await testApp.create();
server = app.getHttpServer();
personRepository = app.get<IPersonRepository>(IPersonRepository);
});
afterAll(async () => {
await testApp.teardown();
});
beforeEach(async () => {
await testApp.reset();
await api.authApi.adminSignUp(server);
loginResponse = await api.authApi.adminLogin(server);
accessToken = loginResponse.accessToken;
const faceAsset = await api.assetApi.upload(server, accessToken, 'face_asset');
visiblePerson = await personRepository.create({
ownerId: loginResponse.userId,
name: 'visible_person',
thumbnailPath: '/thumbnail/face_asset',
});
await personRepository.createFaces([
{
assetId: faceAsset.id,
personId: visiblePerson.id,
embedding: Array.from({ length: 512 }, Math.random),
},
]);
hiddenPerson = await personRepository.create({
ownerId: loginResponse.userId,
name: 'hidden_person',
isHidden: true,
thumbnailPath: '/thumbnail/face_asset',
});
await personRepository.createFaces([
{
assetId: faceAsset.id,
personId: hiddenPerson.id,
embedding: Array.from({ length: 512 }, Math.random),
},
]);
});
describe('GET /person', () => {
beforeEach(async () => {});
it('should require authentication', async () => {
const { status, body } = await request(server).get('/person');
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should return all people (including hidden)', async () => {
const { status, body } = await request(server)
.get('/person')
.set('Authorization', `Bearer ${accessToken}`)
.query({ withHidden: true });
expect(status).toBe(200);
expect(body).toEqual({
total: 2,
people: [
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ name: 'hidden_person' }),
],
});
});
it('should return only visible people', async () => {
const { status, body } = await request(server).get('/person').set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
total: 2,
people: [expect.objectContaining({ name: 'visible_person' })],
});
});
});
describe('GET /person/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get(`/person/${uuidStub.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(server)
.get(`/person/${uuidStub.notFound}`)
.set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest());
});
it('should return person information', async () => {
const { status, body } = await request(server)
.get(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: visiblePerson.id }));
});
});
describe('PUT /person/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).put(`/person/${uuidStub.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorStub.unauthorized);
});
for (const { key, type } of [
{ key: 'name', type: 'string' },
{ key: 'featureFaceAssetId', type: 'string' },
{ key: 'isHidden', type: 'boolean value' },
]) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(server)
.put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest([`${key} must be a ${type}`]));
});
}
it('should not accept invalid birth dates', async () => {
for (const { birthDate, response } of [
{ birthDate: false, response: 'Not found or no person.write access' },
{ birthDate: 'false', response: ['birthDate must be a Date instance'] },
{ birthDate: '123567', response: 'Not found or no person.write access' },
{ birthDate: 123567, response: 'Not found or no person.write access' },
]) {
const { status, body } = await request(server)
.put(`/person/${uuidStub.notFound}`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ birthDate });
expect(status).toBe(400);
expect(body).toEqual(errorStub.badRequest(response));
}
});
it('should update a date of birth', async () => {
const { status, body } = await request(server)
.put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ birthDate: '1990-01-01T05:00:00.000Z' });
expect(status).toBe(200);
expect(body).toMatchObject({ birthDate: '1990-01-01' });
});
it('should clear a date of birth', async () => {
const person = await personRepository.create({
birthDate: new Date('1990-01-01'),
ownerId: loginResponse.userId,
});
expect(person.birthDate).toBeDefined();
const { status, body } = await request(server)
.put(`/person/${person.id}`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ birthDate: null });
expect(status).toBe(200);
expect(body).toMatchObject({ birthDate: null });
});
});
});
+3 -3
View File
@@ -44,7 +44,7 @@ describe(`${SearchController.name}`, () => {
describe('GET /search (exif)', () => {
beforeEach(async () => {
const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id;
const { id: assetId } = await assetRepository.create(generateAsset(loginResponse.userId, libraries));
await assetRepository.upsertExif({ assetId, ...searchStub.exif });
const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true });
@@ -166,7 +166,7 @@ describe(`${SearchController.name}`, () => {
describe('GET /search (smart info)', () => {
beforeEach(async () => {
const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id;
const { id: assetId } = await assetRepository.create(generateAsset(loginResponse.userId, libraries));
await assetRepository.upsertExif({ assetId, ...searchStub.exif });
await smartInfoRepository.upsert({ assetId, ...searchStub.smartInfo }, Array.from({ length: 512 }, Math.random));
@@ -215,7 +215,7 @@ describe(`${SearchController.name}`, () => {
describe('GET /search (file name)', () => {
beforeEach(async () => {
const assetId = (await assetRepository.create(generateAsset(loginResponse.userId, libraries))).id;
const { id: assetId } = await assetRepository.create(generateAsset(loginResponse.userId, libraries));
await assetRepository.upsertExif({ assetId, ...searchStub.exif });
const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true });
-14
View File
@@ -1,14 +0,0 @@
import { ActivityCreateDto, ActivityResponseDto } from '@app/domain';
import request from 'supertest';
export const activityApi = {
create: async (server: any, accessToken: string, dto: ActivityCreateDto) => {
const res = await request(server).post('/activity').set('Authorization', `Bearer ${accessToken}`).send(dto);
expect(res.status === 200 || res.status === 201).toBe(true);
return res.body as ActivityResponseDto;
},
delete: async (server: any, accessToken: string, id: string) => {
const res = await request(server).delete(`/activity/${id}`).set('Authorization', `Bearer ${accessToken}`);
expect(res.status).toEqual(204);
},
};
-28
View File
@@ -1,28 +0,0 @@
import { AddUsersDto, AlbumResponseDto, BulkIdResponseDto, BulkIdsDto, CreateAlbumDto } from '@app/domain';
import request from 'supertest';
export const albumApi = {
create: async (server: any, accessToken: string, dto: CreateAlbumDto) => {
const res = await request(server).post('/album').set('Authorization', `Bearer ${accessToken}`).send(dto);
expect(res.status).toEqual(201);
return res.body as AlbumResponseDto;
},
addAssets: async (server: any, accessToken: string, id: string, dto: BulkIdsDto) => {
const res = await request(server)
.put(`/album/${id}/assets`)
.set('Authorization', `Bearer ${accessToken}`)
.send(dto);
expect(res.status).toEqual(200);
return res.body as BulkIdResponseDto[];
},
addUsers: async (server: any, accessToken: string, id: string, dto: AddUsersDto) => {
const res = await request(server).put(`/album/${id}/users`).set('Authorization', `Bearer ${accessToken}`).send(dto);
expect(res.status).toEqual(200);
return res.body as AlbumResponseDto;
},
getAllAlbums: async (server: any, accessToken: string) => {
const res = await request(server).get(`/album/`).set('Authorization', `Bearer ${accessToken}`).send();
expect(res.status).toEqual(200);
return res.body as AlbumResponseDto[];
},
};
-16
View File
@@ -1,16 +0,0 @@
import { APIKeyCreateResponseDto } from '@app/domain';
import { apiKeyCreateStub } from '@test';
import request from 'supertest';
export const apiKeyApi = {
createApiKey: async (server: any, accessToken: string) => {
const { status, body } = await request(server)
.post('/api-key')
.set('Authorization', `Bearer ${accessToken}`)
.send(apiKeyCreateStub);
expect(status).toBe(201);
return body as APIKeyCreateResponseDto;
},
};
+1 -16
View File
@@ -1,4 +1,4 @@
import { AuthDeviceResponseDto, LoginCredentialDto, LoginResponseDto, UserResponseDto } from '@app/domain';
import { LoginCredentialDto, LoginResponseDto, UserResponseDto } from '@app/domain';
import { adminSignupStub, loginResponseStub, loginStub } from '@test';
import request from 'supertest';
@@ -27,19 +27,4 @@ export const authApi = {
return body as LoginResponseDto;
},
getAuthDevices: async (server: any, accessToken: string) => {
const { status, body } = await request(server).get('/auth/devices').set('Authorization', `Bearer ${accessToken}`);
expect(body).toEqual(expect.any(Array));
expect(status).toBe(200);
return body as AuthDeviceResponseDto[];
},
validateToken: async (server: any, accessToken: string) => {
const { status, body } = await request(server)
.post('/auth/validateToken')
.set('Authorization', `Bearer ${accessToken}`);
expect(body).toEqual({ authStatus: true });
expect(status).toBe(200);
},
};
-10
View File
@@ -1,25 +1,15 @@
import { activityApi } from './activity-api';
import { albumApi } from './album-api';
import { apiKeyApi } from './api-key-api';
import { assetApi } from './asset-api';
import { authApi } from './auth-api';
import { libraryApi } from './library-api';
import { partnerApi } from './partner-api';
import { serverInfoApi } from './server-info-api';
import { sharedLinkApi } from './shared-link-api';
import { trashApi } from './trash-api';
import { userApi } from './user-api';
export const api = {
activityApi,
authApi,
apiKeyApi,
assetApi,
libraryApi,
serverInfoApi,
sharedLinkApi,
trashApi,
albumApi,
userApi,
partnerApi,
};
-10
View File
@@ -1,10 +0,0 @@
import { PartnerResponseDto } from '@app/domain';
import request from 'supertest';
export const partnerApi = {
create: async (server: any, accessToken: string, id: string) => {
const { status, body } = await request(server).post(`/partner/${id}`).set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(201);
return body as PartnerResponseDto;
},
};
-10
View File
@@ -1,10 +0,0 @@
import { ServerConfigDto } from '@app/domain';
import request from 'supertest';
export const serverInfoApi = {
getConfig: async (server: any) => {
const res = await request(server).get('/server-info/config');
expect(res.status).toBe(200);
return res.body as ServerConfigDto;
},
};
-7
View File
@@ -10,11 +10,4 @@ export const sharedLinkApi = {
expect(status).toBe(201);
return body as SharedLinkResponseDto;
},
getMySharedLink: async (server: any, key: string) => {
const { status, body } = await request(server).get('/shared-link/me').query({ key });
expect(status).toBe(200);
return body as SharedLinkResponseDto;
},
};
-18
View File
@@ -18,16 +18,6 @@ export const userApi = {
return body as UserResponseDto;
},
get: async (server: any, accessToken: string, id: string) => {
const { status, body } = await request(server)
.get(`/user/info/${id}`)
.set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id });
return body as UserResponseDto;
},
update: async (server: any, accessToken: string, dto: UpdateUserDto) => {
const { status, body } = await request(server).put('/user').set('Authorization', `Bearer ${accessToken}`).send(dto);
@@ -39,12 +29,4 @@ export const userApi = {
setExternalPath: async (server: any, accessToken: string, id: string, externalPath: string) => {
return await userApi.update(server, accessToken, { id, externalPath });
},
delete: async (server: any, accessToken: string, id: string) => {
const { status, body } = await request(server).delete(`/user/${id}`).set('Authorization', `Bearer ${accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id, deletedAt: expect.any(String) });
return body as UserResponseDto;
},
};
+8 -8
View File
@@ -1,7 +1,7 @@
import { LoginResponseDto } from '@app/domain';
import { AssetType } from '@app/infra/entities';
import { readFile } from 'fs/promises';
import { basename, join } from 'path';
import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { IMMICH_TEST_ASSET_PATH, testApp } from '../../../src/test-utils/utils';
import { api } from '../../client';
@@ -19,7 +19,7 @@ const JPEG = {
iso: 200,
fNumber: 11,
exposureTime: '1/160',
fileSizeInByte: 53493,
fileSizeInByte: 53_493,
make: 'SONY',
model: 'DSLR-A550',
orientation: null,
@@ -42,11 +42,11 @@ const tests = [
exifImageWidth: 4032,
exifImageHeight: 3024,
latitude: 41.2203,
longitude: -96.071625,
longitude: -96.071_625,
make: 'Apple',
model: 'iPhone 7',
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
fileSizeInByte: 880703,
fileSizeInByte: 880_703,
exposureTime: '1/887',
iso: 20,
focalLength: 3.99,
@@ -66,7 +66,7 @@ const tests = [
exifImageHeight: 800,
latitude: null,
longitude: null,
fileSizeInByte: 25408,
fileSizeInByte: 25_408,
},
},
},
@@ -84,7 +84,7 @@ const tests = [
fNumber: 10,
focalLength: 18,
iso: 100,
fileSizeInByte: 9057784,
fileSizeInByte: 9_057_784,
dateTimeOriginal: '2010-07-20T17:27:12.000Z',
latitude: null,
longitude: null,
@@ -106,7 +106,7 @@ const tests = [
fNumber: 11,
focalLength: 85,
iso: 200,
fileSizeInByte: 15856335,
fileSizeInByte: 15_856_335,
dateTimeOriginal: '2016-09-22T22:10:29.060Z',
latitude: null,
longitude: null,
@@ -1,7 +1,7 @@
import { LibraryResponseDto, LibraryService, LoginResponseDto } from '@app/domain';
import { AssetType, LibraryType } from '@app/infra/entities';
import fs from 'fs/promises';
import path from 'path';
import fs from 'node:fs/promises';
import path from 'node:path';
import {
IMMICH_TEST_ASSET_PATH,
IMMICH_TEST_ASSET_TEMP_PATH,
@@ -20,7 +20,8 @@ describe(`Library watcher (e2e)`, () => {
beforeAll(async () => {
process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/../config/library-watcher-e2e-config.json`);
server = (await testApp.create()).getHttpServer();
const app = await testApp.create();
server = app.getHttpServer();
libraryService = testApp.get(LibraryService);
});
+50 -50
View File
@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.95.0",
"version": "1.95.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.95.0",
"version": "1.95.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@babel/runtime": "^7.22.11",
@@ -31,8 +31,8 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"exiftool-vendored": "~24.4.0",
"exiftool-vendored.pl": "12.73",
"exiftool-vendored": "~24.5.0",
"exiftool-vendored.pl": "12.76",
"fluent-ffmpeg": "^2.1.2",
"geo-tz": "^8.0.0",
"glob": "^10.3.3",
@@ -2705,9 +2705,9 @@
}
},
"node_modules/@photostructure/tz-lookup": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.0.tgz",
"integrity": "sha512-gM3Xrs+XhD8ojDN0TgybuzSjsQb9UvF8j9DvR75E2zHlJQNiOztzILvfhVwadgA8JJbSMNzE+kYUnwP8aQnlXw=="
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.1.tgz",
"integrity": "sha512-inMfhc1QVKheq/PHF0v2vRPnPzTljxscuOKK95o3VlZA4T4w4DeYIpu7dC4W1EyjUhfZJlCBUudpmnFGNCqTog=="
},
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
@@ -3179,9 +3179,9 @@
}
},
"node_modules/@types/node": {
"version": "20.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
"integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==",
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -4280,9 +4280,9 @@
}
},
"node_modules/batch-cluster": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-12.1.0.tgz",
"integrity": "sha512-whGyJU4tr7kyg2USByu0/51mML5HsLAeNz5s03kMDYZNsQsGgDJgI47RdY3r7MciCjPkTaTD5O4eOVqOfEO7pg==",
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-13.0.0.tgz",
"integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==",
"engines": {
"node": ">=14"
}
@@ -5998,34 +5998,34 @@
"dev": true
},
"node_modules/exiftool-vendored": {
"version": "24.4.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.4.0.tgz",
"integrity": "sha512-n9GjZ+t0sD4mFGyxVCuyVKkyc4wDQraGE9XE3TbWqJBResbIggMBwcYeo9Q5oVBXe2K8EA/WraWDTqHN+C+52g==",
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz",
"integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==",
"dependencies": {
"@photostructure/tz-lookup": "^9.0.0",
"@types/luxon": "^3.4.1",
"batch-cluster": "^12.1.0",
"@photostructure/tz-lookup": "^9.0.1",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"he": "^1.2.0",
"luxon": "^3.4.4"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.73.0",
"exiftool-vendored.pl": "12.73.0"
"exiftool-vendored.exe": "12.76.0",
"exiftool-vendored.pl": "12.76.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.73.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.73.0.tgz",
"integrity": "sha512-7za1Iv1hBnO92A+Yua04PHicCcwrP4edCzHaYnDOuea2DhmpIhqCgUUgrShcm4tG58ueRBa3GVvhRW/gCm8n4g==",
"version": "12.76.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz",
"integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==",
"optional": true,
"os": [
"win32"
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.73.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.73.0.tgz",
"integrity": "sha512-qX6kiGUTuQ/HFwuP3VJHU///BSwlaHSWm+yrDTHHQG+w+yvjFdtajDTJ96CUvPA/ecehtbTUMqCUz5xgMmHfBw==",
"version": "12.76.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz",
"integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA==",
"os": [
"!win32"
]
@@ -14280,9 +14280,9 @@
}
},
"@photostructure/tz-lookup": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.0.tgz",
"integrity": "sha512-gM3Xrs+XhD8ojDN0TgybuzSjsQb9UvF8j9DvR75E2zHlJQNiOztzILvfhVwadgA8JJbSMNzE+kYUnwP8aQnlXw=="
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.1.tgz",
"integrity": "sha512-inMfhc1QVKheq/PHF0v2vRPnPzTljxscuOKK95o3VlZA4T4w4DeYIpu7dC4W1EyjUhfZJlCBUudpmnFGNCqTog=="
},
"@pkgjs/parseargs": {
"version": "0.11.0",
@@ -14730,9 +14730,9 @@
}
},
"@types/node": {
"version": "20.11.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz",
"integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==",
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"requires": {
"undici-types": "~5.26.4"
}
@@ -15601,9 +15601,9 @@
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
},
"batch-cluster": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-12.1.0.tgz",
"integrity": "sha512-whGyJU4tr7kyg2USByu0/51mML5HsLAeNz5s03kMDYZNsQsGgDJgI47RdY3r7MciCjPkTaTD5O4eOVqOfEO7pg=="
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-13.0.0.tgz",
"integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og=="
},
"bcrypt": {
"version": "5.1.1",
@@ -16841,15 +16841,15 @@
}
},
"exiftool-vendored": {
"version": "24.4.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.4.0.tgz",
"integrity": "sha512-n9GjZ+t0sD4mFGyxVCuyVKkyc4wDQraGE9XE3TbWqJBResbIggMBwcYeo9Q5oVBXe2K8EA/WraWDTqHN+C+52g==",
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz",
"integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==",
"requires": {
"@photostructure/tz-lookup": "^9.0.0",
"@types/luxon": "^3.4.1",
"batch-cluster": "^12.1.0",
"exiftool-vendored.exe": "12.73.0",
"exiftool-vendored.pl": "12.73.0",
"@photostructure/tz-lookup": "^9.0.1",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"exiftool-vendored.exe": "12.76.0",
"exiftool-vendored.pl": "12.76.0",
"he": "^1.2.0",
"luxon": "^3.4.4"
},
@@ -16862,15 +16862,15 @@
}
},
"exiftool-vendored.exe": {
"version": "12.73.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.73.0.tgz",
"integrity": "sha512-7za1Iv1hBnO92A+Yua04PHicCcwrP4edCzHaYnDOuea2DhmpIhqCgUUgrShcm4tG58ueRBa3GVvhRW/gCm8n4g==",
"version": "12.76.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz",
"integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==",
"optional": true
},
"exiftool-vendored.pl": {
"version": "12.73.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.73.0.tgz",
"integrity": "sha512-qX6kiGUTuQ/HFwuP3VJHU///BSwlaHSWm+yrDTHHQG+w+yvjFdtajDTJ96CUvPA/ecehtbTUMqCUz5xgMmHfBw=="
"version": "12.76.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz",
"integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA=="
},
"exit": {
"version": "0.1.2",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.95.0",
"version": "1.95.1",
"description": "",
"author": "",
"private": true,
@@ -56,8 +56,8 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"exiftool-vendored": "~24.4.0",
"exiftool-vendored.pl": "12.73",
"exiftool-vendored": "~24.5.0",
"exiftool-vendored.pl": "12.76",
"fluent-ffmpeg": "^2.1.2",
"geo-tz": "^8.0.0",
"glob": "^10.3.3",
+1 -1
View File
@@ -326,7 +326,7 @@ export class AssetService {
const stackIdsToCheckForDelete: string[] = [];
if (removeParent) {
(options as Partial<AssetEntity>).stack = null;
const assets = await this.assetRepository.getByIds(ids);
const assets = await this.assetRepository.getByIds(ids, { stack: true });
stackIdsToCheckForDelete.push(...new Set(assets.filter((a) => !!a.stackId).map((a) => a.stackId!)));
// This updates the updatedAt column of the parents to indicate that one of its children is removed
// All the unique parent's -> parent is set to null
@@ -73,23 +73,21 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[]
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
const { stripMetadata = false, withStack = false } = options;
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
id: entity.id,
type: entity.type,
thumbhash: entity.thumbhash?.toString('base64') ?? null,
localDateTime: entity.localDateTime,
resized: !!entity.resizePath,
duration: entity.duration ?? '0:00:00.00000',
livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false,
};
if (stripMetadata) {
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
id: entity.id,
type: entity.type,
thumbhash: entity.thumbhash?.toString('base64') ?? null,
localDateTime: entity.localDateTime,
resized: !!entity.resizePath,
duration: entity.duration ?? '0:00:00.00000',
livePhotoVideoId: entity.livePhotoVideoId,
hasMetadata: false,
};
return sanitizedAssetResponse as AssetResponseDto;
}
return {
...sanitizedAssetResponse,
id: entity.id,
deviceAssetId: entity.deviceAssetId,
ownerId: entity.ownerId,
@@ -72,7 +72,7 @@ export class DatabaseService {
In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${this.vectorExt}' manually as a superuser.
See https://immich.app/docs/guides/database-queries for how to query the database.
Alternatively, if your Postgres instance has ${extName[otherExt]}, you may use this instead by setting the environment variable 'DB_VECTOR_EXTENSION=${otherExt}'.
Alternatively, if your Postgres instance has ${extName[otherExt]}, you may use this instead by setting the environment variable 'DB_VECTOR_EXTENSION=${extName[otherExt]}'.
Note that switching between the two extensions after a successful startup is not supported.
The exception is if your version of Immich prior to upgrading was 1.90.2 or earlier.
In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup.
+43 -39
View File
@@ -1,5 +1,6 @@
import {
AssetType,
AudioCodec,
Colorspace,
ExifEntity,
SystemConfigKey,
@@ -475,7 +476,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -542,7 +543,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -571,7 +572,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -629,7 +630,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -706,7 +707,10 @@ describe(MediaService.name, () => {
it('should copy video stream when video matches target', async () => {
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC }]);
configMock.load.mockResolvedValue([
{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC },
{ key: SystemConfigKey.FFMPEG_ACCEPTED_AUDIO_CODECS, value: [AudioCodec.AAC] },
]);
assetMock.getByIds.mockResolvedValue([assetStub.video]);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith(
@@ -770,7 +774,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -836,7 +840,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -868,7 +872,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -897,7 +901,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -928,7 +932,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v vp9',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -962,7 +966,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v vp9',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -994,7 +998,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v vp9',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1026,7 +1030,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v vp9',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1057,7 +1061,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v vp9',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1087,7 +1091,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1117,7 +1121,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1147,7 +1151,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v hevc',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1181,7 +1185,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v hevc',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1248,7 +1252,7 @@ describe(MediaService.name, () => {
'-rc-lookahead 20',
'-i_qfactor 0.75',
`-c:v h264_nvenc`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1286,7 +1290,7 @@ describe(MediaService.name, () => {
'-rc-lookahead 20',
'-i_qfactor 0.75',
`-c:v h264_nvenc`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1320,7 +1324,7 @@ describe(MediaService.name, () => {
'-rc-lookahead 20',
'-i_qfactor 0.75',
`-c:v h264_nvenc`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1355,7 +1359,7 @@ describe(MediaService.name, () => {
'-rc-lookahead 20',
'-i_qfactor 0.75',
`-c:v h264_nvenc`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1386,7 +1390,7 @@ describe(MediaService.name, () => {
'-rc-lookahead 20',
'-i_qfactor 0.75',
`-c:v h264_nvenc`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1418,7 +1422,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'],
outputOptions: [
`-c:v h264_qsv`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1455,7 +1459,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', '-filter_hw_device hw'],
outputOptions: [
`-c:v h264_qsv`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1491,7 +1495,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'],
outputOptions: [
`-c:v h264_qsv`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1524,7 +1528,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'],
outputOptions: [
`-c:v vp9_qsv`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1568,7 +1572,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
outputOptions: [
`-c:v h264_vaapi`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1600,7 +1604,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
outputOptions: [
`-c:v h264_vaapi`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1634,7 +1638,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
outputOptions: [
`-c:v h264_vaapi`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1664,7 +1668,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/card1', '-filter_hw_device accel'],
outputOptions: [
`-c:v h264_vaapi`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1690,7 +1694,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD129', '-filter_hw_device accel'],
outputOptions: [
`-c:v h264_vaapi`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1724,7 +1728,7 @@ describe(MediaService.name, () => {
inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'],
outputOptions: [
`-c:v h264_vaapi`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1757,7 +1761,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1798,7 +1802,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
`-c:v hevc_rkmpp_encoder`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1838,7 +1842,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
`-c:v h264_rkmpp_encoder`,
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1872,7 +1876,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1899,7 +1903,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -1926,7 +1930,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: [
'-c:v h264',
'-c:a aac',
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
'-map 0:0',
@@ -493,7 +493,7 @@ export class MetadataService {
model: tags.Model ?? null,
modifyDate: exifDate(tags.ModifyDate) ?? asset.fileModifiedAt,
orientation: validate(tags.Orientation)?.toString() ?? null,
profileDescription: tags.ProfileDescription || tags.ProfileName || null,
profileDescription: tags.ProfileDescription || null,
projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null,
timeZone: tags.tz ?? null,
};
+2 -1
View File
@@ -127,7 +127,8 @@ export class PersonStatisticsResponseDto {
export class PeopleResponseDto {
@ApiProperty({ type: 'integer' })
total!: number;
@ApiProperty({ type: 'integer' })
hidden!: number;
people!: PersonResponseDto[];
}
@@ -114,35 +114,12 @@ describe(PersonService.name, () => {
});
describe('getAll', () => {
it('should get all people with thumbnails', async () => {
personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.noThumbnail]);
personMock.getNumberOfPeople.mockResolvedValue(1);
await expect(sut.getAll(authStub.admin, { withHidden: undefined })).resolves.toEqual({
total: 1,
people: [responseDto],
});
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
minimumFaceCount: 3,
withHidden: false,
});
});
it('should get all visible people with thumbnails', async () => {
personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]);
personMock.getNumberOfPeople.mockResolvedValue(2);
await expect(sut.getAll(authStub.admin, { withHidden: false })).resolves.toEqual({
total: 2,
people: [responseDto],
});
expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, {
minimumFaceCount: 3,
withHidden: false,
});
});
it('should get all hidden and visible people with thumbnails', async () => {
personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]);
personMock.getNumberOfPeople.mockResolvedValue(2);
personMock.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 });
await expect(sut.getAll(authStub.admin, { withHidden: true })).resolves.toEqual({
total: 2,
hidden: 1,
people: [
responseDto,
{
+3 -6
View File
@@ -82,15 +82,12 @@ export class PersonService {
minimumFaceCount: machineLearning.facialRecognition.minFaces,
withHidden: dto.withHidden || false,
});
const total = await this.repository.getNumberOfPeople(auth.user.id);
const persons: PersonResponseDto[] = people
// with thumbnails
.filter((person) => !!person.thumbnailPath)
.map((person) => mapPerson(person));
const { total, hidden } = await this.repository.getNumberOfPeople(auth.user.id);
return {
people: persons.filter((person) => dto.withHidden || !person.isHidden),
people: people.map((person) => mapPerson(person)),
total,
hidden,
};
}
@@ -28,6 +28,11 @@ export interface PersonStatistics {
assets: number;
}
export interface PeopleStatistics {
total: number;
hidden: number;
}
export interface IPersonRepository {
getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
getAllForUser(userId: string, options: PersonSearchOptions): Promise<PersonEntity[]>;
@@ -54,7 +59,7 @@ export interface IPersonRepository {
getRandomFace(personId: string): Promise<AssetFaceEntity | null>;
getStatistics(personId: string): Promise<PersonStatistics>;
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
getNumberOfPeople(userId: string): Promise<number>;
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
reassignFaces(data: UpdateFacesData): Promise<number>;
update(entity: Partial<PersonEntity>): Promise<PersonEntity>;
}
@@ -157,7 +157,9 @@ type BaseAssetSearchOptions = SearchDateOptions &
export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions;
export type AssetSearchOneToOneRelationOptions = BaseAssetSearchOptions & SearchOneToOneRelationOptions;
export type AssetSearchOneToOneRelationOptions = BaseAssetSearchOptions &
SearchOneToOneRelationOptions &
SearchRelationOptions;
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
+4 -3
View File
@@ -23,6 +23,7 @@ class BaseSearchDto {
isArchived?: boolean;
@QueryBoolean({ optional: true })
@ApiProperty({ default: false })
withArchived?: boolean;
@QueryBoolean({ optional: true })
@@ -118,6 +119,9 @@ class BaseSearchDto {
@Type(() => Number)
@Optional()
size?: number;
@QueryBoolean({ optional: true })
isNotInAlbum?: boolean;
}
export class MetadataSearchDto extends BaseSearchDto {
@@ -170,9 +174,6 @@ export class MetadataSearchDto extends BaseSearchDto {
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
order?: AssetOrder;
@QueryBoolean({ optional: true })
isNotInAlbum?: boolean;
@Optional()
personIds?: string[];
}
@@ -33,7 +33,7 @@ export const defaults = Object.freeze<SystemConfig>({
targetVideoCodec: VideoCodec.H264,
acceptedVideoCodecs: [VideoCodec.H264],
targetAudioCodec: AudioCodec.AAC,
acceptedAudioCodecs: [AudioCodec.AAC],
acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS],
targetResolution: '720',
maxBitrate: '0',
bframes: -1,
@@ -43,7 +43,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
threads: 0,
preset: 'ultrafast',
targetAudioCodec: AudioCodec.AAC,
acceptedAudioCodecs: [AudioCodec.AAC],
acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS],
targetResolution: '720',
targetVideoCodec: VideoCodec.H264,
acceptedVideoCodecs: [VideoCodec.H264],
@@ -116,9 +116,17 @@ export class AssetService {
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
const assets = await this.assetRepository.getAllByFileCreationDate(
{ take: dto.take ?? 1000, skip: dto.skip },
{ ...dto, userIds: [userId], withDeleted: true, orderDirection: 'DESC', withExif: true, isVisible: true },
{
...dto,
userIds: [userId],
withDeleted: true,
orderDirection: 'DESC',
withExif: true,
isVisible: true,
withStacked: true,
},
);
return assets.items.map((asset) => mapAsset(asset));
return assets.items.map((asset) => mapAsset(asset, { withStack: true }));
}
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {
@@ -7,7 +7,7 @@ export class SystemConfigEntity<T = SystemConfigValue> {
key!: SystemConfigKey;
@Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } })
value!: T;
value!: T | T[];
}
export type SystemConfigValue = string | number | boolean;
+3 -3
View File
@@ -183,7 +183,7 @@ export function searchAssetBuilder(
_.omitBy(
{
...status,
isArchived: isArchived ?? withArchived,
isArchived: isArchived ?? (withArchived ? undefined : false),
encodedVideoPath: isEncoded ? Not(IsNull()) : undefined,
livePhotoVideoId: isMotion ? Not(IsNull()) : undefined,
},
@@ -213,9 +213,9 @@ export function searchAssetBuilder(
if (personIds && personIds.length > 0) {
builder
.leftJoin(`${builder.alias}.faces`, 'faces')
.andWhere('faces.personId IN (:...personIds)', { personIds: personIds })
.andWhere('faces.personId IN (:...personIds)', { personIds })
.addGroupBy(`${builder.alias}.id`)
.having('COUNT(faces.id) = :personCount', { personCount: personIds.length });
.having('COUNT(DISTINCT faces.personId) = :personCount', { personCount: personIds.length });
if (withExif) {
builder.addGroupBy('exifInfo.assetId');
@@ -4,11 +4,11 @@ export class AddVectorsToSearchPath1707000751533 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const res = await queryRunner.query(`SELECT current_database() as db`);
const databaseName = res[0]['db'];
await queryRunner.query(`ALTER DATABASE ${databaseName} SET search_path TO "$user", public, vectors`);
await queryRunner.query(`ALTER DATABASE "${databaseName}" SET search_path TO "$user", public, vectors`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const databaseName = await queryRunner.query(`SELECT current_database()`);
await queryRunner.query(`ALTER DATABASE ${databaseName} SET search_path TO "$user", public`);
await queryRunner.query(`ALTER DATABASE "${databaseName}" SET search_path TO "$user", public`);
}
}
@@ -3,6 +3,7 @@ import {
IPersonRepository,
Paginated,
PaginationOptions,
PeopleStatistics,
PersonNameSearchOptions,
PersonSearchOptions,
PersonStatistics,
@@ -69,6 +70,7 @@ export class PersonRepository implements IPersonRepository {
.addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC')
.addOrderBy('COUNT(face.assetId)', 'DESC')
.addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST')
.andWhere("person.thumbnailPath != ''")
.having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 })
.groupBy('person.id')
.limit(500);
@@ -207,15 +209,25 @@ export class PersonRepository implements IPersonRepository {
}
@GenerateSql({ params: [DummyValue.UUID] })
async getNumberOfPeople(userId: string): Promise<number> {
return this.personRepository
async getNumberOfPeople(userId: string): Promise<PeopleStatistics> {
const items = await this.personRepository
.createQueryBuilder('person')
.leftJoin('person.faces', 'face')
.where('person.ownerId = :userId', { userId })
.innerJoin('face.asset', 'asset')
.andWhere('asset.isArchived = false')
.andWhere("person.thumbnailPath != ''")
.select('COUNT(DISTINCT(person.id))', 'total')
.addSelect('COUNT(DISTINCT(person.id)) FILTER (WHERE person.isHidden = true)', 'hidden')
.having('COUNT(face.assetId) != 0')
.groupBy('person.id')
.withDeleted()
.getCount();
.getRawOne();
const result: PeopleStatistics = {
total: items ? Number.parseInt(items.total) : 0,
hidden: items ? Number.parseInt(items.hidden) : 0,
};
return result;
}
create(entity: Partial<PersonEntity>): Promise<PersonEntity> {
+1 -1
View File
@@ -434,7 +434,7 @@ WHERE
AND 1 = 1
AND "asset"."ownerId" IN ($2)
AND 1 = 1
AND 1 = 1
AND "asset"."isArchived" = $3
)
AND ("asset"."deletedAt" IS NULL)
ORDER BY
+10 -1
View File
@@ -26,6 +26,7 @@ FROM
WHERE
"person"."ownerId" = $1
AND "asset"."isArchived" = false
AND "person"."thumbnailPath" != ''
AND "person"."isHidden" = false
GROUP BY
"person"."id"
@@ -344,12 +345,20 @@ LIMIT
-- PersonRepository.getNumberOfPeople
SELECT
COUNT(DISTINCT ("person"."id")) AS "cnt"
COUNT(DISTINCT ("person"."id")) AS "total",
COUNT(DISTINCT ("person"."id")) FILTER (
WHERE
"person"."isHidden" = true
) AS "hidden"
FROM
"person" "person"
LEFT 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"."thumbnailPath" != ''
HAVING
COUNT("face"."assetId") != 0
+10 -4
View File
@@ -79,7 +79,10 @@ FROM
AND "exifInfo"."lensModel" = $2
AND 1 = 1
AND 1 = 1
AND "asset"."isFavorite" = $3
AND (
"asset"."isFavorite" = $3
AND "asset"."isArchived" = $4
)
AND (
"stack"."primaryAssetId" = "asset"."id"
OR "asset"."stackId" IS NULL
@@ -177,16 +180,19 @@ WHERE
AND "exifInfo"."lensModel" = $2
AND 1 = 1
AND 1 = 1
AND "asset"."isFavorite" = $3
AND (
"asset"."isFavorite" = $3
AND "asset"."isArchived" = $4
)
AND (
"stack"."primaryAssetId" = "asset"."id"
OR "asset"."stackId" IS NULL
)
AND "asset"."ownerId" IN ($4)
AND "asset"."ownerId" IN ($5)
)
AND ("asset"."deletedAt" IS NULL)
ORDER BY
"search"."embedding" <= > $5 ASC
"search"."embedding" <= > $6 ASC
LIMIT
101
COMMIT
+5 -5
View File
@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.1.0",
"version": "1.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.1.0",
"version": "1.1.1",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk",
@@ -8733,9 +8733,9 @@
}
},
"node_modules/vite": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.2.tgz",
"integrity": "sha512-uwiFebQbTWRIGbCaTEBVAfKqgqKNKMJ2uPXsXeLIZxM8MVMjoS3j0cG8NrPxdDIadaWnPSjrkLWffLSC+uiP3Q==",
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
"integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
"dev": true,
"dependencies": {
"esbuild": "^0.19.3",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.1.0",
"version": "1.1.1",
"license": "GNU Affero General Public License version 3",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",
@@ -14,12 +14,14 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingAccordion from '../setting-accordion.svelte';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingCheckboxes from '../setting-checkboxes.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSelect from '../setting-select.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -90,7 +92,10 @@
]}
name="acodec"
isEdited={config.ffmpeg.targetAudioCodec !== savedConfig.ffmpeg.targetAudioCodec}
on:select={() => (config.ffmpeg.acceptedAudioCodecs = [config.ffmpeg.targetAudioCodec])}
on:select={() =>
config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)
? null
: config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec)}
/>
<SettingCheckboxes
@@ -5,8 +5,10 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -4,10 +4,12 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingAccordion from '../setting-accordion.svelte';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -4,9 +4,9 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingSelect from '../setting-select.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -4,11 +4,13 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingAccordion from '../setting-accordion.svelte';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSelect from '../setting-select.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -4,10 +4,12 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingAccordion from '../setting-accordion.svelte';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -4,8 +4,8 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -5,9 +5,11 @@
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -5,8 +5,8 @@
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -1,11 +1,13 @@
<script lang="ts">
import SettingButtonsRow from '$lib/components/admin-page/settings/setting-buttons-row.svelte';
import type { SystemConfigDto } from '@immich/sdk';
import { isEqual } from 'lodash-es';
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -13,11 +13,13 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SupportedDatetimePanel from './supported-datetime-panel.svelte';
import SupportedVariablesPanel from './supported-variables-panel.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -1,11 +1,12 @@
<script lang="ts">
import { locale } from '$lib/stores/preferences.store';
import type { SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
import * as luxon from 'luxon';
import { DateTime } from 'luxon';
export let options: SystemConfigTemplateStorageOptionDto;
const getLuxonExample = (format: string) => {
return luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()).toFormat(format);
return DateTime.fromISO('2022-09-04T20:03:05.250Z', { locale: $locale }).toFormat(format);
};
</script>
@@ -4,8 +4,8 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingTextarea from '../setting-textarea.svelte';
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -1,13 +1,16 @@
<script lang="ts">
import SettingButtonsRow from '$lib/components/admin-page/settings/setting-buttons-row.svelte';
import SettingSelect from '$lib/components/admin-page/settings/setting-select.svelte';
import { Colorspace, type SystemConfigDto } from '@immich/sdk';
import { isEqual } from 'lodash-es';
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -4,9 +4,11 @@
import { createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition';
import type { SettingsEventType } from '../admin-settings';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
import SettingSwitch from '../setting-switch.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
@@ -3,10 +3,11 @@
import type { AlbumResponseDto, UserResponseDto } from '@immich/sdk';
import { mdiClose, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import SettingSwitch from '../admin-page/settings/setting-switch.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
export let album: AlbumResponseDto;
export let user: UserResponseDto;

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