mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
Merge branch 'main' of github.com:immich-app/immich into refactor/immich-thumbnail
This commit is contained in:
commit
4532db552e
238 changed files with 4470 additions and 3278 deletions
|
@ -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>
|
||||
|
|
24
cli/package-lock.json
generated
24
cli/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -45,21 +45,12 @@ Below is an example config for Apache2 site configuration.
|
|||
```
|
||||
<VirtualHost *:80>
|
||||
ServerName <snip>
|
||||
ProxyRequests Off
|
||||
ProxyPass / http://127.0.0.1:2283/ timeout=600 upgrade=websocket
|
||||
ProxyPassReverse / http://127.0.0.1:2283/
|
||||
ProxyPreserveHost On
|
||||
|
||||
ProxyRequests off
|
||||
ProxyVia on
|
||||
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_URI} ^/api/socket.io [NC]
|
||||
RewriteCond %{QUERY_STRING} transport=websocket [NC]
|
||||
RewriteRule /(.*) ws://localhost:2283/$1 [P,L]
|
||||
|
||||
ProxyPass /api/socket.io ws://localhost:2283/api/socket.io
|
||||
ProxyPassReverse /api/socket.io ws://localhost:2283/api/socket.io
|
||||
|
||||
<Location />
|
||||
ProxyPass http://localhost:2283/
|
||||
ProxyPassReverse http://localhost:2283/
|
||||
</Location>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
**timeout:** is measured in seconds, and it is particularly useful when long operations are triggered (i.e. Repair), so the server doesn't return an error.
|
||||
|
|
|
@ -4,6 +4,7 @@ name: immich-e2e
|
|||
|
||||
x-server-build: &server-common
|
||||
image: immich-server:latest
|
||||
container_name: immich-e2e-server
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
|
|
89
e2e/package-lock.json
generated
89
e2e/package-lock.json
generated
|
@ -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"
|
||||
|
@ -899,9 +904,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.0.tgz",
|
||||
"integrity": "sha512-e5Y5uK5NNoQMQaNitGQQjo9FoA5ZNcu7Bn6pH+dxUf48u6po1cX38kFBYUHZ9GNVkF4JLbncE0WeWwTw+nLrxg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz",
|
||||
"integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
|
@ -922,17 +927,17 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "1.3.0"
|
||||
"vitest": "1.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.0.tgz",
|
||||
"integrity": "sha512-7bWt0vBTZj08B+Ikv70AnLRicohYwFgzNjFqo9SxxqHHxSlUJGSXmCRORhOnRMisiUryKMdvsi1n27Bc6jL9DQ==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz",
|
||||
"integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/spy": "1.3.0",
|
||||
"@vitest/utils": "1.3.0",
|
||||
"@vitest/spy": "1.3.1",
|
||||
"@vitest/utils": "1.3.1",
|
||||
"chai": "^4.3.10"
|
||||
},
|
||||
"funding": {
|
||||
|
@ -940,12 +945,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.0.tgz",
|
||||
"integrity": "sha512-1Jb15Vo/Oy7mwZ5bXi7zbgszsdIBNjc4IqP8Jpr/8RdBC4nF1CTzIAn2dxYvpF1nGSseeL39lfLQ2uvs5u1Y9A==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz",
|
||||
"integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "1.3.0",
|
||||
"@vitest/utils": "1.3.1",
|
||||
"p-limit": "^5.0.0",
|
||||
"pathe": "^1.1.1"
|
||||
},
|
||||
|
@ -954,9 +959,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.0.tgz",
|
||||
"integrity": "sha512-swmktcviVVPYx9U4SEQXLV6AEY51Y6bZ14jA2yo6TgMxQ3h+ZYiO0YhAHGJNp0ohCFbPAis1R9kK0cvN6lDPQA==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz",
|
||||
"integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"magic-string": "^0.30.5",
|
||||
|
@ -968,9 +973,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.0.tgz",
|
||||
"integrity": "sha512-AkCU0ThZunMvblDpPKgjIi025UxR8V7MZ/g/EwmAGpjIujLVV2X6rGYGmxE2D4FJbAy0/ijdROHMWa2M/6JVMw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz",
|
||||
"integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tinyspy": "^2.2.0"
|
||||
|
@ -980,9 +985,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.0.tgz",
|
||||
"integrity": "sha512-/LibEY/fkaXQufi4GDlQZhikQsPO2entBKtfuyIpr1jV4DpaeasqkeHjhdOhU24vSHshcSuEyVlWdzvv2XmYCw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz",
|
||||
"integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"diff-sequences": "^29.6.3",
|
||||
|
@ -2546,9 +2551,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
|
||||
"integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
|
||||
"integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.19.3",
|
||||
|
@ -2601,9 +2606,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.0.tgz",
|
||||
"integrity": "sha512-D/oiDVBw75XMnjAXne/4feCkCEwcbr2SU1bjAhCcfI5Bq3VoOHji8/wCPAfUkDIeohJ5nSZ39fNxM3dNZ6OBOA==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz",
|
||||
"integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
|
@ -2637,16 +2642,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.0.tgz",
|
||||
"integrity": "sha512-V9qb276J1jjSx9xb75T2VoYXdO1UKi+qfflY7V7w93jzX7oA/+RtYE6TcifxksxsZvygSSMwu2Uw6di7yqDMwg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz",
|
||||
"integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "1.3.0",
|
||||
"@vitest/runner": "1.3.0",
|
||||
"@vitest/snapshot": "1.3.0",
|
||||
"@vitest/spy": "1.3.0",
|
||||
"@vitest/utils": "1.3.0",
|
||||
"@vitest/expect": "1.3.1",
|
||||
"@vitest/runner": "1.3.1",
|
||||
"@vitest/snapshot": "1.3.1",
|
||||
"@vitest/spy": "1.3.1",
|
||||
"@vitest/utils": "1.3.1",
|
||||
"acorn-walk": "^8.3.2",
|
||||
"chai": "^4.3.10",
|
||||
"debug": "^4.3.4",
|
||||
|
@ -2660,7 +2665,7 @@
|
|||
"tinybench": "^2.5.1",
|
||||
"tinypool": "^0.8.2",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "1.3.0",
|
||||
"vite-node": "1.3.1",
|
||||
"why-is-node-running": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
|
@ -2675,8 +2680,8 @@
|
|||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"@vitest/browser": "1.3.0",
|
||||
"@vitest/ui": "1.3.0",
|
||||
"@vitest/browser": "1.3.1",
|
||||
"@vitest/ui": "1.3.1",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
|
|
|
@ -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, {
|
||||
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],
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
},
|
||||
},
|
||||
{ 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, {
|
||||
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, {
|
||||
const [reaction] = await Promise.all([
|
||||
createActivity({
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
type: ReactionType.LIKE,
|
||||
});
|
||||
const { status, body } = await request(server)
|
||||
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, {
|
||||
const [reaction] = await Promise.all([
|
||||
createActivity({
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
type: ReactionType.LIKE,
|
||||
});
|
||||
const { status, body } = await request(server)
|
||||
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, {
|
||||
const reaction = await createActivity(
|
||||
{
|
||||
albumId: album.id,
|
||||
type: ReactionType.COMMENT,
|
||||
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'));
|
||||
});
|
||||
});
|
||||
});
|
51
e2e/src/api/specs/audit.e2e-spec.ts
Normal file
51
e2e/src/api/specs/audit.e2e-spec.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
deleteAssets,
|
||||
getAuditFiles,
|
||||
updateAsset,
|
||||
type LoginResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/audit', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
apiUtils.setup();
|
||||
await dbUtils.reset();
|
||||
await fileUtils.reset();
|
||||
|
||||
admin = await apiUtils.adminSetup();
|
||||
});
|
||||
|
||||
describe('GET :/file-report', () => {
|
||||
it('excludes assets without issues from report', async () => {
|
||||
const [trashedAsset, archivedAsset, _] = await Promise.all([
|
||||
apiUtils.createAsset(admin.accessToken),
|
||||
apiUtils.createAsset(admin.accessToken),
|
||||
apiUtils.createAsset(admin.accessToken),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
deleteAssets(
|
||||
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
),
|
||||
updateAsset(
|
||||
{
|
||||
id: archivedAsset.id,
|
||||
updateAssetDto: { isArchived: true },
|
||||
},
|
||||
{ headers: asBearerAuth(admin.accessToken) }
|
||||
),
|
||||
]);
|
||||
|
||||
const body = await getAuditFiles({
|
||||
headers: asBearerAuth(admin.accessToken),
|
||||
});
|
||||
|
||||
expect(body.orphans).toHaveLength(0);
|
||||
expect(body.extras).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
178
e2e/src/api/specs/person.e2e-spec.ts
Normal file
178
e2e/src/api/specs/person.e2e-spec.ts
Normal 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,
|
||||
[
|
||||
linkWithDeletedAlbum,
|
||||
linkWithAlbum,
|
||||
linkWithAssets,
|
||||
linkWithPassword,
|
||||
linkWithMetadata,
|
||||
linkWithoutMetadata,
|
||||
] = await Promise.all([
|
||||
apiUtils.createSharedLink(user2.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: deletedAlbum.id,
|
||||
}),
|
||||
api.sharedLinkApi.create(server, user1.accessToken, {
|
||||
type: SharedLinkType.ALBUM,
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
}),
|
||||
api.sharedLinkApi.create(server, user1.accessToken, {
|
||||
type: SharedLinkType.INDIVIDUAL,
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset1.id],
|
||||
}),
|
||||
api.sharedLinkApi.create(server, user1.accessToken, {
|
||||
type: SharedLinkType.ALBUM,
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: album.id,
|
||||
password: 'foo',
|
||||
}),
|
||||
api.sharedLinkApi.create(server, user1.accessToken, {
|
||||
type: SharedLinkType.ALBUM,
|
||||
apiUtils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
albumId: metadataAlbum.id,
|
||||
showMetadata: true,
|
||||
}),
|
||||
api.sharedLinkApi.create(server, user1.accessToken, {
|
||||
type: SharedLinkType.ALBUM,
|
||||
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}`);
|
||||
|
|
@ -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);
|
||||
|
|
|
@ -1,24 +1,33 @@
|
|||
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';
|
||||
import { exec, spawn } from 'child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { access } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
import pg from 'pg';
|
||||
import { loginDto, signupDto } from 'src/fixtures';
|
||||
import request from 'supertest';
|
||||
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
export const app = 'http://127.0.0.1:2283/api';
|
||||
|
||||
const directoryExists = (directory: string) =>
|
||||
|
@ -29,6 +38,9 @@ const directoryExists = (directory: string) =>
|
|||
// TODO move test assets into e2e/assets
|
||||
export const testAssetDir = path.resolve(`./../server/test/assets/`);
|
||||
|
||||
const serverContainerName = 'immich-e2e-server';
|
||||
const uploadMediaDir = '/usr/src/app/upload/upload';
|
||||
|
||||
if (!(await directoryExists(`${testAssetDir}/albums`))) {
|
||||
throw new Error(
|
||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`
|
||||
|
@ -44,8 +56,45 @@ export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
|
|||
|
||||
let client: pg.Client | null = null;
|
||||
|
||||
export const dbUtils = {
|
||||
export const fileUtils = {
|
||||
reset: async () => {
|
||||
await execPromise(
|
||||
`docker exec -i "${serverContainerName}" rm -R "${uploadMediaDir}"`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const dbUtils = {
|
||||
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 +103,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 +199,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 +225,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 = {
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
from .ann import Ann, is_available
|
|
@ -32,8 +32,7 @@ T = TypeVar("T", covariant=True)
|
|||
|
||||
|
||||
class Newable(Protocol[T]):
|
||||
def new(self) -> None:
|
||||
...
|
||||
def new(self) -> None: ...
|
||||
|
||||
|
||||
class _Singleton(type, Newable[T]):
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from typing import Any
|
||||
|
||||
import onnx
|
||||
import onnxruntime as ort
|
||||
from huggingface_hub import snapshot_download
|
||||
from onnx.shape_inference import infer_shapes
|
||||
from onnx.tools.update_model_dims import update_inputs_outputs_dims
|
||||
|
||||
import ann.ann
|
||||
from app.models.constants import STATIC_INPUT_PROVIDERS, SUPPORTED_PROVIDERS
|
||||
from app.models.constants import SUPPORTED_PROVIDERS
|
||||
|
||||
from ..config import get_cache_dir, get_hf_model_name, log, settings
|
||||
from ..schemas import ModelRuntime, ModelType
|
||||
|
@ -113,63 +111,25 @@ class InferenceModel(ABC):
|
|||
)
|
||||
model_path = onnx_path
|
||||
|
||||
if any(provider in STATIC_INPUT_PROVIDERS for provider in self.providers):
|
||||
static_path = model_path.parent / "static_1" / "model.onnx"
|
||||
static_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if not static_path.is_file():
|
||||
self._convert_to_static(model_path, static_path)
|
||||
model_path = static_path
|
||||
|
||||
match model_path.suffix:
|
||||
case ".armnn":
|
||||
session = AnnSession(model_path)
|
||||
case ".onnx":
|
||||
cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(model_path.parent)
|
||||
session = ort.InferenceSession(
|
||||
model_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
case _:
|
||||
raise ValueError(f"Unsupported model file type: {model_path.suffix}")
|
||||
return session
|
||||
|
||||
def _convert_to_static(self, source_path: Path, target_path: Path) -> None:
|
||||
inferred = infer_shapes(onnx.load(source_path))
|
||||
inputs = self._get_static_dims(inferred.graph.input)
|
||||
outputs = self._get_static_dims(inferred.graph.output)
|
||||
|
||||
# check_model gets called in update_inputs_outputs_dims and doesn't work for large models
|
||||
check_model = onnx.checker.check_model
|
||||
try:
|
||||
|
||||
def check_model_stub(*args: Any, **kwargs: Any) -> None:
|
||||
pass
|
||||
|
||||
onnx.checker.check_model = check_model_stub
|
||||
updated_model = update_inputs_outputs_dims(inferred, inputs, outputs)
|
||||
finally:
|
||||
onnx.checker.check_model = check_model
|
||||
|
||||
onnx.save(
|
||||
updated_model,
|
||||
target_path,
|
||||
save_as_external_data=True,
|
||||
all_tensors_to_one_file=False,
|
||||
size_threshold=1048576,
|
||||
)
|
||||
|
||||
def _get_static_dims(self, graph_io: Any, dim_size: int = 1) -> dict[str, list[int]]:
|
||||
return {
|
||||
field.name: [
|
||||
d.dim_value if d.HasField("dim_value") else dim_size
|
||||
for shape in field.type.ListFields()
|
||||
if (dim := shape[1].shape.dim)
|
||||
for d in dim
|
||||
]
|
||||
for field in graph_io
|
||||
}
|
||||
|
||||
@property
|
||||
def model_type(self) -> ModelType:
|
||||
return self._model_type
|
||||
|
|
|
@ -54,9 +54,6 @@ _INSIGHTFACE_MODELS = {
|
|||
SUPPORTED_PROVIDERS = ["CUDAExecutionProvider", "OpenVINOExecutionProvider", "CPUExecutionProvider"]
|
||||
|
||||
|
||||
STATIC_INPUT_PROVIDERS = ["OpenVINOExecutionProvider"]
|
||||
|
||||
|
||||
def is_openclip(model_name: str) -> bool:
|
||||
return clean_name(model_name) in _OPENCLIP_MODELS
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from random import randint
|
||||
|
@ -237,12 +238,12 @@ class TestBase:
|
|||
mock_model_path.is_file.return_value = True
|
||||
mock_model_path.suffix = ".armnn"
|
||||
mock_model_path.with_suffix.return_value = mock_model_path
|
||||
mock_session = mocker.patch("app.models.base.AnnSession")
|
||||
mock_ann = mocker.patch("app.models.base.AnnSession")
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
encoder._make_session(mock_model_path)
|
||||
|
||||
mock_session.assert_called_once()
|
||||
mock_ann.assert_called_once()
|
||||
|
||||
def test_make_session_return_ort_if_available_and_ann_is_not(self, mocker: MockerFixture) -> None:
|
||||
mock_armnn_path = mocker.Mock()
|
||||
|
@ -256,6 +257,7 @@ class TestBase:
|
|||
|
||||
mock_ann = mocker.patch("app.models.base.AnnSession")
|
||||
mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
|
||||
mocker.patch("app.models.base.os.chdir")
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
encoder._make_session(mock_armnn_path)
|
||||
|
@ -278,6 +280,26 @@ class TestBase:
|
|||
mock_ann.assert_not_called()
|
||||
mock_ort.assert_not_called()
|
||||
|
||||
def test_make_session_changes_cwd(self, mocker: MockerFixture) -> None:
|
||||
mock_model_path = mocker.Mock()
|
||||
mock_model_path.is_file.return_value = True
|
||||
mock_model_path.suffix = ".onnx"
|
||||
mock_model_path.parent = "model_parent"
|
||||
mock_model_path.with_suffix.return_value = mock_model_path
|
||||
mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
|
||||
mock_chdir = mocker.patch("app.models.base.os.chdir")
|
||||
|
||||
encoder = OpenCLIPEncoder("ViT-B-32__openai")
|
||||
encoder._make_session(mock_model_path)
|
||||
|
||||
mock_chdir.assert_has_calls(
|
||||
[
|
||||
mock.call(mock_model_path.parent),
|
||||
mock.call(os.getcwd()),
|
||||
]
|
||||
)
|
||||
mock_ort.assert_called_once()
|
||||
|
||||
def test_download(self, mocker: MockerFixture) -> None:
|
||||
mock_snapshot_download = mocker.patch("app.models.base.snapshot_download")
|
||||
|
||||
|
|
376
machine-learning/poetry.lock
generated
376
machine-learning/poetry.lock
generated
|
@ -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,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"
|
||||
|
@ -82,10 +82,10 @@ warn_untyped_fields = true
|
|||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py311"
|
||||
select = ["E", "F", "I"]
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"test_main.py" = ["F403"]
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
per-file-ignores = { "test_main.py" = ["F403"] }
|
||||
|
||||
[tool.black]
|
||||
line-length = 120
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -30,7 +30,7 @@ extension LogOnError<T> on AsyncValue<T> {
|
|||
}
|
||||
|
||||
if (hasError && !hasValue) {
|
||||
_asyncErrorLogger.severe("$error", error, stackTrace);
|
||||
_asyncErrorLogger.severe('Could not load value', error, stackTrace);
|
||||
return onError?.call(error, stackTrace) ??
|
||||
ScaffoldErrorBody(errorMsg: error?.toString());
|
||||
}
|
||||
|
|
5
mobile/lib/extensions/response_extensions.dart
Normal file
5
mobile/lib/extensions/response_extensions.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
import 'package:http/http.dart';
|
||||
|
||||
extension LoggerExtension on Response {
|
||||
String toLoggerString() => "Status: $statusCode $reasonPhrase\n\n$body";
|
||||
}
|
|
@ -73,15 +73,14 @@ Future<void> initApp() async {
|
|||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
log.severe(
|
||||
'FlutterError - Catch all error: ${details.toString()} - ${details.exception} - ${details.library} - ${details.context} - ${details.stack}',
|
||||
details,
|
||||
'FlutterError - Catch all',
|
||||
"${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}",
|
||||
details.stack,
|
||||
);
|
||||
};
|
||||
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
log.severe('PlatformDispatcher - Catch all error: $error', error, stack);
|
||||
debugPrint("PlatformDispatcher - Catch all error: $error $stack");
|
||||
log.severe('PlatformDispatcher - Catch all', error, stack);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
|
@ -10,13 +10,14 @@ mixin ErrorLoggerMixin {
|
|||
/// Else, logs the error to the overrided logger and returns an AsyncError<>
|
||||
AsyncFuture<T> guardError<T>(
|
||||
Future<T> Function() fn, {
|
||||
required String errorMessage,
|
||||
Level logLevel = Level.SEVERE,
|
||||
}) async {
|
||||
try {
|
||||
final result = await fn();
|
||||
return AsyncData(result);
|
||||
} catch (error, stackTrace) {
|
||||
logger.log(logLevel, "$error", error, stackTrace);
|
||||
logger.log(logLevel, errorMessage, error, stackTrace);
|
||||
return AsyncError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
@ -26,12 +27,13 @@ mixin ErrorLoggerMixin {
|
|||
Future<T> logError<T>(
|
||||
Future<T> Function() fn, {
|
||||
required T defaultValue,
|
||||
required String errorMessage,
|
||||
Level logLevel = Level.SEVERE,
|
||||
}) async {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error, stackTrace) {
|
||||
logger.log(logLevel, "$error", error, stackTrace);
|
||||
logger.log(logLevel, errorMessage, error, stackTrace);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ class ActivityService with ErrorLoggerMixin {
|
|||
return list != null ? list.map(Activity.fromDto).toList() : [];
|
||||
},
|
||||
defaultValue: [],
|
||||
errorMessage: "Failed to get all activities for album $albumId",
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -35,6 +36,7 @@ class ActivityService with ErrorLoggerMixin {
|
|||
return dto?.comments ?? 0;
|
||||
},
|
||||
defaultValue: 0,
|
||||
errorMessage: "Failed to statistics for album $albumId",
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -45,6 +47,7 @@ class ActivityService with ErrorLoggerMixin {
|
|||
return true;
|
||||
},
|
||||
defaultValue: false,
|
||||
errorMessage: "Failed to delete activity",
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -54,7 +57,8 @@ class ActivityService with ErrorLoggerMixin {
|
|||
String? assetId,
|
||||
String? comment,
|
||||
}) async {
|
||||
return guardError(() async {
|
||||
return guardError(
|
||||
() async {
|
||||
final dto = await _apiService.activityApi.createActivity(
|
||||
ActivityCreateDto(
|
||||
albumId: albumId,
|
||||
|
@ -69,6 +73,8 @@ class ActivityService with ErrorLoggerMixin {
|
|||
return Activity.fromDto(dto);
|
||||
}
|
||||
throw NoResponseDtoError();
|
||||
});
|
||||
},
|
||||
errorMessage: "Failed to create $type for album $albumId",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,179 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart' as store;
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
/// Provides the initialized video player controller
|
||||
/// If the asset is local, use the local file
|
||||
/// Otherwise, use a video player with a URL
|
||||
ChewieController? useChewieController(
|
||||
Asset asset, {
|
||||
EdgeInsets controlsSafeAreaMinimum = const EdgeInsets.only(
|
||||
bottom: 100,
|
||||
),
|
||||
bool showOptions = true,
|
||||
bool showControlsOnInitialize = false,
|
||||
bool autoPlay = true,
|
||||
bool autoInitialize = true,
|
||||
bool allowFullScreen = false,
|
||||
bool allowedScreenSleep = false,
|
||||
bool showControls = true,
|
||||
Widget? customControls,
|
||||
Widget? placeholder,
|
||||
Duration hideControlsTimer = const Duration(seconds: 1),
|
||||
VoidCallback? onPlaying,
|
||||
VoidCallback? onPaused,
|
||||
VoidCallback? onVideoEnded,
|
||||
}) {
|
||||
return use(
|
||||
_ChewieControllerHook(
|
||||
asset: asset,
|
||||
placeholder: placeholder,
|
||||
showOptions: showOptions,
|
||||
controlsSafeAreaMinimum: controlsSafeAreaMinimum,
|
||||
autoPlay: autoPlay,
|
||||
allowFullScreen: allowFullScreen,
|
||||
customControls: customControls,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showControlsOnInitialize: showControlsOnInitialize,
|
||||
showControls: showControls,
|
||||
autoInitialize: autoInitialize,
|
||||
allowedScreenSleep: allowedScreenSleep,
|
||||
onPlaying: onPlaying,
|
||||
onPaused: onPaused,
|
||||
onVideoEnded: onVideoEnded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _ChewieControllerHook extends Hook<ChewieController?> {
|
||||
final Asset asset;
|
||||
final EdgeInsets controlsSafeAreaMinimum;
|
||||
final bool showOptions;
|
||||
final bool showControlsOnInitialize;
|
||||
final bool autoPlay;
|
||||
final bool autoInitialize;
|
||||
final bool allowFullScreen;
|
||||
final bool allowedScreenSleep;
|
||||
final bool showControls;
|
||||
final Widget? customControls;
|
||||
final Widget? placeholder;
|
||||
final Duration hideControlsTimer;
|
||||
final VoidCallback? onPlaying;
|
||||
final VoidCallback? onPaused;
|
||||
final VoidCallback? onVideoEnded;
|
||||
|
||||
const _ChewieControllerHook({
|
||||
required this.asset,
|
||||
this.controlsSafeAreaMinimum = const EdgeInsets.only(
|
||||
bottom: 100,
|
||||
),
|
||||
this.showOptions = true,
|
||||
this.showControlsOnInitialize = false,
|
||||
this.autoPlay = true,
|
||||
this.autoInitialize = true,
|
||||
this.allowFullScreen = false,
|
||||
this.allowedScreenSleep = false,
|
||||
this.showControls = true,
|
||||
this.customControls,
|
||||
this.placeholder,
|
||||
this.hideControlsTimer = const Duration(seconds: 3),
|
||||
this.onPlaying,
|
||||
this.onPaused,
|
||||
this.onVideoEnded,
|
||||
});
|
||||
|
||||
@override
|
||||
createState() => _ChewieControllerHookState();
|
||||
}
|
||||
|
||||
class _ChewieControllerHookState
|
||||
extends HookState<ChewieController?, _ChewieControllerHook> {
|
||||
ChewieController? chewieController;
|
||||
VideoPlayerController? videoPlayerController;
|
||||
|
||||
@override
|
||||
void initHook() async {
|
||||
super.initHook();
|
||||
unawaited(_initialize());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
chewieController?.dispose();
|
||||
videoPlayerController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
ChewieController? build(BuildContext context) {
|
||||
return chewieController;
|
||||
}
|
||||
|
||||
/// Initializes the chewie controller and video player controller
|
||||
Future<void> _initialize() async {
|
||||
if (hook.asset.isLocal && hook.asset.livePhotoVideoId == null) {
|
||||
// Use a local file for the video player controller
|
||||
final file = await hook.asset.local!.file;
|
||||
if (file == null) {
|
||||
throw Exception('No file found for the video');
|
||||
}
|
||||
videoPlayerController = VideoPlayerController.file(file);
|
||||
} else {
|
||||
// Use a network URL for the video player controller
|
||||
final serverEndpoint = store.Store.get(store.StoreKey.serverEndpoint);
|
||||
final String videoUrl = hook.asset.livePhotoVideoId != null
|
||||
? '$serverEndpoint/asset/file/${hook.asset.livePhotoVideoId}'
|
||||
: '$serverEndpoint/asset/file/${hook.asset.remoteId}';
|
||||
|
||||
final url = Uri.parse(videoUrl);
|
||||
final accessToken = store.Store.get(StoreKey.accessToken);
|
||||
|
||||
videoPlayerController = VideoPlayerController.networkUrl(
|
||||
url,
|
||||
httpHeaders: {"x-immich-user-token": accessToken},
|
||||
);
|
||||
}
|
||||
|
||||
videoPlayerController!.addListener(() {
|
||||
final value = videoPlayerController!.value;
|
||||
if (value.isPlaying) {
|
||||
WakelockPlus.enable();
|
||||
hook.onPlaying?.call();
|
||||
} else if (!value.isPlaying) {
|
||||
WakelockPlus.disable();
|
||||
hook.onPaused?.call();
|
||||
}
|
||||
|
||||
if (value.position == value.duration) {
|
||||
WakelockPlus.disable();
|
||||
hook.onVideoEnded?.call();
|
||||
}
|
||||
});
|
||||
|
||||
await videoPlayerController!.initialize();
|
||||
|
||||
setState(() {
|
||||
chewieController = ChewieController(
|
||||
videoPlayerController: videoPlayerController!,
|
||||
controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum,
|
||||
showOptions: hook.showOptions,
|
||||
showControlsOnInitialize: hook.showControlsOnInitialize,
|
||||
autoPlay: hook.autoPlay,
|
||||
autoInitialize: hook.autoInitialize,
|
||||
allowFullScreen: hook.allowFullScreen,
|
||||
allowedScreenSleep: hook.allowedScreenSleep,
|
||||
showControls: hook.showControls,
|
||||
customControls: hook.customControls,
|
||||
placeholder: hook.placeholder,
|
||||
hideControlsTimer: hook.hideControlsTimer,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
|
@ -39,7 +40,8 @@ class ImageViewerService {
|
|||
final failedResponse =
|
||||
imageResponse.statusCode != 200 ? imageResponse : motionReponse;
|
||||
_log.severe(
|
||||
"Motion asset download failed with status - ${failedResponse.statusCode} and response - ${failedResponse.body}",
|
||||
"Motion asset download failed",
|
||||
failedResponse.toLoggerString(),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
@ -75,9 +77,7 @@ class ImageViewerService {
|
|||
.downloadFileWithHttpInfo(asset.remoteId!);
|
||||
|
||||
if (res.statusCode != 200) {
|
||||
_log.severe(
|
||||
"Asset download failed with status - ${res.statusCode} and response - ${res.body}",
|
||||
);
|
||||
_log.severe("Asset download failed", res.toLoggerString());
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,7 @@ class ImageViewerService {
|
|||
return entity != null;
|
||||
}
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error saving file ${error.toString()}", error, stack);
|
||||
_log.severe("Error saving downloaded asset", error, stack);
|
||||
return false;
|
||||
} finally {
|
||||
// Clear temp files
|
||||
|
|
|
@ -48,7 +48,7 @@ class DescriptionInput extends HookConsumerWidget {
|
|||
);
|
||||
} catch (error, stack) {
|
||||
hasError.value = true;
|
||||
_log.severe("Error updating description $error", error, stack);
|
||||
_log.severe("Error updating description", error, stack);
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: "description_input_submit_error".tr(),
|
||||
|
|
|
@ -7,7 +7,7 @@ import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provi
|
|||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class VideoPlayerControls extends ConsumerStatefulWidget {
|
||||
|
@ -66,7 +66,9 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
|||
children: [
|
||||
if (_displayBufferingIndicator)
|
||||
const Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
child: DelayedLoadingIndicator(
|
||||
fadeInDuration: Duration(milliseconds: 400),
|
||||
),
|
||||
)
|
||||
else
|
||||
_buildHitArea(),
|
||||
|
@ -79,6 +81,7 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
|||
@override
|
||||
void dispose() {
|
||||
_dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
@ -92,6 +95,7 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
|||
final oldController = _chewieController;
|
||||
_chewieController = ChewieController.of(context);
|
||||
controller = chewieController.videoPlayerController;
|
||||
_latestValue = controller.value;
|
||||
|
||||
if (oldController != chewieController) {
|
||||
_dispose();
|
||||
|
@ -106,12 +110,10 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
|||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (_latestValue.isPlaying) {
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
} else {
|
||||
if (!_latestValue.isPlaying) {
|
||||
_playPause();
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
}
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
},
|
||||
child: CenterPlayButton(
|
||||
backgroundColor: Colors.black54,
|
||||
|
@ -131,10 +133,11 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
|||
}
|
||||
|
||||
Future<void> _initialize() async {
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
_mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute)));
|
||||
|
||||
controller.addListener(_updateState);
|
||||
_latestValue = controller.value;
|
||||
controller.addListener(_updateState);
|
||||
|
||||
if (controller.value.isPlaying || chewieController.autoPlay) {
|
||||
_startHideTimer();
|
||||
|
@ -167,9 +170,8 @@ class VideoPlayerControlsState extends ConsumerState<VideoPlayerControls>
|
|||
}
|
||||
|
||||
void _startHideTimer() {
|
||||
final hideControlsTimer = chewieController.hideControlsTimer.isNegative
|
||||
? ChewieController.defaultHideControlsTimer
|
||||
: chewieController.hideControlsTimer;
|
||||
final hideControlsTimer = chewieController.hideControlsTimer;
|
||||
_hideTimer?.cancel();
|
||||
_hideTimer = Timer(hideControlsTimer, () {
|
||||
ref.read(showControlsProvider.notifier).show = false;
|
||||
});
|
||||
|
|
|
@ -699,6 +699,18 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
);
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
if (ref.read(showControlsProvider)) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
} else {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
ref.listen(showControlsProvider, (_, show) {
|
||||
if (show) {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
|
@ -795,7 +807,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
minScale: 1.0,
|
||||
basePosition: Alignment.center,
|
||||
child: VideoViewerPage(
|
||||
onPlaying: () => isPlayingVideo.value = true,
|
||||
onPlaying: () {
|
||||
isPlayingVideo.value = true;
|
||||
},
|
||||
onPaused: () =>
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => isPlayingVideo.value = false,
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/hooks/chewiew_controller_hook.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
class VideoViewerPage extends HookConsumerWidget {
|
||||
class VideoViewerPage extends HookWidget {
|
||||
final Asset asset;
|
||||
final bool isMotionVideo;
|
||||
final Widget? placeholder;
|
||||
|
@ -42,211 +34,49 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (asset.isLocal && asset.livePhotoVideoId == null) {
|
||||
final AsyncValue<File> videoFile = ref.watch(_fileFamily(asset.local!));
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: videoFile.when(
|
||||
data: (data) => VideoPlayer(
|
||||
file: data,
|
||||
isMotionVideo: false,
|
||||
onVideoEnded: () {},
|
||||
),
|
||||
error: (error, stackTrace) => Icon(
|
||||
Icons.image_not_supported_outlined,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
loading: () => showDownloadingIndicator
|
||||
? const Center(child: ImmichLoadingIndicator())
|
||||
: Container(),
|
||||
),
|
||||
);
|
||||
}
|
||||
final downloadAssetStatus =
|
||||
ref.watch(imageViewerStateProvider).downloadAssetStatus;
|
||||
final String videoUrl = isMotionVideo
|
||||
? '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.livePhotoVideoId}'
|
||||
: '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}';
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
VideoPlayer(
|
||||
url: videoUrl,
|
||||
accessToken: Store.get(StoreKey.accessToken),
|
||||
isMotionVideo: isMotionVideo,
|
||||
onVideoEnded: onVideoEnded,
|
||||
onPaused: onPaused,
|
||||
onPlaying: onPlaying,
|
||||
placeholder: placeholder,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showControls: showControls,
|
||||
showDownloadingIndicator: showDownloadingIndicator,
|
||||
),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
opacity: (downloadAssetStatus == DownloadAssetStatus.loading &&
|
||||
showDownloadingIndicator)
|
||||
? 1.0
|
||||
: 0.0,
|
||||
child: SizedBox(
|
||||
height: context.height,
|
||||
width: context.width,
|
||||
child: const Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final _fileFamily =
|
||||
FutureProvider.family<File, AssetEntity>((ref, entity) async {
|
||||
final file = await entity.file;
|
||||
if (file == null) {
|
||||
throw Exception();
|
||||
}
|
||||
return file;
|
||||
});
|
||||
|
||||
class VideoPlayer extends StatefulWidget {
|
||||
final String? url;
|
||||
final String? accessToken;
|
||||
final File? file;
|
||||
final bool isMotionVideo;
|
||||
final VoidCallback? onVideoEnded;
|
||||
final Duration hideControlsTimer;
|
||||
final bool showControls;
|
||||
|
||||
final Function()? onPlaying;
|
||||
final Function()? onPaused;
|
||||
|
||||
/// The placeholder to show while the video is loading
|
||||
/// usually, a thumbnail of the video
|
||||
final Widget? placeholder;
|
||||
|
||||
final bool showDownloadingIndicator;
|
||||
|
||||
const VideoPlayer({
|
||||
super.key,
|
||||
this.url,
|
||||
this.accessToken,
|
||||
this.file,
|
||||
this.onVideoEnded,
|
||||
required this.isMotionVideo,
|
||||
this.onPlaying,
|
||||
this.onPaused,
|
||||
this.placeholder,
|
||||
this.hideControlsTimer = const Duration(
|
||||
seconds: 5,
|
||||
),
|
||||
this.showControls = true,
|
||||
this.showDownloadingIndicator = true,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VideoPlayer> createState() => _VideoPlayerState();
|
||||
}
|
||||
|
||||
class _VideoPlayerState extends State<VideoPlayer> {
|
||||
late VideoPlayerController videoPlayerController;
|
||||
ChewieController? chewieController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initializePlayer();
|
||||
|
||||
videoPlayerController.addListener(() {
|
||||
if (videoPlayerController.value.isInitialized) {
|
||||
if (videoPlayerController.value.isPlaying) {
|
||||
WakelockPlus.enable();
|
||||
widget.onPlaying?.call();
|
||||
} else if (!videoPlayerController.value.isPlaying) {
|
||||
WakelockPlus.disable();
|
||||
widget.onPaused?.call();
|
||||
}
|
||||
|
||||
if (videoPlayerController.value.position ==
|
||||
videoPlayerController.value.duration) {
|
||||
WakelockPlus.disable();
|
||||
widget.onVideoEnded?.call();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> initializePlayer() async {
|
||||
try {
|
||||
videoPlayerController = widget.file == null
|
||||
? VideoPlayerController.networkUrl(
|
||||
Uri.parse(widget.url!),
|
||||
httpHeaders: {"x-immich-user-token": widget.accessToken ?? ""},
|
||||
)
|
||||
: VideoPlayerController.file(widget.file!);
|
||||
|
||||
await videoPlayerController.initialize();
|
||||
_createChewieController();
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
debugPrint("ERROR initialize video player $e");
|
||||
}
|
||||
}
|
||||
|
||||
_createChewieController() {
|
||||
chewieController = ChewieController(
|
||||
Widget build(BuildContext context) {
|
||||
final controller = useChewieController(
|
||||
asset,
|
||||
controlsSafeAreaMinimum: const EdgeInsets.only(
|
||||
bottom: 100,
|
||||
),
|
||||
showOptions: true,
|
||||
showControlsOnInitialize: false,
|
||||
videoPlayerController: videoPlayerController,
|
||||
autoPlay: true,
|
||||
autoInitialize: true,
|
||||
allowFullScreen: false,
|
||||
allowedScreenSleep: false,
|
||||
showControls: widget.showControls && !widget.isMotionVideo,
|
||||
placeholder: placeholder,
|
||||
showControls: showControls && !isMotionVideo,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
customControls: const VideoPlayerControls(),
|
||||
hideControlsTimer: widget.hideControlsTimer,
|
||||
onPlaying: onPlaying,
|
||||
onPaused: onPaused,
|
||||
onVideoEnded: onVideoEnded,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
videoPlayerController.pause();
|
||||
videoPlayerController.dispose();
|
||||
chewieController?.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (chewieController?.videoPlayerController.value.isInitialized == true) {
|
||||
return SizedBox(
|
||||
height: context.height,
|
||||
width: context.width,
|
||||
child: Chewie(
|
||||
controller: chewieController!,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
height: context.height,
|
||||
width: context.width,
|
||||
child: Center(
|
||||
child: Stack(
|
||||
// Loading
|
||||
return PopScope(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (controller == null) {
|
||||
return Stack(
|
||||
children: [
|
||||
if (widget.placeholder != null) widget.placeholder!,
|
||||
if (widget.showDownloadingIndicator)
|
||||
const Center(
|
||||
child: ImmichLoadingIndicator(),
|
||||
if (placeholder != null) placeholder!,
|
||||
const DelayedLoadingIndicator(
|
||||
fadeInDuration: Duration(milliseconds: 500),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final size = MediaQuery.of(context).size;
|
||||
return SizedBox(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
child: Chewie(
|
||||
controller: controller,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -245,7 +245,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||
} catch (e, stack) {
|
||||
log.severe(
|
||||
"Failed to get thumbnail for album ${album.name}",
|
||||
e.toString(),
|
||||
e,
|
||||
stack,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||
.then((_) => log.info("Logout was successful for $userEmail"))
|
||||
.onError(
|
||||
(error, stackTrace) =>
|
||||
log.severe("Error logging out $userEmail", error, stackTrace),
|
||||
log.severe("Logout failed for $userEmail", error, stackTrace),
|
||||
);
|
||||
|
||||
await Future.wait([
|
||||
|
@ -129,8 +129,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||
shouldChangePassword: false,
|
||||
isAuthenticated: false,
|
||||
);
|
||||
} catch (e) {
|
||||
log.severe("Error logging out $e");
|
||||
} catch (e, stack) {
|
||||
log.severe('Logout failed', e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ class OAuthService {
|
|||
),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
log.severe("Error performing oAuthLogin: ${e.toString()}", e, stack);
|
||||
log.severe("OAuth login failed", e, stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||
import 'package:immich_mobile/modules/map/models/map_state.model.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
|
@ -51,7 +52,8 @@ class MapStateNotifier extends _$MapStateNotifier {
|
|||
lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current),
|
||||
);
|
||||
_log.severe(
|
||||
"Cannot fetch map light style with status - ${lightResponse.statusCode} and response - ${lightResponse.body}",
|
||||
"Cannot fetch map light style",
|
||||
lightResponse.toLoggerString(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -77,9 +79,7 @@ class MapStateNotifier extends _$MapStateNotifier {
|
|||
state = state.copyWith(
|
||||
darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current),
|
||||
);
|
||||
_log.severe(
|
||||
"Cannot fetch map dark style with status - ${darkResponse.statusCode} and response - ${darkResponse.body}",
|
||||
);
|
||||
_log.severe("Cannot fetch map dark style", darkResponse.toLoggerString());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ class MapSerivce with ErrorLoggerMixin {
|
|||
return markers?.map(MapMarker.fromDto) ?? [];
|
||||
},
|
||||
defaultValue: [],
|
||||
errorMessage: "Failed to get map markers",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -105,10 +105,8 @@ class MapUtils {
|
|||
timeLimit: const Duration(seconds: 5),
|
||||
);
|
||||
return (currentUserLocation, null);
|
||||
} catch (error) {
|
||||
_log.severe(
|
||||
"Cannot get user's current location due to ${error.toString()}",
|
||||
);
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot get user's current location", error, stack);
|
||||
return (null, LocationPermission.unableToDetermine);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,7 +147,7 @@ class MapAssetGrid extends HookConsumerWidget {
|
|||
},
|
||||
error: (error, stackTrace) {
|
||||
log.warning(
|
||||
"Cannot get assets in the current map bounds $error",
|
||||
"Cannot get assets in the current map bounds",
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
|
|
|
@ -47,7 +47,7 @@ class MemoryService {
|
|||
|
||||
return memories.isNotEmpty ? memories : null;
|
||||
} catch (error, stack) {
|
||||
log.severe("Cannot get memories ${error.toString()}", error, stack);
|
||||
log.severe("Cannot get memories", error, stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,9 +55,9 @@ class MemoryCard extends StatelessWidget {
|
|||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// Determine the fit using the aspect ratio
|
||||
BoxFit fit = BoxFit.fitWidth;
|
||||
BoxFit fit = BoxFit.contain;
|
||||
if (asset.width != null && asset.height != null) {
|
||||
final aspectRatio = asset.height! / asset.width!;
|
||||
final aspectRatio = asset.width! / asset.height!;
|
||||
final phoneAspectRatio =
|
||||
constraints.maxWidth / constraints.maxHeight;
|
||||
// Look for a 25% difference in either direction
|
||||
|
|
|
@ -40,7 +40,7 @@ class PartnerService {
|
|||
return userDtos.map((u) => User.fromPartnerDto(u)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
_log.warning("failed to get partners for direction $direction:\n$e");
|
||||
_log.warning("Failed to get partners for direction $direction", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class PartnerService {
|
|||
partner.isPartnerSharedBy = false;
|
||||
await _db.writeTxn(() => _db.users.put(partner));
|
||||
} catch (e) {
|
||||
_log.warning("failed to remove partner ${partner.id}:\n$e");
|
||||
_log.warning("Failed to remove partner ${partner.id}", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -66,7 +66,7 @@ class PartnerService {
|
|||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
_log.warning("failed to add partner ${partner.id}:\n$e");
|
||||
_log.warning("Failed to add partner ${partner.id}", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ class PartnerService {
|
|||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
_log.warning("failed to update partner ${partner.id}:\n$e");
|
||||
_log.warning("Failed to update partner ${partner.id}", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ class SharedLinkService {
|
|||
? AsyncData(list.map(SharedLink.fromDto).toList())
|
||||
: const AsyncData([]);
|
||||
} catch (e, stack) {
|
||||
_log.severe("failed to fetch shared links - $e");
|
||||
_log.severe("Failed to fetch shared links", e, stack);
|
||||
return AsyncError(e, stack);
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ class SharedLinkService {
|
|||
try {
|
||||
return await _apiService.sharedLinkApi.removeSharedLink(id);
|
||||
} catch (e) {
|
||||
_log.severe("failed to delete shared link id - $id with error - $e");
|
||||
_log.severe("Failed to delete shared link id - $id", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ class SharedLinkService {
|
|||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("failed to create shared link with error - $e");
|
||||
_log.severe("Failed to create shared link", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ class SharedLinkService {
|
|||
return SharedLink.fromDto(responseDto);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.severe("failed to update shared link id - $id with error - $e");
|
||||
_log.severe("Failed to update shared link id - $id", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ class TrashNotifier extends StateNotifier<bool> {
|
|||
.read(syncServiceProvider)
|
||||
.handleRemoteAssetRemoval(idsToRemove.cast<String>().toList());
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
|
||||
_log.severe("Cannot empty trash", error, stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ class TrashNotifier extends StateNotifier<bool> {
|
|||
|
||||
return isRemoved;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
|
||||
_log.severe("Cannot remove assets", error, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ class TrashNotifier extends StateNotifier<bool> {
|
|||
return true;
|
||||
}
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
|
||||
_log.severe("Cannot restore assets", error, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ class TrashNotifier extends StateNotifier<bool> {
|
|||
await _db.assets.putAll(updatedAssets);
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
|
||||
_log.severe("Cannot restore trash", error, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ class TrashService {
|
|||
await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteIds));
|
||||
return true;
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore assets ${error.toString()}", error, stack);
|
||||
_log.severe("Cannot restore assets", error, stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ class TrashService {
|
|||
try {
|
||||
await _apiService.trashApi.emptyTrash();
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot empty trash ${error.toString()}", error, stack);
|
||||
_log.severe("Cannot empty trash", error, stack);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,7 @@ class TrashService {
|
|||
try {
|
||||
await _apiService.trashApi.restoreTrash();
|
||||
} catch (error, stack) {
|
||||
_log.severe("Cannot restore trash ${error.toString()}", error, stack);
|
||||
_log.severe("Cannot restore trash", error, stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
@ -16,28 +16,31 @@ class AuthGuard extends AutoRouteGuard {
|
|||
resolver.next(true);
|
||||
|
||||
try {
|
||||
var res = await _apiService.authenticationApi.validateAccessToken();
|
||||
// Look in the store for an access token
|
||||
Store.get(StoreKey.accessToken);
|
||||
|
||||
// Validate the access token with the server
|
||||
final res = await _apiService.authenticationApi.validateAccessToken();
|
||||
if (res == null || res.authStatus != true) {
|
||||
// If the access token is invalid, take user back to login
|
||||
_log.fine("User token is invalid. Redirecting to login");
|
||||
_log.fine('User token is invalid. Redirecting to login');
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
}
|
||||
} on StoreKeyNotFoundException catch (_) {
|
||||
// If there is no access token, take us to the login page
|
||||
_log.warning('No access token in the store.');
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
return;
|
||||
} on ApiException catch (e) {
|
||||
if (e.code == HttpStatus.badRequest &&
|
||||
e.innerException is SocketException) {
|
||||
// offline?
|
||||
_log.fine(
|
||||
"Unable to validate user token. User may be offline and offline browsing is allowed.",
|
||||
);
|
||||
} else {
|
||||
debugPrint("Error [onNavigation] ${e.toString()}");
|
||||
// On an unauthorized request, take us to the login page
|
||||
if (e.code == HttpStatus.unauthorized) {
|
||||
_log.warning("Unauthorized access token.");
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error [onNavigation] ${e.toString()}");
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
return;
|
||||
// Otherwise, this is not fatal, but we still log the warning
|
||||
_log.warning('Error validating access token from server: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1393,7 +1393,7 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
|||
void Function()? onPaused,
|
||||
Widget? placeholder,
|
||||
bool showControls = true,
|
||||
Duration hideControlsTimer = const Duration(seconds: 5),
|
||||
Duration hideControlsTimer = const Duration(milliseconds: 1500),
|
||||
bool showDownloadingIndicator = true,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
|
@ -1429,7 +1429,7 @@ class VideoViewerRouteArgs {
|
|||
this.onPaused,
|
||||
this.placeholder,
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.hideControlsTimer = const Duration(milliseconds: 1500),
|
||||
this.showDownloadingIndicator = true,
|
||||
});
|
||||
|
||||
|
|
|
@ -175,6 +175,11 @@ class Asset {
|
|||
|
||||
int? stackCount;
|
||||
|
||||
/// Aspect ratio of the asset
|
||||
@ignore
|
||||
double? get aspectRatio =>
|
||||
width == null || height == null ? 0 : width! / height!;
|
||||
|
||||
/// `true` if this [Asset] is present on the device
|
||||
@ignore
|
||||
bool get isLocal => localId != null;
|
||||
|
|
|
@ -9,6 +9,7 @@ part 'logger_message.model.g.dart';
|
|||
class LoggerMessage {
|
||||
Id id = Isar.autoIncrement;
|
||||
String message;
|
||||
String? details;
|
||||
@Enumerated(EnumType.ordinal)
|
||||
LogLevel level = LogLevel.INFO;
|
||||
DateTime createdAt;
|
||||
|
@ -17,6 +18,7 @@ class LoggerMessage {
|
|||
|
||||
LoggerMessage({
|
||||
required this.message,
|
||||
required this.details,
|
||||
required this.level,
|
||||
required this.createdAt,
|
||||
required this.context1,
|
||||
|
|
BIN
mobile/lib/shared/models/logger_message.model.g.dart
generated
BIN
mobile/lib/shared/models/logger_message.model.g.dart
generated
Binary file not shown.
|
@ -90,7 +90,7 @@ class AssetService {
|
|||
return allAssets;
|
||||
} catch (error, stack) {
|
||||
log.severe(
|
||||
'Error while getting remote assets: ${error.toString()}',
|
||||
'Error while getting remote assets',
|
||||
error,
|
||||
stack,
|
||||
);
|
||||
|
@ -117,7 +117,7 @@ class AssetService {
|
|||
);
|
||||
return true;
|
||||
} catch (error, stack) {
|
||||
log.severe("Error deleteAssets ${error.toString()}", error, stack);
|
||||
log.severe("Error while deleting assets", error, stack);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import 'package:share_plus/share_plus.dart';
|
|||
/// [ImmichLogger] is a custom logger that is built on top of the [logging] package.
|
||||
/// The logs are written to the database and onto console, using `debugPrint` method.
|
||||
///
|
||||
/// The logs are deleted when exceeding the `maxLogEntries` (default 200) property
|
||||
/// The logs are deleted when exceeding the `maxLogEntries` (default 500) property
|
||||
/// in the class.
|
||||
///
|
||||
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
||||
|
@ -58,6 +58,7 @@ class ImmichLogger {
|
|||
debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
|
||||
final lm = LoggerMessage(
|
||||
message: record.message,
|
||||
details: record.error?.toString(),
|
||||
level: record.level.toLogLevel(),
|
||||
createdAt: record.time,
|
||||
context1: record.loggerName,
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:io';
|
|||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/response_extensions.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
@ -41,7 +42,8 @@ class ShareService {
|
|||
|
||||
if (res.statusCode != 200) {
|
||||
_log.severe(
|
||||
"Asset download failed with status - ${res.statusCode} and response - ${res.body}",
|
||||
"Asset download for ${asset.fileName} failed",
|
||||
res.toLoggerString(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
@ -68,7 +70,7 @@ class ShareService {
|
|||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
_log.severe("Share failed with error $error");
|
||||
_log.severe("Share failed", error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -140,7 +140,7 @@ class SyncService {
|
|||
try {
|
||||
await _db.writeTxn(() => a.put(_db));
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to put new asset into db: $e");
|
||||
_log.severe("Failed to put new asset into db", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -173,7 +173,7 @@ class SyncService {
|
|||
}
|
||||
return false;
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db: $e");
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -232,7 +232,7 @@ class SyncService {
|
|||
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
|
||||
await upsertAssetsWithExif(toAdd + toUpdate);
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db: $e");
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
}
|
||||
await _updateUserAssetsETag(user, now);
|
||||
return true;
|
||||
|
@ -364,7 +364,7 @@ class SyncService {
|
|||
});
|
||||
_log.info("Synced changes of remote album ${album.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote album to database $e");
|
||||
_log.severe("Failed to sync remote album to database", e);
|
||||
}
|
||||
|
||||
if (album.shared || dto.shared) {
|
||||
|
@ -441,7 +441,7 @@ class SyncService {
|
|||
assert(ok);
|
||||
_log.info("Removed local album $album from DB");
|
||||
} catch (e) {
|
||||
_log.severe("Failed to remove local album $album from DB");
|
||||
_log.severe("Failed to remove local album $album from DB", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -577,7 +577,7 @@ class SyncService {
|
|||
});
|
||||
_log.info("Synced changes of local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to update synced album ${ape.name} in DB: $e");
|
||||
_log.severe("Failed to update synced album ${ape.name} in DB", e);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -623,7 +623,7 @@ class SyncService {
|
|||
});
|
||||
_log.info("Fast synced local album ${ape.name} to DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to fast sync local album ${ape.name} to DB: $e");
|
||||
_log.severe("Failed to fast sync local album ${ape.name} to DB", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -656,7 +656,7 @@ class SyncService {
|
|||
await _db.writeTxn(() => _db.albums.store(a));
|
||||
_log.info("Added a new local album to DB: ${ape.name}");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to add new local album ${ape.name} to DB: $e");
|
||||
_log.severe("Failed to add new local album ${ape.name} to DB", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -706,9 +706,7 @@ class SyncService {
|
|||
});
|
||||
_log.info("Upserted ${assets.length} assets into the DB");
|
||||
} on IsarError catch (e) {
|
||||
_log.severe(
|
||||
"Failed to upsert ${assets.length} assets into the DB: ${e.toString()}",
|
||||
);
|
||||
_log.severe("Failed to upsert ${assets.length} assets into the DB", e);
|
||||
// give details on the errors
|
||||
assets.sort(Asset.compareByOwnerChecksum);
|
||||
final inDb = await _db.assets.getAllByOwnerIdChecksum(
|
||||
|
@ -776,7 +774,7 @@ class SyncService {
|
|||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
_log.severe("Failed to remove all local albums and assets: $e");
|
||||
_log.severe("Failed to remove all local albums and assets", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ class UserService {
|
|||
final dto = await _apiService.userApi.getAllUsers(isAll);
|
||||
return dto?.map(User.fromUserDto).toList();
|
||||
} catch (e) {
|
||||
_log.warning("Failed get all users:\n$e");
|
||||
_log.warning("Failed get all users", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ class UserService {
|
|||
),
|
||||
);
|
||||
} catch (e) {
|
||||
_log.warning("Failed to upload profile image:\n$e");
|
||||
_log.warning("Failed to upload profile image", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
40
mobile/lib/shared/ui/delayed_loading_indicator.dart
Normal file
40
mobile/lib/shared/ui/delayed_loading_indicator.dart
Normal file
|
@ -0,0 +1,40 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
|
||||
class DelayedLoadingIndicator extends StatelessWidget {
|
||||
/// The delay to avoid showing the loading indicator
|
||||
final Duration delay;
|
||||
|
||||
/// Defaults to using the [ImmichLoadingIndicator]
|
||||
final Widget? child;
|
||||
|
||||
/// An optional fade in duration to animate the loading
|
||||
final Duration? fadeInDuration;
|
||||
|
||||
const DelayedLoadingIndicator({
|
||||
super.key,
|
||||
this.delay = const Duration(seconds: 3),
|
||||
this.child,
|
||||
this.fadeInDuration,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedSwitcher(
|
||||
duration: fadeInDuration ?? Duration.zero,
|
||||
child: FutureBuilder(
|
||||
future: Future.delayed(delay),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return child ??
|
||||
const ImmichLoadingIndicator(
|
||||
key: ValueKey('loading'),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(key: const ValueKey('hiding'));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
var isDarkTheme = context.isDarkTheme;
|
||||
|
||||
buildStackMessage(String stackTrace) {
|
||||
buildTextWithCopyButton(String header, String text) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
|
@ -28,7 +28,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"STACK TRACES",
|
||||
header,
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: context.primaryColor,
|
||||
|
@ -38,8 +38,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
|||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: stackTrace))
|
||||
.then((_) {
|
||||
Clipboard.setData(ClipboardData(text: text)).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
|
@ -68,73 +67,7 @@ class AppLogDetailPage extends HookConsumerWidget {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SelectableText(
|
||||
stackTrace,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: "Inconsolata",
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
buildLogMessage(String message) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
"MESSAGE",
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: context.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: message)).then((_) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
"Copied to clipboard",
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.copy,
|
||||
size: 16.0,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDarkTheme ? Colors.grey[900] : Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SelectableText(
|
||||
message,
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 12.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
@ -194,11 +127,16 @@ class AppLogDetailPage extends HookConsumerWidget {
|
|||
body: SafeArea(
|
||||
child: ListView(
|
||||
children: [
|
||||
buildLogMessage(logMessage.message),
|
||||
buildTextWithCopyButton("MESSAGE", logMessage.message),
|
||||
if (logMessage.details != null)
|
||||
buildTextWithCopyButton("DETAILS", logMessage.details.toString()),
|
||||
if (logMessage.context1 != null)
|
||||
buildLogContext1(logMessage.context1.toString()),
|
||||
if (logMessage.context2 != null)
|
||||
buildStackMessage(logMessage.context2.toString()),
|
||||
buildTextWithCopyButton(
|
||||
"STACK TRACE",
|
||||
logMessage.context2.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -69,9 +69,9 @@ class AppLogPage extends HookConsumerWidget {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
"Logs - ${logMessages.value.length}",
|
||||
style: const TextStyle(
|
||||
title: const Text(
|
||||
"Logs",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
|
@ -135,29 +135,15 @@ class AppLogPage extends HookConsumerWidget {
|
|||
dense: true,
|
||||
tileColor: getTileColor(logMessage.level),
|
||||
minLeadingWidth: 10,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: "#$index ",
|
||||
style: TextStyle(
|
||||
color: isDarkTheme ? Colors.white70 : Colors.grey[600],
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: truncateLogMessage(logMessage.message, 4),
|
||||
title: Text(
|
||||
truncateLogMessage(logMessage.message, 4),
|
||||
style: const TextStyle(
|
||||
fontSize: 14.0,
|
||||
fontFamily: "Inconsolata",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
style: const TextStyle(fontSize: 14.0, fontFamily: "Inconsolata"),
|
||||
),
|
||||
subtitle: Text(
|
||||
"[${logMessage.context1}] Logged on ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}",
|
||||
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}",
|
||||
style: TextStyle(
|
||||
fontSize: 12.0,
|
||||
color: Colors.grey[600],
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||
import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
|
||||
|
||||
final _loadingEntry = OverlayEntry(
|
||||
builder: (context) => SizedBox.square(
|
||||
|
@ -9,7 +9,12 @@ final _loadingEntry = OverlayEntry(
|
|||
child: DecoratedBox(
|
||||
decoration:
|
||||
BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
|
||||
child: const Center(child: ImmichLoadingIndicator()),
|
||||
child: const Center(
|
||||
child: DelayedLoadingIndicator(
|
||||
delay: Duration(seconds: 1),
|
||||
fadeInDuration: Duration(milliseconds: 400),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -27,19 +32,19 @@ class _LoadingOverlay extends Hook<ValueNotifier<bool>> {
|
|||
|
||||
class _LoadingOverlayState
|
||||
extends HookState<ValueNotifier<bool>, _LoadingOverlay> {
|
||||
late final _isProcessing = ValueNotifier(false)..addListener(_listener);
|
||||
OverlayEntry? overlayEntry;
|
||||
late final _isLoading = ValueNotifier(false)..addListener(_listener);
|
||||
OverlayEntry? _loadingOverlay;
|
||||
|
||||
void _listener() {
|
||||
setState(() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_isProcessing.value) {
|
||||
overlayEntry?.remove();
|
||||
overlayEntry = _loadingEntry;
|
||||
if (_isLoading.value) {
|
||||
_loadingOverlay?.remove();
|
||||
_loadingOverlay = _loadingEntry;
|
||||
Overlay.of(context).insert(_loadingEntry);
|
||||
} else {
|
||||
overlayEntry?.remove();
|
||||
overlayEntry = null;
|
||||
_loadingOverlay?.remove();
|
||||
_loadingOverlay = null;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -47,17 +52,17 @@ class _LoadingOverlayState
|
|||
|
||||
@override
|
||||
ValueNotifier<bool> build(BuildContext context) {
|
||||
return _isProcessing;
|
||||
return _isLoading;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isProcessing.dispose();
|
||||
_isLoading.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Object? get debugValue => _isProcessing.value;
|
||||
Object? get debugValue => _isLoading.value;
|
||||
|
||||
@override
|
||||
String get debugLabel => 'useProcessingOverlay<>';
|
||||
|
|
|
@ -35,10 +35,10 @@ class SplashScreenPage extends HookConsumerWidget {
|
|||
deviceIsOffline = true;
|
||||
log.fine("Device seems to be offline upon launch");
|
||||
} else {
|
||||
log.severe(e);
|
||||
log.severe("Failed to resolve endpoint", e);
|
||||
}
|
||||
} catch (e) {
|
||||
log.severe(e);
|
||||
log.severe("Failed to resolve endpoint", e);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -53,7 +53,7 @@ class SplashScreenPage extends HookConsumerWidget {
|
|||
ref.read(authenticationProvider.notifier).logout();
|
||||
|
||||
log.severe(
|
||||
'Cannot set success login info: $error',
|
||||
'Cannot set success login info',
|
||||
error,
|
||||
stackTrace,
|
||||
);
|
||||
|
|
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
|
@ -108,6 +108,7 @@ doc/PersonResponseDto.md
|
|||
doc/PersonStatisticsResponseDto.md
|
||||
doc/PersonUpdateDto.md
|
||||
doc/PersonWithFacesResponseDto.md
|
||||
doc/PlacesResponseDto.md
|
||||
doc/QueueStatusDto.md
|
||||
doc/ReactionLevel.md
|
||||
doc/ReactionType.md
|
||||
|
@ -308,6 +309,7 @@ lib/model/person_response_dto.dart
|
|||
lib/model/person_statistics_response_dto.dart
|
||||
lib/model/person_update_dto.dart
|
||||
lib/model/person_with_faces_response_dto.dart
|
||||
lib/model/places_response_dto.dart
|
||||
lib/model/queue_status_dto.dart
|
||||
lib/model/reaction_level.dart
|
||||
lib/model/reaction_type.dart
|
||||
|
@ -485,6 +487,7 @@ test/person_response_dto_test.dart
|
|||
test/person_statistics_response_dto_test.dart
|
||||
test/person_update_dto_test.dart
|
||||
test/person_with_faces_response_dto_test.dart
|
||||
test/places_response_dto_test.dart
|
||||
test/queue_status_dto_test.dart
|
||||
test/reaction_level_test.dart
|
||||
test/reaction_type_test.dart
|
||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/MetadataSearchDto.md
generated
BIN
mobile/openapi/doc/MetadataSearchDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/PeopleResponseDto.md
generated
BIN
mobile/openapi/doc/PeopleResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/PlacesResponseDto.md
generated
Normal file
BIN
mobile/openapi/doc/PlacesResponseDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/SearchApi.md
generated
BIN
mobile/openapi/doc/SearchApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SmartSearchDto.md
generated
BIN
mobile/openapi/doc/SmartSearchDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/search_api.dart
generated
BIN
mobile/openapi/lib/api/search_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
BIN
mobile/openapi/lib/model/metadata_search_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/people_response_dto.dart
generated
BIN
mobile/openapi/lib/model/people_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/places_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/places_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/smart_search_dto.dart
generated
BIN
mobile/openapi/lib/model/smart_search_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/metadata_search_dto_test.dart
generated
BIN
mobile/openapi/test/metadata_search_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/people_response_dto_test.dart
generated
BIN
mobile/openapi/test/people_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/places_response_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/places_response_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/search_api_test.dart
generated
BIN
mobile/openapi/test/search_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/smart_search_dto_test.dart
generated
BIN
mobile/openapi/test/smart_search_dto_test.dart
generated
Binary file not shown.
|
@ -413,10 +413,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: file
|
||||
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
|
||||
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "7.0.0"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -569,10 +569,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_udid
|
||||
sha256: "666412097b86d9a6f9803073d0f0ba70de9b198fe6493d89d352a1f8cd6c5c84"
|
||||
sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.0"
|
||||
flutter_web_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -619,10 +619,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: geolocator
|
||||
sha256: e946395fc608842bb2f6c914807e9183f86f3cb787f6b8f832753e5251036f02
|
||||
sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.1.0"
|
||||
version: "11.0.0"
|
||||
geolocator_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -651,10 +651,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: geolocator_web
|
||||
sha256: "59083f7e0871b78299918d92bf930a14377f711d2d1156c558cd5ebae6c20d58"
|
||||
sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "3.0.0"
|
||||
geolocator_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -860,6 +860,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.0"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -907,18 +931,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
|
@ -1002,10 +1026,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
version: "1.9.0"
|
||||
path_provider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1138,10 +1162,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
version: "3.1.4"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1170,10 +1194,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: process
|
||||
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
|
||||
sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
version: "5.0.2"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1298,10 +1322,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
|
||||
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.2"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1322,10 +1346,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
|
||||
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.2"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1639,10 +1663,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
|
||||
sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.10.0"
|
||||
version: "13.0.0"
|
||||
wakelock_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1687,10 +1711,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: webdriver
|
||||
sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49"
|
||||
sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
version: "3.0.3"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -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:
|
||||
|
@ -32,8 +32,8 @@ dependencies:
|
|||
git:
|
||||
url: https://github.com/maplibre/flutter-maplibre-gl.git
|
||||
ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
|
||||
geolocator: ^10.1.0 # used to move to current location in map view
|
||||
flutter_udid: ^2.1.1
|
||||
geolocator: ^11.0.0 # used to move to current location in map view
|
||||
flutter_udid: ^3.0.0
|
||||
package_info_plus: ^5.0.1
|
||||
url_launcher: ^6.2.4
|
||||
http: 0.13.5
|
||||
|
|
|
@ -2463,6 +2463,7 @@
|
|||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
|
@ -4690,6 +4691,50 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/search/places": {
|
||||
"get": {
|
||||
"operationId": "searchPlaces",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PlacesResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Search"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/search/smart": {
|
||||
"post": {
|
||||
"operationId": "searchSmart",
|
||||
|
@ -6413,7 +6458,7 @@
|
|||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.95.0",
|
||||
"version": "1.95.1",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
|
@ -8429,6 +8474,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"withDeleted": {
|
||||
|
@ -8591,6 +8637,9 @@
|
|||
},
|
||||
"PeopleResponseDto": {
|
||||
"properties": {
|
||||
"hidden": {
|
||||
"type": "integer"
|
||||
},
|
||||
"people": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
|
@ -8602,6 +8651,7 @@
|
|||
}
|
||||
},
|
||||
"required": [
|
||||
"hidden",
|
||||
"people",
|
||||
"total"
|
||||
],
|
||||
|
@ -8750,6 +8800,31 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PlacesResponseDto": {
|
||||
"properties": {
|
||||
"admin1name": {
|
||||
"type": "string"
|
||||
},
|
||||
"admin2name": {
|
||||
"type": "string"
|
||||
},
|
||||
"latitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"longitude": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"latitude",
|
||||
"longitude",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueueStatusDto": {
|
||||
"properties": {
|
||||
"isActive": {
|
||||
|
@ -9435,6 +9510,9 @@
|
|||
"isMotion": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isNotInAlbum": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isOffline": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
@ -9497,6 +9575,7 @@
|
|||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"withDeleted": {
|
||||
|
|
142
open-api/typescript-sdk/axios-client/api.ts
generated
142
open-api/typescript-sdk/axios-client/api.ts
generated
|
@ -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>}
|
||||
|
@ -2988,6 +2994,43 @@ export interface PersonWithFacesResponseDto {
|
|||
*/
|
||||
'thumbnailPath': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PlacesResponseDto
|
||||
*/
|
||||
export interface PlacesResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'admin1name'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'admin2name'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'latitude': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'longitude': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PlacesResponseDto
|
||||
*/
|
||||
'name': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -3880,6 +3923,12 @@ export interface SmartSearchDto {
|
|||
* @memberof SmartSearchDto
|
||||
*/
|
||||
'isMotion'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SmartSearchDto
|
||||
*/
|
||||
'isNotInAlbum'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
|
@ -15435,6 +15484,51 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
|
|||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
searchPlaces: async (name: string, options: RawAxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'name' is not null or undefined
|
||||
assertParamExists('searchPlaces', 'name', name)
|
||||
const localVarPath = `/search/places`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (name !== undefined) {
|
||||
localVarQueryParameter['name'] = name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
@ -15572,6 +15666,18 @@ export const SearchApiFp = function(configuration?: Configuration) {
|
|||
const operationBasePath = operationServerMap['SearchApi.searchPerson']?.[index]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async searchPlaces(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PlacesResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.searchPlaces(name, options);
|
||||
const index = configuration?.serverIndex ?? 0;
|
||||
const operationBasePath = operationServerMap['SearchApi.searchPlaces']?.[index]?.url;
|
||||
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SmartSearchDto} smartSearchDto
|
||||
|
@ -15639,6 +15745,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
|
|||
searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
|
||||
return localVarFp.searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchApiSearchPlacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
searchPlaces(requestParameters: SearchApiSearchPlacesRequest, options?: RawAxiosRequestConfig): AxiosPromise<Array<PlacesResponseDto>> {
|
||||
return localVarFp.searchPlaces(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchApiSearchSmartRequest} requestParameters Request parameters.
|
||||
|
@ -15805,6 +15920,20 @@ export interface SearchApiSearchPersonRequest {
|
|||
readonly withHidden?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for searchPlaces operation in SearchApi.
|
||||
* @export
|
||||
* @interface SearchApiSearchPlacesRequest
|
||||
*/
|
||||
export interface SearchApiSearchPlacesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SearchApiSearchPlaces
|
||||
*/
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for searchSmart operation in SearchApi.
|
||||
* @export
|
||||
|
@ -15881,6 +16010,17 @@ export class SearchApi extends BaseAPI {
|
|||
return SearchApiFp(this.configuration).searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SearchApiSearchPlacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof SearchApi
|
||||
*/
|
||||
public searchPlaces(requestParameters: SearchApiSearchPlacesRequest, options?: RawAxiosRequestConfig) {
|
||||
return SearchApiFp(this.configuration).searchPlaces(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SearchApiSearchSmartRequest} requestParameters Request parameters.
|
||||
|
|
2
open-api/typescript-sdk/axios-client/base.ts
generated
2
open-api/typescript-sdk/axios-client/base.ts
generated
|
@ -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).
|
||||
|
|
2
open-api/typescript-sdk/axios-client/common.ts
generated
2
open-api/typescript-sdk/axios-client/common.ts
generated
|
@ -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).
|
||||
|
|
|
@ -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).
|
||||
|
|
2
open-api/typescript-sdk/axios-client/index.ts
generated
2
open-api/typescript-sdk/axios-client/index.ts
generated
|
@ -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).
|
||||
|
|
BIN
open-api/typescript-sdk/fetch-client.ts
generated
BIN
open-api/typescript-sdk/fetch-client.ts
generated
Binary file not shown.
15
open-api/typescript-sdk/fetch-errors.ts
Normal file
15
open-api/typescript-sdk/fetch-errors.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { HttpError } from '@oazapfts/runtime';
|
||||
|
||||
export interface ApiExceptionResponse {
|
||||
message: string;
|
||||
error?: string;
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
export interface ApiHttpError extends HttpError {
|
||||
data: ApiExceptionResponse;
|
||||
}
|
||||
|
||||
export function isHttpError(error: unknown): error is ApiHttpError {
|
||||
return error instanceof HttpError;
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export * from './fetch-client';
|
||||
export * from './fetch-errors';
|
||||
|
|
6
open-api/typescript-sdk/package-lock.json
generated
6
open-api/typescript-sdk/package-lock.json
generated
|
@ -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"
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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 }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 });
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
|
@ -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[];
|
||||
},
|
||||
};
|
|
@ -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;
|
||||
},
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue