Compare commits

...

31 Commits

Author SHA1 Message Date
Alex The Bot
608543da0b Version v1.77.0 2023-09-06 03:30:44 +00:00
Maarten Rijke
b4fa60d4fd feat(web): show original uploader in shared album photo details (#3977)
* feat(web): show original uploader in shared album photo details

* feat: send owner in asset by id response

* chore: open api

* fix: linting

* fix: change to Shared By

* openapi

* openapi

* api

* styling

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-09-06 10:14:44 +07:00
dependabot[bot]
b1467bd1da chore(deps): bump actions/checkout from 3 to 4 (#3983)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-06 08:50:22 +07:00
Mert
3cf0f5f11b fix(ml): model downloading improvements (#3988)
* handle ort `NoSuchFile` error, stricter file check

* keep exception order
2023-09-06 08:48:40 +07:00
Jason Rasmussen
454737ca79 refactor(server): update asset endpoint (#3973)
* refactor(server): update asset

* chore: open api
2023-09-04 22:25:31 -04:00
Maarten Rijke
26bc889f8d feat(server): include shared albums in getByAssetId (#3978)
This commit changes the album.getByAssetId API to also consider
albums that have been shared with the current user.
This way when the user is browing their timeline and clicks to show
the asset details they will see if the asset appears in not only their
own albums but also albums shared with them.
2023-09-04 20:49:32 -04:00
Dhrumil Shah
54775b896f fix(mobile): change password page not navigating back (#3967) 2023-09-05 06:36:16 +07:00
Jason Rasmussen
9217fb4094 fix(web): skeleton loading (#3972) 2023-09-04 19:33:57 -04:00
Mert
04d4a30471 fix(server): await thumbnail generation before returning (#3975)
* await sharp command, minor fixes

* removed outdated test
2023-09-04 19:24:55 -04:00
shenlong
90f9501902 fix(mobile): curated places taking more size on large screens (#3959) 2023-09-05 06:10:27 +07:00
shenlong
f8d26bd865 fix(mobile): map markers not loading with int coordinates (#3957)
* fix(mobile): increase zoom-level for map zoom to asset

* refactor(mobile): map-view - rename lastAssetOffsetInSheet

* Workaround OpenAPI Dart generator bug

* fix(mobile): map - increase appbar top padding

* fix(mobile): navigation bar overlapping map bottom sheet

* fix(mobile): map - do not animate the drag handle of bottom sheet on scroll

* fix(mobile): F-Droid build failure due to map view

* fix(mobile): remove jank on map asset marker update

* fix(mobile): map view app-bar padding is made dynamic

* fix(mobile): reduce debounce time in bottom sheet asset scroll

* fix(mobile): bottom sheet - reduce drag handle total height

---------

Co-authored-by: Daniele Ricci <daniele@casaricci.it>
2023-09-05 06:08:43 +07:00
Jason Rasmussen
816d040d81 fix(server): lint import order (#3974)
* fix: use prettier extension

* chore: format fix
2023-09-04 21:45:59 +02:00
Mert
2069293cc1 feat(server): wide gamut thumbnails (#3658) 2023-09-03 02:21:51 -04:00
JasBogans
4bd77d5899 fix(web): sidebar artifact when toggle themes (#3955)
* fix for sidebar artifact when clicking the toggle

* Fix the delay in the search-bar

* format

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-09-03 08:31:12 +07:00
Mert
f8ff342852 feat(server): advanced settings for transcoding (#3775)
* set stream with `-map` flag

* updated tests

* fixed audio stream mapping

* added bframe setting to config

* updated api

* added b-frame option in dashboard

* updated tests and formatting

* "Advanced" section for FFmpeg with extra options

* updated api

* updated tests and formatting

* styling

* made vp9 bitstream filters conditional on b-frames

* fixed gop size condition

* add cq override

* simplified isEdited conditions

* simplified conditional flow for cq mode

* fixed dto

* clarified cq mode in description

* formatting

* added npl setting

* Adjusted b-frame title and description

* fixed rebase

* changed defaults for pascal compatibility, added temporal aq setting

* updated api

* added temporal aq to ui

* polished dashboard

* formatting
2023-09-03 08:22:42 +07:00
Patrick Eigensatz
67ac686704 docs: bulk upload: Fix "upload directory" instructions (#3942)
The command examples are titled "Upload current directory" and "Upload target directory", however the presented commands skip the `/import` directory, if the `--recursive` flag is not explicitly specified.
2023-09-02 02:06:40 +00:00
Jason Rasmussen
4e5bf7ae2e test: server-info e2e tests (#3948) 2023-09-01 22:01:54 -04:00
Mert
b7fd5dcb4a dev(ml): fixed docker-compose.dev.yml, updated locust (#3951)
* fixed dev docker compose

* updated locustfile

* deleted old script, moved comments to locustfile
2023-09-01 21:59:17 -04:00
Valonso
bea287c5b3 fix(web): images not loading on search and gallery (#3902)
* Check all observed entries, not only first

* fix: formatting

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-09-01 15:39:15 -04:00
JasBogans
46c716d450 feat(web): skeleton on asset loading (#3867)
* feat(web): skeletron on asset loading

* feat: add skeleton to all asset grid views

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-09-01 13:12:09 -04:00
Mert
9539a361e4 fix(server): non-nullable IsOptional (#3939)
* custom `IsOptional`

* added link to source

* formatting

* Update server/src/domain/domain.util.ts

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

* nullable birth date endpoint

* made `nullable` a property

* formatting

* removed unused dto

* updated decorator arg

* fixed album e2e tests

* add null tests for auth e2e

* add null test for person e2e

* fixed tests

* added null test for user e2e

* removed unusued import

* log key in test name

* chore: add note about mobile not being able to use the endpoint

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-09-01 16:40:00 +00:00
Villena Guillaume
ca35e5557b feat(web): Improved assets upload (#3850)
* Improved asset upload algorithm.

- Upload Queue: New process algorithm
- Upload Queue: Concurrency correctly respected when dragging / adding multiple group of files to the queue
- Upload Task: Add more information about progress (upload speed and remaining time)
- Upload Panel: Add more information to about the queue status (Remaining, Errors, Duplicated, Uploaded)
- Error recovery: asset information are kept in the queue to give the user a chance to read the error message
- Error recovery: on error allow the user to retry the upload or hide the error / all errors

* Support "live" editing of the upload concurrency

* Fixed some issues

* Reformat

* fix: merge, linting, dark mode, upload to share

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2023-09-01 12:00:51 -04:00
Jason Rasmussen
a26ed3d1a6 refactor(web,server): use feature flags for oauth (#3928)
* refactor: oauth to use feature flags

* chore: open api

* chore: e2e test for authorize endpoint
2023-09-01 18:08:42 +07:00
Daniele Ricci
c7d53a5006 docs: remove obsolete environment variables (#3926)
* Obsolete environment variables

* Review fixes

* Review fixes

* Review fixes
2023-09-01 18:05:45 +07:00
Mert
41461e0d5d chore(ml): memory optimisations (#3934) 2023-08-31 18:30:53 -05:00
Daniele Ricci
c0a48d7357 Create real anchors around admin sidebar buttons (#3925) 2023-08-31 18:26:40 -05:00
Daniele Ricci
66cc744c22 feat(web): face tooltips (#3924) 2023-08-31 18:25:13 -05:00
Alex The Bot
58ae734fc2 Version v1.76.1 2023-08-30 08:26:04 +00:00
Mert
54b2779b79 chore(ml): improved logging (#3918)
* fixed `minScore` not being set correctly

* apply to init

* don't send `enabled`

* fix eslint warning

* added logger

* added logging

* refinements

* enable access log for info level

* formatting

* merged strings

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2023-08-30 08:22:01 +00:00
Mert
df26e12db6 fix(ml): minScore not being set correctly (#3916)
* fixed `minScore` not being set correctly

* apply to init

* don't send `enabled`

* fix eslint warning

* better error message
2023-08-30 03:16:00 -05:00
Alex
343d89c032 chore: post release 2023-08-29 14:51:57 -05:00
234 changed files with 3847 additions and 1506 deletions

View File

@@ -31,7 +31,7 @@ jobs:
ref="${input_ref:-$github_ref}"
echo "ref=$ref" >> $GITHUB_OUTPUT
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ${{ steps.get-ref.outputs.ref }}

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Cleanup
run: |

View File

@@ -42,7 +42,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@@ -36,7 +36,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0
@@ -120,7 +120,7 @@ jobs:
platforms: "linux/arm64,linux/amd64"
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2.2.0

View File

@@ -30,7 +30,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
token: ${{ secrets.ORG_RELEASE_TOKEN }}
@@ -64,7 +64,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
token: ${{ secrets.ORG_RELEASE_TOKEN }}

View File

@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2

View File

@@ -19,7 +19,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
@@ -37,7 +37,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
@@ -59,7 +59,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
@@ -89,7 +89,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
@@ -115,7 +115,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
@@ -144,7 +144,7 @@ jobs:
name: Run mobile unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
@@ -161,7 +161,7 @@ jobs:
run:
working-directory: ./machine-learning
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v4
@@ -189,7 +189,7 @@ jobs:
name: Check generated files are up-to-date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Run API generation
run: npm --prefix server run api:generate
- name: Find file changes
@@ -224,7 +224,7 @@ jobs:
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install server dependencies
run: npm --prefix server ci
- name: Run existing migrations
@@ -249,7 +249,7 @@ jobs:
# name: Run mobile end-to-end integration tests
# runs-on: macos-latest
# steps:
# - uses: actions/checkout@v3
# - uses: actions/checkout@v4
# - uses: actions/setup-java@v3
# with:
# distribution: 'zulu'

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.76.0
* The version of the OpenAPI document: 1.77.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -645,6 +645,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto
*/
'originalPath': string;
/**
*
* @type {UserResponseDto}
* @memberof AssetResponseDto
*/
'owner'?: UserResponseDto;
/**
*
* @type {string}
@@ -909,6 +915,21 @@ export const CLIPMode = {
export type CLIPMode = typeof CLIPMode[keyof typeof CLIPMode];
/**
*
* @export
* @enum {string}
*/
export const CQMode = {
Auto: 'auto',
Cqp: 'cqp',
Icq: 'icq'
} as const;
export type CQMode = typeof CQMode[keyof typeof CQMode];
/**
*
* @export
@@ -1031,6 +1052,20 @@ export interface ClassificationConfig {
}
/**
*
* @export
* @enum {string}
*/
export const Colorspace = {
Srgb: 'srgb',
P3: 'p3'
} as const;
export type Colorspace = typeof Colorspace[keyof typeof Colorspace];
/**
*
* @export
@@ -1861,6 +1896,19 @@ export const ModelType = {
export type ModelType = typeof ModelType[keyof typeof ModelType];
/**
*
* @export
* @interface OAuthAuthorizeResponseDto
*/
export interface OAuthAuthorizeResponseDto {
/**
*
* @type {string}
* @memberof OAuthAuthorizeResponseDto
*/
'url': string;
}
/**
*
* @export
@@ -1969,7 +2017,7 @@ export interface PeopleUpdateDto {
*/
export interface PeopleUpdateItem {
/**
* Person date of birth.
* Person date of birth. Note: the mobile app cannot currently set the birth date to null.
* @type {string}
* @memberof PeopleUpdateItem
*/
@@ -2043,7 +2091,7 @@ export interface PersonResponseDto {
*/
export interface PersonUpdateDto {
/**
* Person date of birth.
* Person date of birth. Note: the mobile app cannot currently set the birth date to null.
* @type {string}
* @memberof PersonUpdateDto
*/
@@ -2799,24 +2847,54 @@ export interface SystemConfigFFmpegDto {
* @memberof SystemConfigFFmpegDto
*/
'accel': TranscodeHWAccel;
/**
*
* @type {number}
* @memberof SystemConfigFFmpegDto
*/
'bframes': number;
/**
*
* @type {CQMode}
* @memberof SystemConfigFFmpegDto
*/
'cqMode': CQMode;
/**
*
* @type {number}
* @memberof SystemConfigFFmpegDto
*/
'crf': number;
/**
*
* @type {number}
* @memberof SystemConfigFFmpegDto
*/
'gopSize': number;
/**
*
* @type {string}
* @memberof SystemConfigFFmpegDto
*/
'maxBitrate': string;
/**
*
* @type {number}
* @memberof SystemConfigFFmpegDto
*/
'npl': number;
/**
*
* @type {string}
* @memberof SystemConfigFFmpegDto
*/
'preset': string;
/**
*
* @type {number}
* @memberof SystemConfigFFmpegDto
*/
'refs': number;
/**
*
* @type {AudioCodec}
@@ -2835,6 +2913,12 @@ export interface SystemConfigFFmpegDto {
* @memberof SystemConfigFFmpegDto
*/
'targetVideoCodec': VideoCodec;
/**
*
* @type {boolean}
* @memberof SystemConfigFFmpegDto
*/
'temporalAQ': boolean;
/**
*
* @type {number}
@@ -3120,12 +3204,24 @@ export interface SystemConfigTemplateStorageOptionDto {
* @interface SystemConfigThumbnailDto
*/
export interface SystemConfigThumbnailDto {
/**
*
* @type {Colorspace}
* @memberof SystemConfigThumbnailDto
*/
'colorspace': Colorspace;
/**
*
* @type {number}
* @memberof SystemConfigThumbnailDto
*/
'jpegSize': number;
/**
*
* @type {number}
* @memberof SystemConfigThumbnailDto
*/
'quality': number;
/**
*
* @type {number}
@@ -3133,6 +3229,8 @@ export interface SystemConfigThumbnailDto {
*/
'webpSize': number;
}
/**
*
* @export
@@ -3325,12 +3423,6 @@ export interface UpdateAssetDto {
* @memberof UpdateAssetDto
*/
'isFavorite'?: boolean;
/**
*
* @type {Array<string>}
* @memberof UpdateAssetDto
*/
'tagIds'?: Array<string>;
}
/**
*
@@ -6207,7 +6299,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
};
},
/**
* Update an asset
*
* @param {string} id
* @param {UpdateAssetDto} updateAssetDto
* @param {*} [options] Override http request option.
@@ -6686,7 +6778,7 @@ export const AssetApiFp = function(configuration?: Configuration) {
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
* Update an asset
*
* @param {string} id
* @param {UpdateAssetDto} updateAssetDto
* @param {*} [options] Override http request option.
@@ -6943,7 +7035,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
return localVarFp.serveFile(requestParameters.id, requestParameters.isThumb, requestParameters.isWeb, requestParameters.key, options).then((request) => request(axios, basePath));
},
/**
* Update an asset
*
* @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@@ -7860,7 +7952,7 @@ export class AssetApi extends BaseAPI {
}
/**
* Update an asset
*
* @param {AssetApiUpdateAssetRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
@@ -8890,6 +8982,41 @@ export class JobApi extends BaseAPI {
*/
export const OAuthApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
authorizeOAuth: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'oAuthConfigDto' is not null or undefined
assertParamExists('authorizeOAuth', 'oAuthConfigDto', oAuthConfigDto)
const localVarPath = `/oauth/authorize`;
// 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: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(oAuthConfigDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {OAuthCallbackDto} oAuthCallbackDto
@@ -8926,9 +9053,10 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration
};
},
/**
*
* @deprecated use feature flags and /oauth/authorize
* @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
*/
generateConfig: async (oAuthConfigDto: OAuthConfigDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@@ -9081,6 +9209,16 @@ export const OAuthApiAxiosParamCreator = function (configuration?: Configuration
export const OAuthApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = OAuthApiAxiosParamCreator(configuration)
return {
/**
*
* @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async authorizeOAuth(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<OAuthAuthorizeResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.authorizeOAuth(oAuthConfigDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {OAuthCallbackDto} oAuthCallbackDto
@@ -9092,9 +9230,10 @@ export const OAuthApiFp = function(configuration?: Configuration) {
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @deprecated use feature flags and /oauth/authorize
* @param {OAuthConfigDto} oAuthConfigDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
*/
async generateConfig(oAuthConfigDto: OAuthConfigDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<OAuthConfigResponseDto>> {
@@ -9139,6 +9278,15 @@ export const OAuthApiFp = function(configuration?: Configuration) {
export const OAuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = OAuthApiFp(configuration)
return {
/**
*
* @param {OAuthApiAuthorizeOAuthRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
authorizeOAuth(requestParameters: OAuthApiAuthorizeOAuthRequest, options?: AxiosRequestConfig): AxiosPromise<OAuthAuthorizeResponseDto> {
return localVarFp.authorizeOAuth(requestParameters.oAuthConfigDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {OAuthApiCallbackRequest} requestParameters Request parameters.
@@ -9149,9 +9297,10 @@ export const OAuthApiFactory = function (configuration?: Configuration, basePath
return localVarFp.callback(requestParameters.oAuthCallbackDto, options).then((request) => request(axios, basePath));
},
/**
*
* @deprecated use feature flags and /oauth/authorize
* @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
*/
generateConfig(requestParameters: OAuthApiGenerateConfigRequest, options?: AxiosRequestConfig): AxiosPromise<OAuthConfigResponseDto> {
@@ -9185,6 +9334,20 @@ export const OAuthApiFactory = function (configuration?: Configuration, basePath
};
};
/**
* Request parameters for authorizeOAuth operation in OAuthApi.
* @export
* @interface OAuthApiAuthorizeOAuthRequest
*/
export interface OAuthApiAuthorizeOAuthRequest {
/**
*
* @type {OAuthConfigDto}
* @memberof OAuthApiAuthorizeOAuth
*/
readonly oAuthConfigDto: OAuthConfigDto
}
/**
* Request parameters for callback operation in OAuthApi.
* @export
@@ -9234,6 +9397,17 @@ export interface OAuthApiLinkRequest {
* @extends {BaseAPI}
*/
export class OAuthApi extends BaseAPI {
/**
*
* @param {OAuthApiAuthorizeOAuthRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof OAuthApi
*/
public authorizeOAuth(requestParameters: OAuthApiAuthorizeOAuthRequest, options?: AxiosRequestConfig) {
return OAuthApiFp(this.configuration).authorizeOAuth(requestParameters.oAuthConfigDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {OAuthApiCallbackRequest} requestParameters Request parameters.
@@ -9246,9 +9420,10 @@ export class OAuthApi extends BaseAPI {
}
/**
*
* @deprecated use feature flags and /oauth/authorize
* @param {OAuthApiGenerateConfigRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
* @memberof OAuthApi
*/

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.76.0
* The version of the OpenAPI document: 1.77.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.76.0
* The version of the OpenAPI document: 1.77.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.76.0
* The version of the OpenAPI document: 1.77.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.76.0
* The version of the OpenAPI document: 1.77.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -34,7 +34,7 @@ services:
ports:
- 3003:3003
volumes:
- ../machine-learning/app:/usr/src/app
- ../machine-learning:/usr/src/app
- model-cache:/cache
env_file:
- .env

View File

@@ -68,16 +68,16 @@ Be aware that as this runs inside a container, you need to mount the folder from
```bash title="Upload current directory"
cd /DIRECTORY/WITH/IMAGES
docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
```
```bash title="Upload target directory"
docker run -it --rm -v "/DIRECTORY/WITH/IMAGES:/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
docker run -it --rm -v "/DIRECTORY/WITH/IMAGES:/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
```
```bash title="Create an alias"
alias immich='docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest'
immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
immich upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
```
:::tip Internal networking
@@ -88,7 +88,7 @@ If you are running the CLI container on the same machine as your Immich server,
3. Use `--server http://immich-server:3001` for the upload command instead of the external address.
```bash title="Upload to internal address"
docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://immich-server:3001
docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://immich-server:3001
```
:::

View File

@@ -46,23 +46,22 @@ These environment variables are used by the `docker-compose.yml` file and do **N
## Ports
| Variable | Description | Default | Services |
| :---------------------- | :-------------------- | :-----: | :--------------- |
| `PORT` | Web Port | `3000` | web |
| `SERVER_PORT` | Server Port | `3001` | server |
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
| Variable | Description | Default | Services |
| :---------------------- | :-------------------- | :-------: | :--------------- |
| `PORT` | Web Port | `3000` | web |
| `SERVER_PORT` | Server Port | `3001` | server |
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
## URLs
| Variable | Description | Default | Services |
| :-------------------------------- | :--------------------------- | :-----------------------------------: | :-------------------- |
| `IMMICH_WEB_URL` | Immich Web URL | `http://immich-web:3000` | proxy |
| `IMMICH_SERVER_URL` | Immich Server URL | `http://immich-server:3001` | web, proxy |
| `IMMICH_MACHINE_LEARNING_ENABLED` | Enabled machine learning | `true` | server, microservices |
| `IMMICH_MACHINE_LEARNING_URL` | Immich Machine Learning URL, | `http://immich-machine-learning:3003` | server, microservices |
| `PUBLIC_IMMICH_SERVER_URL` | Public Immich URL | `http://immich-server:3001` | web |
| `IMMICH_API_URL_EXTERNAL` | Immich API URL External | `/api` | web |
| Variable | Description | Default | Services |
| :------------------------- | :---------------------- | :-------------------------: | :--------- |
| `IMMICH_WEB_URL` | Immich Web URL | `http://immich-web:3000` | proxy |
| `IMMICH_SERVER_URL` | Immich Server URL | `http://immich-server:3001` | web, proxy |
| `PUBLIC_IMMICH_SERVER_URL` | Public Immich URL | `http://immich-server:3001` | web |
| `IMMICH_API_URL_EXTERNAL` | Immich API URL External | `/api` | web |
:::info
@@ -178,18 +177,21 @@ Typesense URL example JSON before encoding:
## Machine Learning
| Variable | Description | Default | Services |
| :------------------------------------------ | :----------------------------- | :-------------------: | :--------------- |
| `MACHINE_LEARNING_MIN_FACE_SCORE` | Minimum Face Score | `0.7` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL` | Model TTL | `300` | machine learning |
| `MACHINE_LEARNING_EAGER_STARTUP` | Eager Startup | `true` | machine learning |
| `MACHINE_LEARNING_MIN_TAG_SCORE` | Minimum Tag Score | `0.9` | machine learning |
| `MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL` | Facial Recognition Model | `buffalo_l` | machine learning |
| `MACHINE_LEARNING_CLIP_TEXT_MODEL` | Clip Text Model | `clip-ViT-B-32` | machine learning |
| `MACHINE_LEARNING_CLIP_IMAGE_MODEL` | Clip Image Model | `clip-ViT-B-32` | machine learning |
| `MACHINE_LEARNING_CLASSIFICATION_MODEL` | Classification Model | `microsoft/resnet-50` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | ML Cache Location | `/cache` | machine learning |
| `TRANSFORMERS_CACHE` | ML Transformers Cache Location | `/cache` | machine learning |
| Variable | Description | Default | Services |
| :----------------------------------------------- | :----------------------------------------- | :-----------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Model TTL | `300` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | ML Cache Location | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Request thread pool size | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
:::info
Other machine learning parameters can be tuned from the admin UI.
:::
## Docker Secrets

View File

@@ -1,4 +1,4 @@
FROM python:3.11.4-bullseye@sha256:5b401676aff858495a5c9c726c60b8b73fe52833e9e16eccdb59e93d52741727 as builder
FROM python:3.11-bookworm as builder
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
@@ -14,9 +14,9 @@ COPY poetry.lock pyproject.toml requirements.txt ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
RUN pip install --no-deps -r requirements.txt
FROM python:3.11.4-slim-bullseye@sha256:91d194f58f50594cda71dcd2e8fdefd90e7ecc57d07823813b67c8521e565dcd
FROM python:3.11-slim-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
ENV NODE_ENV=production \
@@ -27,6 +27,7 @@ ENV NODE_ENV=production \
PYTHONPATH=/usr/src
COPY --from=builder /opt/venv /opt/venv
COPY start.sh log_conf.json ./
COPY app .
ENTRYPOINT ["tini", "--"]
CMD ["python", "-m", "app.main"]
CMD ["./start.sh"]

View File

@@ -1,14 +1,18 @@
import logging
import os
from pathlib import Path
import gunicorn
import starlette
from pydantic import BaseSettings
from rich.console import Console
from rich.logging import RichHandler
from .schemas import ModelType
class Settings(BaseSettings):
cache_folder: str = "/cache"
eager_startup: bool = False
model_ttl: int = 0
host: str = "0.0.0.0"
port: int = 3003
@@ -23,6 +27,14 @@ class Settings(BaseSettings):
case_sensitive = False
class LogSettings(BaseSettings):
log_level: str = "info"
no_color: bool = False
class Config:
case_sensitive = False
_clean_name = str.maketrans(":\\/", "___", ".")
@@ -30,4 +42,28 @@ def get_cache_dir(model_name: str, model_type: ModelType) -> Path:
return Path(settings.cache_folder) / model_type.value / model_name.translate(_clean_name)
LOG_LEVELS: dict[str, int] = {
"critical": logging.ERROR,
"error": logging.ERROR,
"warning": logging.WARNING,
"warn": logging.WARNING,
"info": logging.INFO,
"log": logging.INFO,
"debug": logging.DEBUG,
"verbose": logging.DEBUG,
}
settings = Settings()
log_settings = LogSettings()
class CustomRichHandler(RichHandler):
def __init__(self) -> None:
console = Console(color_system="standard", no_color=log_settings.no_color)
super().__init__(
show_path=False, omit_repeated_times=False, console=console, tracebacks_suppress=[gunicorn, starlette]
)
log = logging.getLogger("gunicorn.access")
log.setLevel(LOG_LEVELS.get(log_settings.log_level.lower(), logging.INFO))

View File

@@ -1,17 +1,15 @@
import asyncio
import os
from concurrent.futures import ThreadPoolExecutor
from typing import Any
import orjson
import uvicorn
from fastapi import FastAPI, Form, HTTPException, UploadFile
from fastapi.responses import ORJSONResponse
from starlette.formparsers import MultiPartParser
from app.models.base import InferenceModel
from .config import settings
from .config import log, settings
from .models.cache import ModelCache
from .schemas import (
MessageResponse,
@@ -20,14 +18,20 @@ from .schemas import (
)
MultiPartParser.max_file_size = 2**24 # spools to disk if payload is 16 MiB or larger
app = FastAPI()
def init_state() -> None:
app.state.model_cache = ModelCache(ttl=settings.model_ttl, revalidate=settings.model_ttl > 0)
log.info(
(
"Created in-memory cache with unloading "
f"{f'after {settings.model_ttl}s of inactivity' if settings.model_ttl > 0 else 'disabled'}."
)
)
# asyncio is a huge bottleneck for performance, so we use a thread pool to run blocking code
app.state.thread_pool = ThreadPoolExecutor(settings.request_threads)
app.state.thread_pool = ThreadPoolExecutor(settings.request_threads) if settings.request_threads > 0 else None
log.info(f"Initialized request thread pool with {settings.request_threads} threads.")
@app.on_event("startup")
@@ -66,15 +70,7 @@ async def predict(
async def run(model: InferenceModel, inputs: Any) -> Any:
return await asyncio.get_running_loop().run_in_executor(app.state.thread_pool, model.predict, inputs)
if __name__ == "__main__":
is_dev = os.getenv("NODE_ENV") == "development"
uvicorn.run(
"app.main:app",
host=settings.host,
port=settings.port,
reload=is_dev,
workers=settings.workers,
)
if app.state.thread_pool is not None:
return await asyncio.get_running_loop().run_in_executor(app.state.thread_pool, model.predict, inputs)
else:
return model.predict(inputs)

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import os
import pickle
from abc import ABC, abstractmethod
from pathlib import Path
@@ -9,9 +8,9 @@ from typing import Any
from zipfile import BadZipFile
import onnxruntime as ort
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf # type: ignore
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile # type: ignore
from ..config import get_cache_dir, settings
from ..config import get_cache_dir, log, settings
from ..schemas import ModelType
@@ -37,22 +36,42 @@ class InferenceModel(ABC):
self.provider_options = model_kwargs.pop(
"provider_options", [{"arena_extend_strategy": "kSameAsRequested"}] * len(self.providers)
)
log.debug(
(
f"Setting '{self.model_name}' execution providers to {self.providers}"
"in descending order of preference"
),
)
log.debug(f"Setting execution provider options to {self.provider_options}")
self.sess_options = PicklableSessionOptions()
# avoid thread contention between models
if inter_op_num_threads > 1:
self.sess_options.execution_mode = ort.ExecutionMode.ORT_PARALLEL
log.debug(f"Setting execution_mode to {self.sess_options.execution_mode.name}")
log.debug(f"Setting inter_op_num_threads to {inter_op_num_threads}")
log.debug(f"Setting intra_op_num_threads to {intra_op_num_threads}")
self.sess_options.inter_op_num_threads = inter_op_num_threads
self.sess_options.intra_op_num_threads = intra_op_num_threads
self.sess_options.enable_cpu_mem_arena = False
try:
loader(**model_kwargs)
except (OSError, InvalidProtobuf, BadZipFile):
except (OSError, InvalidProtobuf, BadZipFile, NoSuchFile):
log.warn(
(
f"Failed to load {self.model_type.replace('_', ' ')} model '{self.model_name}'."
"Clearing cache and retrying."
)
)
self.clear_cache()
loader(**model_kwargs)
def download(self, **model_kwargs: Any) -> None:
if not self.cached:
print(f"Downloading {self.model_type.value.replace('_', ' ')} model. This may take a while...")
log.info(
(f"Downloading {self.model_type.replace('_', ' ')} model '{self.model_name}'." "This may take a while.")
)
self._download(**model_kwargs)
def load(self, **model_kwargs: Any) -> None:
@@ -62,7 +81,7 @@ class InferenceModel(ABC):
def predict(self, inputs: Any, **model_kwargs: Any) -> Any:
if not self._loaded:
print(f"Loading {self.model_type.value.replace('_', ' ')} model...")
log.info(f"Loading {self.model_type.replace('_', ' ')} model '{self.model_name}'")
self.load()
if model_kwargs:
self.configure(**model_kwargs)
@@ -109,13 +128,23 @@ class InferenceModel(ABC):
def clear_cache(self) -> None:
if not self.cache_dir.exists():
log.warn(
f"Attempted to clear cache for model '{self.model_name}' but cache directory does not exist.",
)
return
if not rmtree.avoids_symlink_attacks:
raise RuntimeError("Attempted to clear cache, but rmtree is not safe on this platform.")
if self.cache_dir.is_dir():
log.info(f"Cleared cache directory for model '{self.model_name}'.")
rmtree(self.cache_dir)
else:
log.warn(
(
f"Encountered file instead of directory at cache path "
f"for '{self.model_name}'. Removing file and replacing with a directory."
),
)
self.cache_dir.unlink()
self.cache_dir.mkdir(parents=True, exist_ok=True)

View File

@@ -12,6 +12,7 @@ from clip_server.model.tokenization import Tokenizer
from PIL import Image
from torchvision.transforms import CenterCrop, Compose, Normalize, Resize, ToTensor
from ..config import log
from ..schemas import ModelType
from .base import InferenceModel
@@ -105,9 +106,11 @@ class CLIPEncoder(InferenceModel):
if model_name in _MODELS:
return model_name
elif model_name in _ST_TO_JINA_MODEL_NAME:
print(
(f"Warning: Sentence-Transformer model names such as '{model_name}' are no longer supported."),
(f"Using '{_ST_TO_JINA_MODEL_NAME[model_name]}' instead as it is the best match for '{model_name}'."),
log.warn(
(
f"Sentence-Transformer models like '{model_name}' are not supported."
f"Using '{_ST_TO_JINA_MODEL_NAME[model_name]}' instead as it is the best match for '{model_name}'."
),
)
return _ST_TO_JINA_MODEL_NAME[model_name]
else:
@@ -128,6 +131,10 @@ class CLIPEncoder(InferenceModel):
os.remove(file)
return True
@property
def cached(self) -> bool:
return (self.cache_dir / "textual.onnx").is_file() and (self.cache_dir / "visual.onnx").is_file()
# same as `_transform_blob` without `_blob2image`
def _transform_pil_image(n_px: int) -> Compose:

View File

@@ -23,7 +23,7 @@ class FaceRecognizer(InferenceModel):
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
self.min_score = min_score
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(model_name, cache_dir, **model_kwargs)
def _download(self, **model_kwargs: Any) -> None:
@@ -105,4 +105,4 @@ class FaceRecognizer(InferenceModel):
return self.cache_dir.is_dir() and any(self.cache_dir.glob("*.onnx"))
def configure(self, **model_kwargs: Any) -> None:
self.det_model.det_thresh = model_kwargs.get("min_score", self.det_model.det_thresh)
self.det_model.det_thresh = model_kwargs.pop("minScore", self.det_model.det_thresh)

View File

@@ -8,6 +8,7 @@ from optimum.pipelines import pipeline
from PIL import Image
from transformers import AutoImageProcessor
from ..config import log
from ..schemas import ModelType
from .base import InferenceModel
@@ -22,7 +23,7 @@ class ImageClassifier(InferenceModel):
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
self.min_score = min_score
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(model_name, cache_dir, **model_kwargs)
def _download(self, **model_kwargs: Any) -> None:
@@ -35,19 +36,25 @@ class ImageClassifier(InferenceModel):
)
def _load(self, **model_kwargs: Any) -> None:
processor = AutoImageProcessor.from_pretrained(self.cache_dir)
processor = AutoImageProcessor.from_pretrained(self.cache_dir, cache_dir=self.cache_dir)
model_path = self.cache_dir / "model.onnx"
model_kwargs |= {
"cache_dir": self.cache_dir,
"provider": self.providers[0],
"provider_options": self.provider_options[0],
"session_options": self.sess_options,
}
model_path = self.cache_dir / "model.onnx"
if model_path.exists():
model = ORTModelForImageClassification.from_pretrained(self.cache_dir, **model_kwargs)
self.model = pipeline(self.model_type.value, model, feature_extractor=processor)
else:
log.info(
(
f"ONNX model not found in cache directory for '{self.model_name}'."
"Exporting optimized model for future use."
),
)
self.sess_options.optimized_model_filepath = model_path.as_posix()
self.model = pipeline(
self.model_type.value,
@@ -65,4 +72,4 @@ class ImageClassifier(InferenceModel):
return tags
def configure(self, **model_kwargs: Any) -> None:
self.min_score = model_kwargs.get("min_score", self.min_score)
self.min_score = model_kwargs.pop("minScore", self.min_score)

View File

@@ -1,24 +0,0 @@
export MACHINE_LEARNING_CACHE_FOLDER=/tmp/model_cache
export MACHINE_LEARNING_MIN_FACE_SCORE=0.034 # returns 1 face per request; setting this to 0 blows up the number of faces to the thousands
export MACHINE_LEARNING_MIN_TAG_SCORE=0.0
export PID_FILE=/tmp/locust_pid
export LOG_FILE=/tmp/gunicorn.log
export HEADLESS=false
export HOST=127.0.0.1:3003
export CONCURRENCY=4
export NUM_ENDPOINTS=3
export PYTHONPATH=app
gunicorn app.main:app --worker-class uvicorn.workers.UvicornWorker \
--bind $HOST --daemon --error-logfile $LOG_FILE --pid $PID_FILE
while true ; do
echo "Loading models..."
sleep 5
if cat $LOG_FILE | grep -q -E "startup complete"; then break; fi
done
# "users" are assigned only one task, so multiply concurrency by the number of tasks
locust --host http://$HOST --web-host 127.0.0.1 \
--run-time 120s --users $(($CONCURRENCY * $NUM_ENDPOINTS)) $(if $HEADLESS; then echo "--headless"; fi)
if [[ -e $PID_FILE ]]; then kill $(cat $PID_FILE); fi

View File

@@ -1,13 +1,32 @@
from io import BytesIO
import json
from typing import Any
from locust import HttpUser, events, task
from locust.env import Environment
from PIL import Image
from argparse import ArgumentParser
byte_image = BytesIO()
@events.init_command_line_parser.add_listener
def _(parser: ArgumentParser) -> None:
parser.add_argument("--tag-model", type=str, default="microsoft/resnet-50")
parser.add_argument("--clip-model", type=str, default="ViT-B-32::openai")
parser.add_argument("--face-model", type=str, default="buffalo_l")
parser.add_argument("--tag-min-score", type=int, default=0.0,
help="Returns all tags at or above this score. The default returns all tags.")
parser.add_argument("--face-min-score", type=int, default=0.034,
help=("Returns all faces at or above this score. The default returns 1 face per request; "
"setting this to 0 blows up the number of faces to the thousands."))
parser.add_argument("--image-size", type=int, default=1000)
@events.test_start.add_listener
def on_test_start(environment, **kwargs):
def on_test_start(environment: Environment, **kwargs: Any) -> None:
global byte_image
image = Image.new("RGB", (1000, 1000))
assert environment.parsed_options is not None
image = Image.new("RGB", (environment.parsed_options.image_size, environment.parsed_options.image_size))
byte_image = BytesIO()
image.save(byte_image, format="jpeg")
@@ -19,34 +38,55 @@ class InferenceLoadTest(HttpUser):
headers: dict[str, str] = {"Content-Type": "image/jpg"}
# re-use the image across all instances in a process
def on_start(self):
def on_start(self) -> None:
global byte_image
self.data = byte_image.getvalue()
class ClassificationLoadTest(InferenceLoadTest):
class ClassificationFormDataLoadTest(InferenceLoadTest):
@task
def classify(self):
self.client.post(
"/image-classifier/tag-image", data=self.data, headers=self.headers
)
def classify(self) -> None:
data = [
("modelName", self.environment.parsed_options.clip_model),
("modelType", "clip"),
("options", json.dumps({"minScore": self.environment.parsed_options.tag_min_score})),
]
files = {"image": self.data}
self.client.post("/predict", data=data, files=files)
class CLIPLoadTest(InferenceLoadTest):
class CLIPTextFormDataLoadTest(InferenceLoadTest):
@task
def encode_image(self):
self.client.post(
"/sentence-transformer/encode-image",
data=self.data,
headers=self.headers,
)
def encode_text(self) -> None:
data = [
("modelName", self.environment.parsed_options.clip_model),
("modelType", "clip"),
("options", json.dumps({"mode": "text"})),
("text", "test search query")
]
self.client.post("/predict", data=data)
class RecognitionLoadTest(InferenceLoadTest):
class CLIPVisionFormDataLoadTest(InferenceLoadTest):
@task
def recognize(self):
self.client.post(
"/facial-recognition/detect-faces",
data=self.data,
headers=self.headers,
)
def encode_image(self) -> None:
data = [
("modelName", self.environment.parsed_options.clip_model),
("modelType", "clip"),
("options", json.dumps({"mode": "vision"})),
]
files = {"image": self.data}
self.client.post("/predict", data=data, files=files)
class RecognitionFormDataLoadTest(InferenceLoadTest):
@task
def recognize(self) -> None:
data = [
("modelName", self.environment.parsed_options.face_model),
("modelType", "facial-recognition"),
("options", json.dumps({"minScore": self.environment.parsed_options.face_min_score})),
]
files = {"image": self.data}
self.client.post("/predict", data=data, files=files)

View File

@@ -0,0 +1,17 @@
{
"version": 1,
"disable_existing_loggers": true,
"formatters": { "rich": { "show_path": false, "omit_repeated_times": false } },
"handlers": {
"console": {
"class": "app.config.CustomRichHandler",
"formatter": "rich",
"level": "INFO"
}
},
"loggers": {
"gunicorn.access": { "propagate": true },
"gunicorn.error": { "propagate": true }
},
"root": { "handlers": ["console"] }
}

View File

@@ -164,13 +164,13 @@ tests = ["pytest"]
[[package]]
name = "anyio"
version = "3.7.1"
version = "4.0.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"},
{file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"},
{file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"},
{file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"},
]
[package.dependencies]
@@ -178,9 +178,9 @@ idna = ">=2.8"
sniffio = ">=1.1"
[package.extras]
doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"]
test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (<0.22)"]
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.22)"]
[[package]]
name = "async-timeout"
@@ -874,18 +874,18 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6
[[package]]
name = "filelock"
version = "3.12.2"
version = "3.12.3"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"},
{file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"},
{file = "filelock-3.12.3-py3-none-any.whl", hash = "sha256:f067e40ccc40f2b48395a80fcbd4728262fab54e232e090a4063ab804179efeb"},
{file = "filelock-3.12.3.tar.gz", hash = "sha256:0ecc1dd2ec4672a10c8550a8182f1bd0c0a5088470ecd5a125e45f49472fac3d"},
]
[package.extras]
docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"]
[[package]]
name = "flask"
@@ -1453,17 +1453,17 @@ test = ["objgraph", "psutil"]
[[package]]
name = "gunicorn"
version = "20.1.0"
version = "21.2.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.5"
files = [
{file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"},
{file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"},
{file = "gunicorn-21.2.0-py3-none-any.whl", hash = "sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0"},
{file = "gunicorn-21.2.0.tar.gz", hash = "sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033"},
]
[package.dependencies]
setuptools = ">=3.0"
packaging = "*"
[package.extras]
eventlet = ["eventlet (>=0.24.1)"]
@@ -2619,69 +2619,61 @@ files = [
[[package]]
name = "pandas"
version = "2.0.3"
version = "2.1.0"
description = "Powerful data structures for data analysis, time series, and statistics"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "pandas-2.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c7c9f27a4185304c7caf96dc7d91bc60bc162221152de697c98eb0b2648dd8"},
{file = "pandas-2.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f167beed68918d62bffb6ec64f2e1d8a7d297a038f86d4aed056b9493fca407f"},
{file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce0c6f76a0f1ba361551f3e6dceaff06bde7514a374aa43e33b588ec10420183"},
{file = "pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba619e410a21d8c387a1ea6e8a0e49bb42216474436245718d7f2e88a2f8d7c0"},
{file = "pandas-2.0.3-cp310-cp310-win32.whl", hash = "sha256:3ef285093b4fe5058eefd756100a367f27029913760773c8bf1d2d8bebe5d210"},
{file = "pandas-2.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:9ee1a69328d5c36c98d8e74db06f4ad518a1840e8ccb94a4ba86920986bb617e"},
{file = "pandas-2.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b084b91d8d66ab19f5bb3256cbd5ea661848338301940e17f4492b2ce0801fe8"},
{file = "pandas-2.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37673e3bdf1551b95bf5d4ce372b37770f9529743d2498032439371fc7b7eb26"},
{file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9cb1e14fdb546396b7e1b923ffaeeac24e4cedd14266c3497216dd4448e4f2d"},
{file = "pandas-2.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9cd88488cceb7635aebb84809d087468eb33551097d600c6dad13602029c2df"},
{file = "pandas-2.0.3-cp311-cp311-win32.whl", hash = "sha256:694888a81198786f0e164ee3a581df7d505024fbb1f15202fc7db88a71d84ebd"},
{file = "pandas-2.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:6a21ab5c89dcbd57f78d0ae16630b090eec626360085a4148693def5452d8a6b"},
{file = "pandas-2.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9e4da0d45e7f34c069fe4d522359df7d23badf83abc1d1cef398895822d11061"},
{file = "pandas-2.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:32fca2ee1b0d93dd71d979726b12b61faa06aeb93cf77468776287f41ff8fdc5"},
{file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d3624b3ae734490e4d63c430256e716f488c4fcb7c8e9bde2d3aa46c29089"},
{file = "pandas-2.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eae3dc34fa1aa7772dd3fc60270d13ced7346fcbcfee017d3132ec625e23bb0"},
{file = "pandas-2.0.3-cp38-cp38-win32.whl", hash = "sha256:f3421a7afb1a43f7e38e82e844e2bca9a6d793d66c1a7f9f0ff39a795bbc5e02"},
{file = "pandas-2.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:69d7f3884c95da3a31ef82b7618af5710dba95bb885ffab339aad925c3e8ce78"},
{file = "pandas-2.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5247fb1ba347c1261cbbf0fcfba4a3121fbb4029d95d9ef4dc45406620b25c8b"},
{file = "pandas-2.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81af086f4543c9d8bb128328b5d32e9986e0c84d3ee673a2ac6fb57fd14f755e"},
{file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1994c789bf12a7c5098277fb43836ce090f1073858c10f9220998ac74f37c69b"},
{file = "pandas-2.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ec591c48e29226bcbb316e0c1e9423622bc7a4eaf1ef7c3c9fa1a3981f89641"},
{file = "pandas-2.0.3-cp39-cp39-win32.whl", hash = "sha256:04dbdbaf2e4d46ca8da896e1805bc04eb85caa9a82e259e8eed00254d5e0c682"},
{file = "pandas-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:1168574b036cd8b93abc746171c9b4f1b83467438a5e45909fed645cf8692dbc"},
{file = "pandas-2.0.3.tar.gz", hash = "sha256:c02f372a88e0d17f36d3093a644c73cfc1788e876a7c4bcb4020a77512e2043c"},
{file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"},
{file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"},
{file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"},
{file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"},
{file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"},
{file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"},
{file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"},
{file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"},
{file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"},
{file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"},
{file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"},
{file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"},
{file = "pandas-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f100b3876b8c6d1a2c66207288ead435dc71041ee4aea789e55ef0e06408cb"},
{file = "pandas-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f330845ad21c11db51e02d8d69acc9035edfd1116926ff7245c7215db57957"},
{file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9a6ccf0963db88f9b12df6720e55f337447aea217f426a22d71f4213a3099a6"},
{file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99e678180bc59b0c9443314297bddce4ad35727a1a2656dbe585fd78710b3b9"},
{file = "pandas-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b31da36d376d50a1a492efb18097b9101bdbd8b3fbb3f49006e02d4495d4c644"},
{file = "pandas-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0164b85937707ec7f70b34a6c3a578dbf0f50787f910f21ca3b26a7fd3363437"},
{file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"},
]
[package.dependencies]
numpy = [
{version = ">=1.21.0", markers = "python_version >= \"3.10\""},
{version = ">=1.23.2", markers = "python_version >= \"3.11\""},
]
numpy = {version = ">=1.23.2", markers = "python_version >= \"3.11\""}
python-dateutil = ">=2.8.2"
pytz = ">=2020.1"
tzdata = ">=2022.1"
[package.extras]
all = ["PyQt5 (>=5.15.1)", "SQLAlchemy (>=1.4.16)", "beautifulsoup4 (>=4.9.3)", "bottleneck (>=1.3.2)", "brotlipy (>=0.7.0)", "fastparquet (>=0.6.3)", "fsspec (>=2021.07.0)", "gcsfs (>=2021.07.0)", "html5lib (>=1.1)", "hypothesis (>=6.34.2)", "jinja2 (>=3.0.0)", "lxml (>=4.6.3)", "matplotlib (>=3.6.1)", "numba (>=0.53.1)", "numexpr (>=2.7.3)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pandas-gbq (>=0.15.0)", "psycopg2 (>=2.8.6)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "python-snappy (>=0.6.0)", "pyxlsb (>=1.0.8)", "qtpy (>=2.2.0)", "s3fs (>=2021.08.0)", "scipy (>=1.7.1)", "tables (>=3.6.1)", "tabulate (>=0.8.9)", "xarray (>=0.21.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)", "zstandard (>=0.15.2)"]
aws = ["s3fs (>=2021.08.0)"]
clipboard = ["PyQt5 (>=5.15.1)", "qtpy (>=2.2.0)"]
compression = ["brotlipy (>=0.7.0)", "python-snappy (>=0.6.0)", "zstandard (>=0.15.2)"]
computation = ["scipy (>=1.7.1)", "xarray (>=0.21.0)"]
excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.7)", "pyxlsb (>=1.0.8)", "xlrd (>=2.0.1)", "xlsxwriter (>=1.4.3)"]
all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"]
aws = ["s3fs (>=2022.05.0)"]
clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"]
compression = ["zstandard (>=0.17.0)"]
computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"]
consortium-standard = ["dataframe-api-compat (>=0.1.7)"]
excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"]
feather = ["pyarrow (>=7.0.0)"]
fss = ["fsspec (>=2021.07.0)"]
gcp = ["gcsfs (>=2021.07.0)", "pandas-gbq (>=0.15.0)"]
hdf5 = ["tables (>=3.6.1)"]
html = ["beautifulsoup4 (>=4.9.3)", "html5lib (>=1.1)", "lxml (>=4.6.3)"]
mysql = ["SQLAlchemy (>=1.4.16)", "pymysql (>=1.0.2)"]
output-formatting = ["jinja2 (>=3.0.0)", "tabulate (>=0.8.9)"]
fss = ["fsspec (>=2022.05.0)"]
gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"]
hdf5 = ["tables (>=3.7.0)"]
html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"]
mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"]
output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"]
parquet = ["pyarrow (>=7.0.0)"]
performance = ["bottleneck (>=1.3.2)", "numba (>=0.53.1)", "numexpr (>=2.7.1)"]
performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"]
plot = ["matplotlib (>=3.6.1)"]
postgresql = ["SQLAlchemy (>=1.4.16)", "psycopg2 (>=2.8.6)"]
spss = ["pyreadstat (>=1.1.2)"]
sql-other = ["SQLAlchemy (>=1.4.16)"]
test = ["hypothesis (>=6.34.2)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"]
xml = ["lxml (>=4.6.3)"]
postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"]
spss = ["pyreadstat (>=1.1.5)"]
sql-other = ["SQLAlchemy (>=1.4.36)"]
test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"]
xml = ["lxml (>=4.8.0)"]
[[package]]
name = "pathspec"
@@ -3877,13 +3869,13 @@ files = [
[[package]]
name = "tifffile"
version = "2023.8.25"
version = "2023.8.30"
description = "Read and write TIFF files"
optional = false
python-versions = ">=3.9"
files = [
{file = "tifffile-2023.8.25-py3-none-any.whl", hash = "sha256:40318485b59e9acb62e7139f22bd46e6760f92daea562b79900bfce3ee2613b7"},
{file = "tifffile-2023.8.25.tar.gz", hash = "sha256:0a3ebcdfe71eb61a487dd22eaf21ed8962c511e6eb692153c7ac15f81798dfa4"},
{file = "tifffile-2023.8.30-py3-none-any.whl", hash = "sha256:62364eef35a6fdcc7bc2ad6f97dd270f577efb01b31260ff800af76a66c1e145"},
{file = "tifffile-2023.8.30.tar.gz", hash = "sha256:6a8c53b012a286b75d09a1498ab32f202f24cc6270a105b5d5911dc4426f162a"},
]
[package.dependencies]
@@ -3894,13 +3886,13 @@ all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib"
[[package]]
name = "timm"
version = "0.9.5"
version = "0.9.6"
description = "PyTorch Image Models"
optional = false
python-versions = ">=3.7"
files = [
{file = "timm-0.9.5-py3-none-any.whl", hash = "sha256:6e70af3a347bddb4167db46c3252a83c59165332ecf6b3df480d49c22866fa46"},
{file = "timm-0.9.5.tar.gz", hash = "sha256:669835f0030cfb2412c464b7b563bb240d4d41a141226afbbf1b457e4f18cff1"},
{file = "timm-0.9.6-py3-none-any.whl", hash = "sha256:7549a924b86a6151d4083a880c27ae86ce729e1b5c8c6099657217d0a0526a4e"},
{file = "timm-0.9.6.tar.gz", hash = "sha256:6c3c0451b69431de0290eed5662e66b134caf916f1cb9b4aa3b9a13c3d61fd03"},
]
[package.dependencies]
@@ -4126,13 +4118,13 @@ telegram = ["requests"]
[[package]]
name = "transformers"
version = "4.32.0"
version = "4.32.1"
description = "State-of-the-art Machine Learning for JAX, PyTorch and TensorFlow"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "transformers-4.32.0-py3-none-any.whl", hash = "sha256:32d8adf0ed76285508e7fd66657b4448ec1f882599ae6bf6f9c36bd7bf798402"},
{file = "transformers-4.32.0.tar.gz", hash = "sha256:ca510f9688d2fe7347abbbfbd13f2f6dcd3c8349870c8d0ed98beed5f579b354"},
{file = "transformers-4.32.1-py3-none-any.whl", hash = "sha256:b930d3dbd907a3f300cf49e54d63a56f8a0ab16b01a2c2a61ecff37c6de1da08"},
{file = "transformers-4.32.1.tar.gz", hash = "sha256:1edc8ae1de357d97c3d36b04412aa63d55e6fc0c4b39b419a7d380ed947d2252"},
]
[package.dependencies]
@@ -4693,4 +4685,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "6d200d3ea1ccf9fb89f44043e3e0845e70f19aac374b96227559375f44508dc5"
content-hash = "4e97a32e7525cfedbf23892b8c1191b3fe7b4d09b9f043cdb285ed9772862d67"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.76.0"
version = "1.77.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"
@@ -33,13 +33,13 @@ open-clip-torch = "^2.20.0"
python-multipart = "^0.0.6"
orjson = "^3.9.5"
safetensors = "0.3.2"
gunicorn = "^21.1.0"
[tool.poetry.group.dev.dependencies]
mypy = "^1.3.0"
black = "^23.3.0"
pytest = "^7.3.1"
locust = "^2.15.1"
gunicorn = "^20.1.0"
httpx = "^0.24.1"
pytest-asyncio = "^0.21.0"
pytest-cov = "^4.1.0"
@@ -74,6 +74,7 @@ warn_untyped_fields = true
module = [
"huggingface_hub",
"transformers",
"gunicorn",
"cv2",
"insightface.model_zoo",
"insightface.utils.face_align",

13
machine-learning/start.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env sh
export LD_PRELOAD="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
: "${MACHINE_LEARNING_HOST:=0.0.0.0}"
: "${MACHINE_LEARNING_PORT:=3003}"
: "${MACHINE_LEARNING_WORKERS:=1}"
gunicorn app.main:app \
-k uvicorn.workers.UvicornWorker \
-w $MACHINE_LEARNING_WORKERS \
-b $MACHINE_LEARNING_HOST:$MACHINE_LEARNING_PORT \
--log-config-json log_conf.json

View File

@@ -96,3 +96,8 @@ dependencies {
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
}
// This is uncommented in F-Droid build script
//f configurations.all {
//f exclude group: 'com.google.android.gms'
//f }

View File

@@ -55,7 +55,6 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 99,
"android.injected.version.name" => "1.76.0",
"android.injected.version.code" => 100,
"android.injected.version.name" => "1.77.0",
}
)
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')

View File

@@ -10,12 +10,12 @@
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="67.877631">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="63.585931">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="23.895222">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="24.755096">
</testcase>

View File

@@ -193,6 +193,8 @@
"login_form_save_login": "Stay logged in",
"login_form_server_empty": "Enter a server URL.",
"login_form_server_error": "Could not connect to server.",
"login_password_changed_success": "Password updated successfully",
"login_password_changed_error": "There was an error updating your password",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"notification_permission_dialog_cancel": "Cancel",

View File

@@ -169,4 +169,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
COCOAPODS: 1.11.3
COCOAPODS: 1.12.1

View File

@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 113;
CURRENT_PROJECT_VERSION = 116;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 113;
CURRENT_PROJECT_VERSION = 116;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 113;
CURRENT_PROJECT_VERSION = 116;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -59,11 +59,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.73.0</string>
<string>1.76.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>113</string>
<string>116</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.76.0"
version_number: "1.77.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000187">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000243">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.403882">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.611762">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.068392">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.937008">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.988079">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.740416">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="96.47923">
<testcase classname="fastlane.lanes" name="4: build_app" time="93.625943">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="57.517755">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="62.107671">
</testcase>

View File

@@ -113,7 +113,17 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Store.delete(StoreKey.accessToken),
]);
state = state.copyWith(isAuthenticated: false);
state = state.copyWith(
deviceId: "",
userId: "",
userEmail: "",
firstName: '',
lastName: '',
profileImagePath: '',
isAdmin: false,
shouldChangePassword: false,
isAuthenticated: false,
);
} catch (e) {
log.severe("Error logging out $e");
}

View File

@@ -2,13 +2,14 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart';
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/asset.provider.dart';
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
class ChangePasswordForm extends HookConsumerWidget {
const ChangePasswordForm({Key? key}) : super(key: key);
@@ -84,14 +85,35 @@ class ChangePasswordForm extends HookConsumerWidget {
.read(manualUploadProvider.notifier)
.cancelBackup();
ref.read(backupProvider.notifier).cancelBackup();
ref.read(assetProvider.notifier).clearAllAsset();
await ref
.read(assetProvider.notifier)
.clearAllAsset();
ref.read(websocketProvider.notifier).disconnect();
AutoRouter.of(context).replace(const LoginRoute());
AutoRouter.of(context).navigateBack();
ImmichToast.show(
context: context,
msg: "login_password_changed_success".tr(),
toastType: ToastType.success,
gravity: ToastGravity.TOP,
);
} else {
ImmichToast.show(
context: context,
msg: "login_password_changed_error".tr(),
toastType: ToastType.error,
gravity: ToastGravity.TOP,
);
}
}
},
),
TextButton.icon(
icon: const Icon(Icons.arrow_back),
onPressed: () => AutoRouter.of(context).navigateBack(),
label: const Text('Back'),
),
],
),
),

View File

@@ -5,10 +5,10 @@ import 'package:immich_mobile/utils/image_url_builder.dart';
class AssetMarkerIcon extends StatelessWidget {
const AssetMarkerIcon({
Key? key,
super.key,
required this.id,
this.isDarkTheme = false,
}) : super(key: key);
});
final String id;
final bool isDarkTheme;

View File

@@ -122,7 +122,7 @@ class MapAppBar extends HookWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 30),
padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top + 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -41,7 +42,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
// Non-State variables
bool userTappedOnMap = false;
RenderList? _cachedRenderList;
int lastAssetOffsetInSheet = -1;
int assetOffsetInSheet = -1;
late final DraggableScrollableController bottomSheetController;
late final Debounce debounce;
@@ -50,14 +51,16 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
super.initState();
bottomSheetController = DraggableScrollableController();
debounce = Debounce(
const Duration(milliseconds: 200),
const Duration(milliseconds: 100),
);
}
@override
Widget build(BuildContext context) {
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
double maxHeight = MediaQuery.of(context).size.height;
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final bottomPadding =
Platform.isAndroid ? MediaQuery.of(context).padding.bottom - 10 : 0.0;
final maxHeight = MediaQuery.of(context).size.height - bottomPadding;
final isSheetScrolled = useState(false);
final isSheetExpanded = useState(false);
final assetsInBound = useState(<Asset>[]);
@@ -68,7 +71,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
assetsInBound.value = event.assets;
} else if (event is MapPageOnTapEvent) {
userTappedOnMap = true;
lastAssetOffsetInSheet = -1;
assetOffsetInSheet = -1;
bottomSheetController.animateTo(
0.1,
duration: const Duration(milliseconds: 200),
@@ -98,8 +101,8 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
columnOffset = columnOffset < renderElement.totalCount
? columnOffset
: renderElement.totalCount - 1;
lastAssetOffsetInSheet = rowOffset + columnOffset;
final asset = _cachedRenderList?.allAssets?[lastAssetOffsetInSheet];
assetOffsetInSheet = rowOffset + columnOffset;
final asset = _cachedRenderList?.allAssets?[assetOffsetInSheet];
userTappedOnMap = false;
if (!userTappedOnMap && isSheetExpanded.value) {
widget.bottomSheetEventSC.add(
@@ -162,10 +165,10 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
}
void onTapMapButton() {
if (lastAssetOffsetInSheet != -1) {
if (assetOffsetInSheet != -1) {
widget.bottomSheetEventSC.add(
MapPageZoomToAsset(
_cachedRenderList?.allAssets?[lastAssetOffsetInSheet],
_cachedRenderList?.allAssets?[assetOffsetInSheet],
),
);
}
@@ -176,7 +179,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
? "${assetsInBound.value.length} photo${assetsInBound.value.length > 1 ? "s" : ""}"
: "map_no_assets_in_bounds".tr();
final dragHandle = Container(
height: 75,
height: 60,
width: double.infinity,
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
@@ -187,9 +190,9 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 12),
const SizedBox(height: 5),
const CustomDraggingHandle(),
const SizedBox(height: 12),
const SizedBox(height: 15),
Text(
textToDisplay,
style: TextStyle(
@@ -199,6 +202,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
),
),
Divider(
height: 10,
color: Theme.of(context)
.textTheme
.displayLarge
@@ -226,6 +230,7 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
);
return SingleChildScrollView(
controller: scrollController,
physics: const ClampingScrollPhysics(),
child: dragHandle,
);
}
@@ -238,118 +243,125 @@ class AssetsInBoundBottomSheetState extends ConsumerState<MapPageBottomSheet> {
if (!sheetExtended) {
// reset state
userTappedOnMap = false;
lastAssetOffsetInSheet = -1;
assetOffsetInSheet = -1;
isSheetScrolled.value = false;
}
return true;
},
child: Stack(
children: [
DraggableScrollableSheet(
controller: bottomSheetController,
initialChildSize: 0.1,
minChildSize: 0.1,
maxChildSize: 0.55,
snap: true,
builder: (
BuildContext context,
ScrollController scrollController,
) {
return Card(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
surfaceTintColor: Colors.transparent,
elevation: 18.0,
margin: const EdgeInsets.all(0),
child: Column(
children: [
buildDragHandle(scrollController),
if (isSheetExpanded.value && assetsInBound.value.isNotEmpty)
ref
.watch(
renderListProvider(
assetsInBound.value,
),
)
.when(
data: (renderList) {
_cachedRenderList = renderList;
final assetGrid = ImmichAssetGrid(
shrinkWrap: true,
renderList: renderList,
showDragScroll: false,
selectionActive: widget.selectionEnabled,
showMultiSelectIndicator: false,
listener: widget.selectionlistener,
visibleItemsListener: visibleItemsListener,
);
child: Padding(
padding: EdgeInsets.only(
bottom: bottomPadding,
),
child: Stack(
children: [
DraggableScrollableSheet(
controller: bottomSheetController,
initialChildSize: 0.1,
minChildSize: 0.1,
maxChildSize: 0.55,
snap: true,
builder: (
BuildContext context,
ScrollController scrollController,
) {
return Card(
color: isDarkMode ? Colors.grey[900] : Colors.grey[100],
surfaceTintColor: Colors.transparent,
elevation: 18.0,
margin: const EdgeInsets.all(0),
child: Column(
children: [
buildDragHandle(scrollController),
if (isSheetExpanded.value &&
assetsInBound.value.isNotEmpty)
ref
.watch(
renderListProvider(
assetsInBound.value,
),
)
.when(
data: (renderList) {
_cachedRenderList = renderList;
final assetGrid = ImmichAssetGrid(
shrinkWrap: true,
renderList: renderList,
showDragScroll: false,
selectionActive: widget.selectionEnabled,
showMultiSelectIndicator: false,
listener: widget.selectionlistener,
visibleItemsListener: visibleItemsListener,
);
return Expanded(child: assetGrid);
},
error: (error, stackTrace) {
log.warning(
"Cannot get assets in the current map bounds ${error.toString()}",
error,
stackTrace,
);
return const SizedBox.shrink();
},
loading: () => const SizedBox.shrink(),
return Expanded(child: assetGrid);
},
error: (error, stackTrace) {
log.warning(
"Cannot get assets in the current map bounds ${error.toString()}",
error,
stackTrace,
);
return const SizedBox.shrink();
},
loading: () => const SizedBox.shrink(),
),
if (isSheetExpanded.value && assetsInBound.value.isEmpty)
Expanded(
child: SingleChildScrollView(
child: buildNoPhotosWidget(),
),
if (isSheetExpanded.value && assetsInBound.value.isEmpty)
Expanded(
child: SingleChildScrollView(
child: buildNoPhotosWidget(),
),
),
],
],
),
);
},
),
Positioned(
bottom: maxHeight * currentExtend.value,
left: 0,
child: GestureDetector(
onTap: () => launchUrl(
Uri.parse('https://openstreetmap.org/copyright'),
),
);
},
),
Positioned(
bottom: maxHeight * currentExtend.value,
left: 0,
child: GestureDetector(
onTap: () => launchUrl(
Uri.parse('https://openstreetmap.org/copyright'),
),
child: ColoredBox(
color:
(widget.isDarkTheme ? Colors.grey[900] : Colors.grey[100])!,
child: Padding(
padding: const EdgeInsets.all(3),
child: Text(
'© OpenStreetMap contributors',
style: TextStyle(
fontSize: 6,
color: !widget.isDarkTheme
? Colors.grey[900]
: Colors.grey[100],
child: ColoredBox(
color: (widget.isDarkTheme
? Colors.grey[900]
: Colors.grey[100])!,
child: Padding(
padding: const EdgeInsets.all(3),
child: Text(
'© OpenStreetMap contributors',
style: TextStyle(
fontSize: 6,
color: !widget.isDarkTheme
? Colors.grey[900]
: Colors.grey[100],
),
),
),
),
),
),
),
Positioned(
bottom: maxHeight * (0.14 + (currentExtend.value - 0.1)),
right: 15,
child: ElevatedButton(
onPressed: () =>
widget.bottomSheetEventSC.add(const MapPageZoomToLocation()),
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(12),
),
child: const Icon(
Icons.my_location,
size: 22,
fill: 1,
Positioned(
bottom: maxHeight * (0.14 + (currentExtend.value - 0.1)),
right: 15,
child: ElevatedButton(
onPressed: () => widget.bottomSheetEventSC
.add(const MapPageZoomToLocation()),
style: ElevatedButton.styleFrom(
shape: const CircleBorder(),
padding: const EdgeInsets.all(12),
),
child: const Icon(
Icons.my_location,
size: 22,
fill: 1,
),
),
),
),
],
],
),
),
);
}

View File

@@ -166,14 +166,15 @@ class MapPageState extends ConsumerState<MapPage> {
final mapMarker = mapMarkerData.value
.firstWhereOrNull((e) => e.asset.id == assetInBottomSheet.id);
if (mapMarker != null) {
const zoomLevel = 16.0;
LatLng? newCenter = mapController.centerBoundsWithPadding(
mapMarker.point,
const Offset(0, -120),
zoomLevel: 6,
zoomLevel: zoomLevel,
);
if (newCenter != null) {
forceAssetUpdate = true;
mapController.move(newCenter, 6);
mapController.move(newCenter, zoomLevel);
}
}
}
@@ -385,6 +386,7 @@ class MapPageState extends ConsumerState<MapPage> {
builder: (ctx) => GestureDetector(
onTap: () => openAssetInViewer(closestAssetMarker.value!.asset),
child: AssetMarkerIcon(
key: Key(closestAssetMarker.value!.asset.remoteId!),
isDarkTheme: isDarkTheme,
id: closestAssetMarker.value!.asset.remoteId!,
),
@@ -421,8 +423,15 @@ class MapPageState extends ConsumerState<MapPage> {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarColor: Colors.black.withOpacity(0.5),
statusBarIconBrightness: Brightness.light,
statusBarColor:
(isDarkTheme ? Colors.black : Colors.white).withOpacity(0.5),
statusBarIconBrightness:
isDarkTheme ? Brightness.light : Brightness.dark,
systemNavigationBarColor:
isDarkTheme ? Colors.grey[900] : Colors.grey[100],
systemNavigationBarIconBrightness:
isDarkTheme ? Brightness.light : Brightness.dark,
systemNavigationBarDividerColor: Colors.transparent,
),
child: Theme(
// Override app theme based on map theme

View File

@@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -27,7 +28,7 @@ class SearchPage extends HookConsumerWidget {
final curatedLocation = ref.watch(getCuratedLocationProvider);
final curatedPeople = ref.watch(getCuratedPeopleProvider);
var isDarkTheme = Theme.of(context).brightness == Brightness.dark;
double imageSize = MediaQuery.of(context).size.width / 3;
double imageSize = math.min(MediaQuery.of(context).size.width / 3, 150);
TextStyle categoryTitleStyle = const TextStyle(
fontWeight: FontWeight.bold,

View File

@@ -37,12 +37,14 @@ doc/BulkIdResponseDto.md
doc/BulkIdsDto.md
doc/CLIPConfig.md
doc/CLIPMode.md
doc/CQMode.md
doc/ChangePasswordDto.md
doc/CheckDuplicateAssetDto.md
doc/CheckDuplicateAssetResponseDto.md
doc/CheckExistingAssetsDto.md
doc/CheckExistingAssetsResponseDto.md
doc/ClassificationConfig.md
doc/Colorspace.md
doc/CreateAlbumDto.md
doc/CreateProfileImageResponseDto.md
doc/CreateTagDto.md
@@ -73,6 +75,7 @@ doc/MemoryLaneResponseDto.md
doc/MergePersonDto.md
doc/ModelType.md
doc/OAuthApi.md
doc/OAuthAuthorizeResponseDto.md
doc/OAuthCallbackDto.md
doc/OAuthConfigDto.md
doc/OAuthConfigResponseDto.md
@@ -197,6 +200,8 @@ lib/model/check_existing_assets_response_dto.dart
lib/model/classification_config.dart
lib/model/clip_config.dart
lib/model/clip_mode.dart
lib/model/colorspace.dart
lib/model/cq_mode.dart
lib/model/create_album_dto.dart
lib/model/create_profile_image_response_dto.dart
lib/model/create_tag_dto.dart
@@ -225,6 +230,7 @@ lib/model/map_marker_response_dto.dart
lib/model/memory_lane_response_dto.dart
lib/model/merge_person_dto.dart
lib/model/model_type.dart
lib/model/o_auth_authorize_response_dto.dart
lib/model/o_auth_callback_dto.dart
lib/model/o_auth_config_dto.dart
lib/model/o_auth_config_response_dto.dart
@@ -322,6 +328,8 @@ test/check_existing_assets_response_dto_test.dart
test/classification_config_test.dart
test/clip_config_test.dart
test/clip_mode_test.dart
test/colorspace_test.dart
test/cq_mode_test.dart
test/create_album_dto_test.dart
test/create_profile_image_response_dto_test.dart
test/create_tag_dto_test.dart
@@ -352,6 +360,7 @@ test/memory_lane_response_dto_test.dart
test/merge_person_dto_test.dart
test/model_type_test.dart
test/o_auth_api_test.dart
test/o_auth_authorize_response_dto_test.dart
test/o_auth_callback_dto_test.dart
test/o_auth_config_dto_test.dart
test/o_auth_config_response_dto_test.dart

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.76.0
- API version: 1.77.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -124,6 +124,7 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
*OAuthApi* | [**authorizeOAuth**](doc//OAuthApi.md#authorizeoauth) | **POST** /oauth/authorize |
*OAuthApi* | [**callback**](doc//OAuthApi.md#callback) | **POST** /oauth/callback |
*OAuthApi* | [**generateConfig**](doc//OAuthApi.md#generateconfig) | **POST** /oauth/config |
*OAuthApi* | [**link**](doc//OAuthApi.md#link) | **POST** /oauth/link |
@@ -210,12 +211,14 @@ Class | Method | HTTP request | Description
- [BulkIdsDto](doc//BulkIdsDto.md)
- [CLIPConfig](doc//CLIPConfig.md)
- [CLIPMode](doc//CLIPMode.md)
- [CQMode](doc//CQMode.md)
- [ChangePasswordDto](doc//ChangePasswordDto.md)
- [CheckDuplicateAssetDto](doc//CheckDuplicateAssetDto.md)
- [CheckDuplicateAssetResponseDto](doc//CheckDuplicateAssetResponseDto.md)
- [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
- [ClassificationConfig](doc//ClassificationConfig.md)
- [Colorspace](doc//Colorspace.md)
- [CreateAlbumDto](doc//CreateAlbumDto.md)
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
- [CreateTagDto](doc//CreateTagDto.md)
@@ -244,6 +247,7 @@ Class | Method | HTTP request | Description
- [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md)
- [MergePersonDto](doc//MergePersonDto.md)
- [ModelType](doc//ModelType.md)
- [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md)
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
- [OAuthConfigDto](doc//OAuthConfigDto.md)
- [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)

View File

@@ -1368,8 +1368,6 @@ Name | Type | Description | Notes
Update an asset
### Example
```dart
import 'package:openapi/api.dart';

View File

@@ -21,6 +21,7 @@ Name | Type | Description | Notes
**livePhotoVideoId** | **String** | | [optional]
**originalFileName** | **String** | |
**originalPath** | **String** | |
**owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional]
**ownerId** | **String** | |
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
**resized** | **bool** | |

14
mobile/openapi/doc/CQMode.md generated Normal file
View File

@@ -0,0 +1,14 @@
# openapi.model.CQMode
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

14
mobile/openapi/doc/Colorspace.md generated Normal file
View File

@@ -0,0 +1,14 @@
# openapi.model.Colorspace
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -9,6 +9,7 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**authorizeOAuth**](OAuthApi.md#authorizeoauth) | **POST** /oauth/authorize |
[**callback**](OAuthApi.md#callback) | **POST** /oauth/callback |
[**generateConfig**](OAuthApi.md#generateconfig) | **POST** /oauth/config |
[**link**](OAuthApi.md#link) | **POST** /oauth/link |
@@ -16,6 +17,47 @@ Method | HTTP request | Description
[**unlink**](OAuthApi.md#unlink) | **POST** /oauth/unlink |
# **authorizeOAuth**
> OAuthAuthorizeResponseDto authorizeOAuth(oAuthConfigDto)
### Example
```dart
import 'package:openapi/api.dart';
final api_instance = OAuthApi();
final oAuthConfigDto = OAuthConfigDto(); // OAuthConfigDto |
try {
final result = api_instance.authorizeOAuth(oAuthConfigDto);
print(result);
} catch (e) {
print('Exception when calling OAuthApi->authorizeOAuth: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**oAuthConfigDto** | [**OAuthConfigDto**](OAuthConfigDto.md)| |
### Return type
[**OAuthAuthorizeResponseDto**](OAuthAuthorizeResponseDto.md)
### Authorization
No authorization required
### HTTP request headers
- **Content-Type**: application/json
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **callback**
> LoginResponseDto callback(oAuthCallbackDto)
@@ -62,6 +104,8 @@ No authorization required
@deprecated use feature flags and /oauth/authorize
### Example
```dart
import 'package:openapi/api.dart';

View File

@@ -0,0 +1,15 @@
# openapi.model.OAuthAuthorizeResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**url** | **String** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**birthDate** | [**DateTime**](DateTime.md) | Person date of birth. | [optional]
**birthDate** | [**DateTime**](DateTime.md) | Person date of birth. Note: the mobile app cannot currently set the birth date to null. | [optional]
**featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]
**id** | **String** | Person id. |
**isHidden** | **bool** | Person visibility | [optional]

View File

@@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**birthDate** | [**DateTime**](DateTime.md) | Person date of birth. | [optional]
**birthDate** | [**DateTime**](DateTime.md) | Person date of birth. Note: the mobile app cannot currently set the birth date to null. | [optional]
**featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]
**isHidden** | **bool** | Person visibility | [optional]
**name** | **String** | Person name. | [optional]

View File

@@ -9,12 +9,18 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**accel** | [**TranscodeHWAccel**](TranscodeHWAccel.md) | |
**bframes** | **int** | |
**cqMode** | [**CQMode**](CQMode.md) | |
**crf** | **int** | |
**gopSize** | **int** | |
**maxBitrate** | **String** | |
**npl** | **int** | |
**preset** | **String** | |
**refs** | **int** | |
**targetAudioCodec** | [**AudioCodec**](AudioCodec.md) | |
**targetResolution** | **String** | |
**targetVideoCodec** | [**VideoCodec**](VideoCodec.md) | |
**temporalAQ** | **bool** | |
**threads** | **int** | |
**tonemap** | [**ToneMapping**](ToneMapping.md) | |
**transcode** | [**TranscodePolicy**](TranscodePolicy.md) | |

View File

@@ -8,7 +8,9 @@ import 'package:openapi/api.dart';
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**colorspace** | [**Colorspace**](Colorspace.md) | |
**jpegSize** | **int** | |
**quality** | **int** | |
**webpSize** | **int** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -11,7 +11,6 @@ Name | Type | Description | Notes
**description** | **String** | | [optional]
**isArchived** | **bool** | | [optional]
**isFavorite** | **bool** | | [optional]
**tagIds** | **List<String>** | | [optional] [default to const []]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@@ -73,12 +73,14 @@ part 'model/bulk_id_response_dto.dart';
part 'model/bulk_ids_dto.dart';
part 'model/clip_config.dart';
part 'model/clip_mode.dart';
part 'model/cq_mode.dart';
part 'model/change_password_dto.dart';
part 'model/check_duplicate_asset_dto.dart';
part 'model/check_duplicate_asset_response_dto.dart';
part 'model/check_existing_assets_dto.dart';
part 'model/check_existing_assets_response_dto.dart';
part 'model/classification_config.dart';
part 'model/colorspace.dart';
part 'model/create_album_dto.dart';
part 'model/create_profile_image_response_dto.dart';
part 'model/create_tag_dto.dart';
@@ -107,6 +109,7 @@ part 'model/map_marker_response_dto.dart';
part 'model/memory_lane_response_dto.dart';
part 'model/merge_person_dto.dart';
part 'model/model_type.dart';
part 'model/o_auth_authorize_response_dto.dart';
part 'model/o_auth_callback_dto.dart';
part 'model/o_auth_config_dto.dart';
part 'model/o_auth_config_response_dto.dart';

View File

@@ -1384,10 +1384,7 @@ class AssetApi {
return null;
}
/// Update an asset
///
/// Note: This method returns the HTTP [Response].
///
/// Performs an HTTP 'PUT /asset/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
@@ -1419,8 +1416,6 @@ class AssetApi {
);
}
/// Update an asset
///
/// Parameters:
///
/// * [String] id (required):

View File

@@ -16,6 +16,53 @@ class OAuthApi {
final ApiClient apiClient;
/// Performs an HTTP 'POST /oauth/authorize' operation and returns the [Response].
/// Parameters:
///
/// * [OAuthConfigDto] oAuthConfigDto (required):
Future<Response> authorizeOAuthWithHttpInfo(OAuthConfigDto oAuthConfigDto,) async {
// ignore: prefer_const_declarations
final path = r'/oauth/authorize';
// ignore: prefer_final_locals
Object? postBody = oAuthConfigDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [OAuthConfigDto] oAuthConfigDto (required):
Future<OAuthAuthorizeResponseDto?> authorizeOAuth(OAuthConfigDto oAuthConfigDto,) async {
final response = await authorizeOAuthWithHttpInfo(oAuthConfigDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'OAuthAuthorizeResponseDto',) as OAuthAuthorizeResponseDto;
}
return null;
}
/// Performs an HTTP 'POST /oauth/callback' operation and returns the [Response].
/// Parameters:
///
@@ -63,7 +110,10 @@ class OAuthApi {
return null;
}
/// Performs an HTTP 'POST /oauth/config' operation and returns the [Response].
/// @deprecated use feature flags and /oauth/authorize
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [OAuthConfigDto] oAuthConfigDto (required):
@@ -92,6 +142,8 @@ class OAuthApi {
);
}
/// @deprecated use feature flags and /oauth/authorize
///
/// Parameters:
///
/// * [OAuthConfigDto] oAuthConfigDto (required):

View File

@@ -239,6 +239,8 @@ class ApiClient {
return CLIPConfig.fromJson(value);
case 'CLIPMode':
return CLIPModeTypeTransformer().decode(value);
case 'CQMode':
return CQModeTypeTransformer().decode(value);
case 'ChangePasswordDto':
return ChangePasswordDto.fromJson(value);
case 'CheckDuplicateAssetDto':
@@ -251,6 +253,8 @@ class ApiClient {
return CheckExistingAssetsResponseDto.fromJson(value);
case 'ClassificationConfig':
return ClassificationConfig.fromJson(value);
case 'Colorspace':
return ColorspaceTypeTransformer().decode(value);
case 'CreateAlbumDto':
return CreateAlbumDto.fromJson(value);
case 'CreateProfileImageResponseDto':
@@ -307,6 +311,8 @@ class ApiClient {
return MergePersonDto.fromJson(value);
case 'ModelType':
return ModelTypeTypeTransformer().decode(value);
case 'OAuthAuthorizeResponseDto':
return OAuthAuthorizeResponseDto.fromJson(value);
case 'OAuthCallbackDto':
return OAuthCallbackDto.fromJson(value);
case 'OAuthConfigDto':

View File

@@ -67,6 +67,12 @@ String parameterToString(dynamic value) {
if (value is CLIPMode) {
return CLIPModeTypeTransformer().encode(value).toString();
}
if (value is CQMode) {
return CQModeTypeTransformer().encode(value).toString();
}
if (value is Colorspace) {
return ColorspaceTypeTransformer().encode(value).toString();
}
if (value is DeleteAssetStatus) {
return DeleteAssetStatusTypeTransformer().encode(value).toString();
}

View File

@@ -26,6 +26,7 @@ class AssetResponseDto {
this.livePhotoVideoId,
required this.originalFileName,
required this.originalPath,
this.owner,
required this.ownerId,
this.people = const [],
required this.resized,
@@ -69,6 +70,14 @@ class AssetResponseDto {
String originalPath;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
UserResponseDto? owner;
String ownerId;
List<PersonResponseDto> people;
@@ -107,6 +116,7 @@ class AssetResponseDto {
other.livePhotoVideoId == livePhotoVideoId &&
other.originalFileName == originalFileName &&
other.originalPath == originalPath &&
other.owner == owner &&
other.ownerId == ownerId &&
other.people == people &&
other.resized == resized &&
@@ -132,6 +142,7 @@ class AssetResponseDto {
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(originalFileName.hashCode) +
(originalPath.hashCode) +
(owner == null ? 0 : owner!.hashCode) +
(ownerId.hashCode) +
(people.hashCode) +
(resized.hashCode) +
@@ -142,7 +153,7 @@ class AssetResponseDto {
(updatedAt.hashCode);
@override
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, livePhotoVideoId=$livePhotoVideoId, originalFileName=$originalFileName, originalPath=$originalPath, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, livePhotoVideoId=$livePhotoVideoId, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -167,6 +178,11 @@ class AssetResponseDto {
}
json[r'originalFileName'] = this.originalFileName;
json[r'originalPath'] = this.originalPath;
if (this.owner != null) {
json[r'owner'] = this.owner;
} else {
// json[r'owner'] = null;
}
json[r'ownerId'] = this.ownerId;
json[r'people'] = this.people;
json[r'resized'] = this.resized;
@@ -207,6 +223,7 @@ class AssetResponseDto {
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType<String>(json, r'ownerId')!,
people: PersonResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType<bool>(json, r'resized')!,

85
mobile/openapi/lib/model/colorspace.dart generated Normal file
View File

@@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class Colorspace {
/// Instantiate a new enum with the provided [value].
const Colorspace._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const srgb = Colorspace._(r'srgb');
static const p3 = Colorspace._(r'p3');
/// List of all possible values in this [enum][Colorspace].
static const values = <Colorspace>[
srgb,
p3,
];
static Colorspace? fromJson(dynamic value) => ColorspaceTypeTransformer().decode(value);
static List<Colorspace>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <Colorspace>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = Colorspace.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [Colorspace] to String,
/// and [decode] dynamic data back to [Colorspace].
class ColorspaceTypeTransformer {
factory ColorspaceTypeTransformer() => _instance ??= const ColorspaceTypeTransformer._();
const ColorspaceTypeTransformer._();
String encode(Colorspace data) => data.value;
/// Decodes a [dynamic value][data] to a Colorspace.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
Colorspace? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'srgb': return Colorspace.srgb;
case r'p3': return Colorspace.p3;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [ColorspaceTypeTransformer] instance.
static ColorspaceTypeTransformer? _instance;
}

88
mobile/openapi/lib/model/cq_mode.dart generated Normal file
View File

@@ -0,0 +1,88 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class CQMode {
/// Instantiate a new enum with the provided [value].
const CQMode._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const auto = CQMode._(r'auto');
static const cqp = CQMode._(r'cqp');
static const icq = CQMode._(r'icq');
/// List of all possible values in this [enum][CQMode].
static const values = <CQMode>[
auto,
cqp,
icq,
];
static CQMode? fromJson(dynamic value) => CQModeTypeTransformer().decode(value);
static List<CQMode>? listFromJson(dynamic json, {bool growable = false,}) {
final result = <CQMode>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = CQMode.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [CQMode] to String,
/// and [decode] dynamic data back to [CQMode].
class CQModeTypeTransformer {
factory CQModeTypeTransformer() => _instance ??= const CQModeTypeTransformer._();
const CQModeTypeTransformer._();
String encode(CQMode data) => data.value;
/// Decodes a [dynamic value][data] to a CQMode.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
CQMode? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'auto': return CQMode.auto;
case r'cqp': return CQMode.cqp;
case r'icq': return CQMode.icq;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [CQModeTypeTransformer] instance.
static CQModeTypeTransformer? _instance;
}

View File

@@ -57,8 +57,8 @@ class MapMarkerResponseDto {
return MapMarkerResponseDto(
id: mapValueOfType<String>(json, r'id')!,
lat: mapValueOfType<double>(json, r'lat')!,
lon: mapValueOfType<double>(json, r'lon')!,
lat: (mapValueOfType<num>(json, r'lat')!).toDouble(),
lon: (mapValueOfType<num>(json, r'lon')!).toDouble(),
);
}
return null;

View File

@@ -0,0 +1,98 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class OAuthAuthorizeResponseDto {
/// Returns a new [OAuthAuthorizeResponseDto] instance.
OAuthAuthorizeResponseDto({
required this.url,
});
String url;
@override
bool operator ==(Object other) => identical(this, other) || other is OAuthAuthorizeResponseDto &&
other.url == url;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(url.hashCode);
@override
String toString() => 'OAuthAuthorizeResponseDto[url=$url]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'url'] = this.url;
return json;
}
/// Returns a new [OAuthAuthorizeResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static OAuthAuthorizeResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return OAuthAuthorizeResponseDto(
url: mapValueOfType<String>(json, r'url')!,
);
}
return null;
}
static List<OAuthAuthorizeResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <OAuthAuthorizeResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = OAuthAuthorizeResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, OAuthAuthorizeResponseDto> mapFromJson(dynamic json) {
final map = <String, OAuthAuthorizeResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = OAuthAuthorizeResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of OAuthAuthorizeResponseDto-objects as value to a dart map
static Map<String, List<OAuthAuthorizeResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<OAuthAuthorizeResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = OAuthAuthorizeResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'url',
};
}

View File

@@ -20,7 +20,7 @@ class PeopleUpdateItem {
this.name,
});
/// Person date of birth.
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate;
/// Asset is used to get the feature face thumbnail.

View File

@@ -19,7 +19,7 @@ class PersonUpdateDto {
this.name,
});
/// Person date of birth.
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate;
/// Asset is used to get the feature face thumbnail.

View File

@@ -14,12 +14,18 @@ class SystemConfigFFmpegDto {
/// Returns a new [SystemConfigFFmpegDto] instance.
SystemConfigFFmpegDto({
required this.accel,
required this.bframes,
required this.cqMode,
required this.crf,
required this.gopSize,
required this.maxBitrate,
required this.npl,
required this.preset,
required this.refs,
required this.targetAudioCodec,
required this.targetResolution,
required this.targetVideoCodec,
required this.temporalAQ,
required this.threads,
required this.tonemap,
required this.transcode,
@@ -28,18 +34,30 @@ class SystemConfigFFmpegDto {
TranscodeHWAccel accel;
int bframes;
CQMode cqMode;
int crf;
int gopSize;
String maxBitrate;
int npl;
String preset;
int refs;
AudioCodec targetAudioCodec;
String targetResolution;
VideoCodec targetVideoCodec;
bool temporalAQ;
int threads;
ToneMapping tonemap;
@@ -51,12 +69,18 @@ class SystemConfigFFmpegDto {
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigFFmpegDto &&
other.accel == accel &&
other.bframes == bframes &&
other.cqMode == cqMode &&
other.crf == crf &&
other.gopSize == gopSize &&
other.maxBitrate == maxBitrate &&
other.npl == npl &&
other.preset == preset &&
other.refs == refs &&
other.targetAudioCodec == targetAudioCodec &&
other.targetResolution == targetResolution &&
other.targetVideoCodec == targetVideoCodec &&
other.temporalAQ == temporalAQ &&
other.threads == threads &&
other.tonemap == tonemap &&
other.transcode == transcode &&
@@ -66,29 +90,41 @@ class SystemConfigFFmpegDto {
int get hashCode =>
// ignore: unnecessary_parenthesis
(accel.hashCode) +
(bframes.hashCode) +
(cqMode.hashCode) +
(crf.hashCode) +
(gopSize.hashCode) +
(maxBitrate.hashCode) +
(npl.hashCode) +
(preset.hashCode) +
(refs.hashCode) +
(targetAudioCodec.hashCode) +
(targetResolution.hashCode) +
(targetVideoCodec.hashCode) +
(temporalAQ.hashCode) +
(threads.hashCode) +
(tonemap.hashCode) +
(transcode.hashCode) +
(twoPass.hashCode);
@override
String toString() => 'SystemConfigFFmpegDto[accel=$accel, crf=$crf, maxBitrate=$maxBitrate, preset=$preset, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]';
String toString() => 'SystemConfigFFmpegDto[accel=$accel, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, npl=$npl, preset=$preset, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'accel'] = this.accel;
json[r'bframes'] = this.bframes;
json[r'cqMode'] = this.cqMode;
json[r'crf'] = this.crf;
json[r'gopSize'] = this.gopSize;
json[r'maxBitrate'] = this.maxBitrate;
json[r'npl'] = this.npl;
json[r'preset'] = this.preset;
json[r'refs'] = this.refs;
json[r'targetAudioCodec'] = this.targetAudioCodec;
json[r'targetResolution'] = this.targetResolution;
json[r'targetVideoCodec'] = this.targetVideoCodec;
json[r'temporalAQ'] = this.temporalAQ;
json[r'threads'] = this.threads;
json[r'tonemap'] = this.tonemap;
json[r'transcode'] = this.transcode;
@@ -105,12 +141,18 @@ class SystemConfigFFmpegDto {
return SystemConfigFFmpegDto(
accel: TranscodeHWAccel.fromJson(json[r'accel'])!,
bframes: mapValueOfType<int>(json, r'bframes')!,
cqMode: CQMode.fromJson(json[r'cqMode'])!,
crf: mapValueOfType<int>(json, r'crf')!,
gopSize: mapValueOfType<int>(json, r'gopSize')!,
maxBitrate: mapValueOfType<String>(json, r'maxBitrate')!,
npl: mapValueOfType<int>(json, r'npl')!,
preset: mapValueOfType<String>(json, r'preset')!,
refs: mapValueOfType<int>(json, r'refs')!,
targetAudioCodec: AudioCodec.fromJson(json[r'targetAudioCodec'])!,
targetResolution: mapValueOfType<String>(json, r'targetResolution')!,
targetVideoCodec: VideoCodec.fromJson(json[r'targetVideoCodec'])!,
temporalAQ: mapValueOfType<bool>(json, r'temporalAQ')!,
threads: mapValueOfType<int>(json, r'threads')!,
tonemap: ToneMapping.fromJson(json[r'tonemap'])!,
transcode: TranscodePolicy.fromJson(json[r'transcode'])!,
@@ -163,12 +205,18 @@ class SystemConfigFFmpegDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'accel',
'bframes',
'cqMode',
'crf',
'gopSize',
'maxBitrate',
'npl',
'preset',
'refs',
'targetAudioCodec',
'targetResolution',
'targetVideoCodec',
'temporalAQ',
'threads',
'tonemap',
'transcode',

View File

@@ -13,31 +13,43 @@ part of openapi.api;
class SystemConfigThumbnailDto {
/// Returns a new [SystemConfigThumbnailDto] instance.
SystemConfigThumbnailDto({
required this.colorspace,
required this.jpegSize,
required this.quality,
required this.webpSize,
});
Colorspace colorspace;
int jpegSize;
int quality;
int webpSize;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigThumbnailDto &&
other.colorspace == colorspace &&
other.jpegSize == jpegSize &&
other.quality == quality &&
other.webpSize == webpSize;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(colorspace.hashCode) +
(jpegSize.hashCode) +
(quality.hashCode) +
(webpSize.hashCode);
@override
String toString() => 'SystemConfigThumbnailDto[jpegSize=$jpegSize, webpSize=$webpSize]';
String toString() => 'SystemConfigThumbnailDto[colorspace=$colorspace, jpegSize=$jpegSize, quality=$quality, webpSize=$webpSize]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'colorspace'] = this.colorspace;
json[r'jpegSize'] = this.jpegSize;
json[r'quality'] = this.quality;
json[r'webpSize'] = this.webpSize;
return json;
}
@@ -50,7 +62,9 @@ class SystemConfigThumbnailDto {
final json = value.cast<String, dynamic>();
return SystemConfigThumbnailDto(
colorspace: Colorspace.fromJson(json[r'colorspace'])!,
jpegSize: mapValueOfType<int>(json, r'jpegSize')!,
quality: mapValueOfType<int>(json, r'quality')!,
webpSize: mapValueOfType<int>(json, r'webpSize')!,
);
}
@@ -99,7 +113,9 @@ class SystemConfigThumbnailDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'colorspace',
'jpegSize',
'quality',
'webpSize',
};
}

View File

@@ -16,7 +16,6 @@ class UpdateAssetDto {
this.description,
this.isArchived,
this.isFavorite,
this.tagIds = const [],
});
///
@@ -43,25 +42,21 @@ class UpdateAssetDto {
///
bool? isFavorite;
List<String> tagIds;
@override
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
other.description == description &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite &&
other.tagIds == tagIds;
other.isFavorite == isFavorite;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(description == null ? 0 : description!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(tagIds.hashCode);
(isFavorite == null ? 0 : isFavorite!.hashCode);
@override
String toString() => 'UpdateAssetDto[description=$description, isArchived=$isArchived, isFavorite=$isFavorite, tagIds=$tagIds]';
String toString() => 'UpdateAssetDto[description=$description, isArchived=$isArchived, isFavorite=$isFavorite]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -80,7 +75,6 @@ class UpdateAssetDto {
} else {
// json[r'isFavorite'] = null;
}
json[r'tagIds'] = this.tagIds;
return json;
}
@@ -95,9 +89,6 @@ class UpdateAssetDto {
description: mapValueOfType<String>(json, r'description'),
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
tagIds: json[r'tagIds'] is List
? (json[r'tagIds'] as List).cast<String>()
: const [],
);
}
return null;

View File

@@ -144,8 +144,6 @@ void main() {
// TODO
});
// Update an asset
//
//Future<AssetResponseDto> updateAsset(String id, UpdateAssetDto updateAssetDto) async
test('test updateAsset', () async {
// TODO

View File

@@ -82,6 +82,11 @@ void main() {
// TODO
});
// UserResponseDto owner
test('to test the property `owner`', () async {
// TODO
});
// String ownerId
test('to test the property `ownerId`', () async {
// TODO

21
mobile/openapi/test/colorspace_test.dart generated Normal file
View File

@@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for Colorspace
void main() {
group('test Colorspace', () {
});
}

21
mobile/openapi/test/cq_mode_test.dart generated Normal file
View File

@@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for CQMode
void main() {
group('test CQMode', () {
});
}

View File

@@ -17,11 +17,18 @@ void main() {
// final instance = OAuthApi();
group('tests for OAuthApi', () {
//Future<OAuthAuthorizeResponseDto> authorizeOAuth(OAuthConfigDto oAuthConfigDto) async
test('test authorizeOAuth', () async {
// TODO
});
//Future<LoginResponseDto> callback(OAuthCallbackDto oAuthCallbackDto) async
test('test callback', () async {
// TODO
});
// @deprecated use feature flags and /oauth/authorize
//
//Future<OAuthConfigResponseDto> generateConfig(OAuthConfigDto oAuthConfigDto) async
test('test generateConfig', () async {
// TODO

View File

@@ -0,0 +1,27 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for OAuthAuthorizeResponseDto
void main() {
// final instance = OAuthAuthorizeResponseDto();
group('test OAuthAuthorizeResponseDto', () {
// String url
test('to test the property `url`', () async {
// TODO
});
});
}

View File

@@ -16,7 +16,7 @@ void main() {
// final instance = PeopleUpdateItem();
group('test PeopleUpdateItem', () {
// Person date of birth.
// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
// DateTime birthDate
test('to test the property `birthDate`', () async {
// TODO

View File

@@ -16,7 +16,7 @@ void main() {
// final instance = PersonUpdateDto();
group('test PersonUpdateDto', () {
// Person date of birth.
// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
// DateTime birthDate
test('to test the property `birthDate`', () async {
// TODO

View File

@@ -21,21 +21,46 @@ void main() {
// TODO
});
// int bframes
test('to test the property `bframes`', () async {
// TODO
});
// CQMode cqMode
test('to test the property `cqMode`', () async {
// TODO
});
// int crf
test('to test the property `crf`', () async {
// TODO
});
// int gopSize
test('to test the property `gopSize`', () async {
// TODO
});
// String maxBitrate
test('to test the property `maxBitrate`', () async {
// TODO
});
// int npl
test('to test the property `npl`', () async {
// TODO
});
// String preset
test('to test the property `preset`', () async {
// TODO
});
// int refs
test('to test the property `refs`', () async {
// TODO
});
// AudioCodec targetAudioCodec
test('to test the property `targetAudioCodec`', () async {
// TODO
@@ -51,6 +76,11 @@ void main() {
// TODO
});
// bool temporalAQ
test('to test the property `temporalAQ`', () async {
// TODO
});
// int threads
test('to test the property `threads`', () async {
// TODO

View File

@@ -16,11 +16,21 @@ void main() {
// final instance = SystemConfigThumbnailDto();
group('test SystemConfigThumbnailDto', () {
// Colorspace colorspace
test('to test the property `colorspace`', () async {
// TODO
});
// int jpegSize
test('to test the property `jpegSize`', () async {
// TODO
});
// int quality
test('to test the property `quality`', () async {
// TODO
});
// int webpSize
test('to test the property `webpSize`', () async {
// TODO

View File

@@ -31,11 +31,6 @@ void main() {
// TODO
});
// List<String> tagIds (default value: const [])
test('to test the property `tagIds`', () async {
// TODO
});
});

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.76.0+99
version: 1.77.0+100
isar_version: &isar_version 3.1.0+1
environment:
@@ -60,6 +60,15 @@ dependencies:
image_picker: ^0.8.5+3 # only used to select user profile image from system gallery -> we can simply select an image from within immich?
logging: ^1.1.0
# This is uncommented in F-Droid build script
# Taken from https://github.com/Myzel394/locus/blob/445013d22ec1d759027d4303bd65b30c5c8588c8/pubspec.yaml#L105
#fdependency_overrides:
#f geolocator_android:
#f git:
#f url: https://github.com/Zverik/flutter-geolocator.git
#f ref: floss
#f path: geolocator_android
dev_dependencies:
flutter_test:
sdk: flutter

View File

@@ -3,5 +3,6 @@
"trailingComma": "all",
"printWidth": 120,
"semi": true,
"organizeImportsSkipDestructiveCodeActions": true
"organizeImportsSkipDestructiveCodeActions": true,
"plugins": ["prettier-plugin-organize-imports"]
}

View File

@@ -2020,7 +2020,6 @@
},
"/asset/{id}": {
"put": {
"description": "Update an asset",
"operationId": "updateAsset",
"parameters": [
{
@@ -2477,6 +2476,37 @@
]
}
},
"/oauth/authorize": {
"post": {
"operationId": "authorizeOAuth",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthConfigDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/OAuthAuthorizeResponseDto"
}
}
},
"description": ""
}
},
"tags": [
"OAuth"
]
}
},
"/oauth/callback": {
"post": {
"operationId": "callback",
@@ -2510,6 +2540,8 @@
},
"/oauth/config": {
"post": {
"deprecated": true,
"description": "@deprecated use feature flags and /oauth/authorize",
"operationId": "generateConfig",
"parameters": [],
"requestBody": {
@@ -4680,7 +4712,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.76.0",
"version": "1.77.0",
"contact": {}
},
"tags": [],
@@ -5176,6 +5208,9 @@
"originalPath": {
"type": "string"
},
"owner": {
"$ref": "#/components/schemas/UserResponseDto"
},
"ownerId": {
"type": "string"
},
@@ -5214,8 +5249,8 @@
"type",
"id",
"deviceAssetId",
"ownerId",
"deviceId",
"ownerId",
"originalPath",
"originalFileName",
"resized",
@@ -5382,6 +5417,14 @@
],
"type": "string"
},
"CQMode": {
"enum": [
"auto",
"cqp",
"icq"
],
"type": "string"
},
"ChangePasswordDto": {
"properties": {
"newPassword": {
@@ -5482,6 +5525,13 @@
],
"type": "object"
},
"Colorspace": {
"enum": [
"srgb",
"p3"
],
"type": "string"
},
"CreateAlbumDto": {
"properties": {
"albumName": {
@@ -6202,6 +6252,17 @@
],
"type": "string"
},
"OAuthAuthorizeResponseDto": {
"properties": {
"url": {
"type": "string"
}
},
"required": [
"url"
],
"type": "object"
},
"OAuthCallbackDto": {
"properties": {
"url": {
@@ -6287,7 +6348,7 @@
"PeopleUpdateItem": {
"properties": {
"birthDate": {
"description": "Person date of birth.",
"description": "Person date of birth.\nNote: the mobile app cannot currently set the birth date to null.",
"format": "date",
"nullable": true,
"type": "string"
@@ -6346,7 +6407,7 @@
"PersonUpdateDto": {
"properties": {
"birthDate": {
"description": "Person date of birth.",
"description": "Person date of birth.\nNote: the mobile app cannot currently set the birth date to null.",
"format": "date",
"nullable": true,
"type": "string"
@@ -6957,15 +7018,30 @@
"accel": {
"$ref": "#/components/schemas/TranscodeHWAccel"
},
"bframes": {
"type": "integer"
},
"cqMode": {
"$ref": "#/components/schemas/CQMode"
},
"crf": {
"type": "integer"
},
"gopSize": {
"type": "integer"
},
"maxBitrate": {
"type": "string"
},
"npl": {
"type": "integer"
},
"preset": {
"type": "string"
},
"refs": {
"type": "integer"
},
"targetAudioCodec": {
"$ref": "#/components/schemas/AudioCodec"
},
@@ -6975,6 +7051,9 @@
"targetVideoCodec": {
"$ref": "#/components/schemas/VideoCodec"
},
"temporalAQ": {
"type": "boolean"
},
"threads": {
"type": "integer"
},
@@ -6993,12 +7072,18 @@
"threads",
"targetVideoCodec",
"targetAudioCodec",
"bframes",
"refs",
"gopSize",
"npl",
"cqMode",
"transcode",
"accel",
"tonemap",
"preset",
"targetResolution",
"maxBitrate",
"temporalAQ",
"twoPass"
],
"type": "object"
@@ -7208,16 +7293,24 @@
},
"SystemConfigThumbnailDto": {
"properties": {
"colorspace": {
"$ref": "#/components/schemas/Colorspace"
},
"jpegSize": {
"type": "integer"
},
"quality": {
"type": "integer"
},
"webpSize": {
"type": "integer"
}
},
"required": [
"webpSize",
"jpegSize"
"jpegSize",
"quality",
"colorspace"
],
"type": "object"
},
@@ -7333,18 +7426,6 @@
},
"isFavorite": {
"type": "boolean"
},
"tagIds": {
"example": [
"bf973405-3f2a-48d2-a687-2ed4167164be",
"dd41870b-5d00-46d2-924e-1d8489a0aa0f",
"fad77c3f-deef-4e7e-9608-14c1aa4e559a"
],
"items": {
"type": "string"
},
"title": "Array of tag IDs to add to the asset",
"type": "array"
}
},
"type": "object"

View File

@@ -204,6 +204,10 @@ class {{{classname}}} {
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
: {{{datatypeWithEnum}}}.parse(json[r'{{{baseName}}}'].toString()),
{{/isNumber}}
{{#isDouble}}
{{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(),
{{/isDouble}}
{{^isDouble}}
{{^isNumber}}
{{^isEnum}}
{{{name}}}: mapValueOfType<{{{datatypeWithEnum}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
@@ -212,6 +216,7 @@ class {{{classname}}} {
{{{name}}}: {{{enumName}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
{{/isEnum}}
{{/isNumber}}
{{/isDouble}}
{{/isMap}}
{{/isArray}}
{{/complexType}}

View File

@@ -1,5 +1,5 @@
--- native_class.mustache 2023-06-22 12:56:11.090350406 -0500
+++ native_class1.mustache 2023-06-22 12:57:14.498184792 -0500
--- native_class.mustache 2023-08-31 23:09:59.584269162 +0200
+++ native_class1.mustache 2023-08-31 22:59:53.633083270 +0200
@@ -91,14 +91,14 @@
{{/isDateTime}}
{{#isNullable}}
@@ -35,3 +35,22 @@
return {{{classname}}}(
{{#vars}}
{{#isDateTime}}
@@ -215,6 +204,10 @@
? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}
: {{{datatypeWithEnum}}}.parse(json[r'{{{baseName}}}'].toString()),
{{/isNumber}}
+ {{#isDouble}}
+ {{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(),
+ {{/isDouble}}
+ {{^isDouble}}
{{^isNumber}}
{{^isEnum}}
{{{name}}}: mapValueOfType<{{{datatypeWithEnum}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
@@ -223,6 +216,7 @@
{{{name}}}: {{{enumName}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}},
{{/isEnum}}
{{/isNumber}}
+ {{/isDouble}}
{{/isMap}}
{{/isArray}}
{{/complexType}}

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.76.0",
"version": "1.77.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.76.0",
"version": "1.77.0",
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.22.11",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.76.0",
"version": "1.77.0",
"description": "",
"author": "",
"private": true,

View File

@@ -1,7 +1,7 @@
import { AlbumEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { AssetResponseDto, mapAsset } from '../asset';
import { mapUser, UserResponseDto } from '../user';
import { UserResponseDto, mapUser } from '../user';
export class AlbumResponseDto {
id!: string;

View File

@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { ValidateUUID } from '../../domain.util';
import { IsNotEmpty, IsString } from 'class-validator';
import { Optional, ValidateUUID } from '../../domain.util';
export class CreateAlbumDto {
@IsNotEmpty()
@@ -9,7 +9,7 @@ export class CreateAlbumDto {
albumName!: string;
@IsString()
@IsOptional()
@Optional()
description?: string;
@ValidateUUID({ optional: true, each: true })

View File

@@ -1,12 +1,12 @@
import { IsOptional, IsString } from 'class-validator';
import { ValidateUUID } from '../../domain.util';
import { IsString } from 'class-validator';
import { Optional, ValidateUUID } from '../../domain.util';
export class UpdateAlbumDto {
@IsOptional()
@Optional()
@IsString()
albumName?: string;
@IsOptional()
@Optional()
@IsString()
description?: string;

View File

@@ -1,9 +1,9 @@
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
import { toBoolean } from '../../domain.util';
import { IsBoolean } from 'class-validator';
import { Optional, toBoolean } from '../../domain.util';
export class AlbumInfoDto {
@IsOptional()
@Optional()
@IsBoolean()
@Transform(toBoolean)
withoutAssets?: boolean;

View File

@@ -1,10 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean, IsOptional } from 'class-validator';
import { toBoolean, ValidateUUID } from '../../domain.util';
import { IsBoolean } from 'class-validator';
import { Optional, ValidateUUID, toBoolean } from '../../domain.util';
export class GetAlbumsDto {
@IsOptional()
@Optional()
@IsBoolean()
@Transform(toBoolean)
@ApiProperty()

View File

@@ -1,9 +1,9 @@
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { IsNotEmpty, IsString } from 'class-validator';
import { Optional } from '../domain.util';
export class APIKeyCreateDto {
@IsString()
@IsNotEmpty()
@IsOptional()
@Optional()
name?: string;
}

View File

@@ -1,4 +1,4 @@
import { AssetEntity, AssetType } from '@app/infra/entities';
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
import { Paginated, PaginationOptions } from '../domain.util';
export type AssetStats = Record<AssetType, number>;
@@ -86,4 +86,5 @@ export interface IAssetRepository {
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
}

View File

@@ -1,9 +1,9 @@
import { AssetType } from '@app/infra/entities';
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import {
IAccessRepositoryMock,
assetStub,
authStub,
IAccessRepositoryMock,
newAccessRepositoryMock,
newAssetRepositoryMock,
newCryptoRepositoryMock,
@@ -519,6 +519,30 @@ describe(AssetService.name, () => {
});
});
describe('update', () => {
it('should require asset write access for the id', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
BadRequestException,
);
expect(assetMock.save).not.toHaveBeenCalled();
});
it('should update the asset', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
assetMock.save.mockResolvedValue(assetStub.image);
await sut.update(authStub.admin, 'asset-1', { isFavorite: true });
expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-1', isFavorite: true });
});
it('should update the exif description', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
assetMock.save.mockResolvedValue(assetStub.image);
await sut.update(authStub.admin, 'asset-1', { description: 'Test description' });
expect(assetMock.upsertExif).toHaveBeenCalledWith({ assetId: 'asset-1', description: 'Test description' });
});
});
describe('updateAll', () => {
it('should require asset write access for all ids', async () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);

View File

@@ -9,7 +9,7 @@ import { ICryptoRepository } from '../crypto';
import { mimeTypes } from '../domain.constant';
import { HumanReadableSize, usePagination } from '../domain.util';
import { IJobRepository, JobName } from '../job';
import { ImmichReadStream, IStorageRepository, StorageCore, StorageFolder } from '../storage';
import { IStorageRepository, ImmichReadStream, StorageCore, StorageFolder } from '../storage';
import { IAssetRepository } from './asset.repository';
import {
AssetBulkUpdateDto,
@@ -21,17 +21,18 @@ import {
DownloadInfoDto,
DownloadResponseDto,
MapMarkerDto,
mapStats,
MemoryLaneDto,
TimeBucketAssetDto,
TimeBucketDto,
UpdateAssetDto,
mapStats,
} from './dto';
import {
AssetResponseDto,
mapAsset,
MapMarkerResponseDto,
MemoryLaneResponseDto,
TimeBucketResponseDto,
mapAsset,
} from './response-dto';
export enum UploadFieldName {
@@ -279,6 +280,19 @@ export class AssetService {
return mapStats(stats);
}
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);
const { description, ...rest } = dto;
if (description !== undefined) {
await this.assetRepository.upsertExif({ assetId: id, description });
}
const asset = await this.assetRepository.save({ id, ...rest });
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [id] } });
return mapAsset(asset);
}
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto) {
const { ids, ...options } = dto;
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);

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