diff --git a/e2e/src/specs/server/api/api-key.e2e-spec.ts b/e2e/src/specs/server/api/api-key.e2e-spec.ts index 8ffcf1f52562e..ec481cee01a69 100644 --- a/e2e/src/specs/server/api/api-key.e2e-spec.ts +++ b/e2e/src/specs/server/api/api-key.e2e-spec.ts @@ -34,14 +34,14 @@ describe('/api-keys', () => { permissions: [Permission.ApiKeyRead], }); expect(body).toEqual({ + id: expect.any(String), + name: 'API Key', + permissions: [Permission.ApiKeyRead], + createdAt: expect.any(String), + updatedAt: expect.any(String), secret: expect.any(String), - apiKey: { - id: expect.any(String), - name: 'API Key', - permissions: [Permission.ApiKeyRead], - createdAt: expect.any(String), - updatedAt: expect.any(String), - }, + // TODO: remove in v4 + apiKey: expect.any(Object), }); expect(status).toBe(201); }); @@ -72,14 +72,14 @@ describe('/api-keys', () => { .send({ name: 'API Key', permissions: [Permission.All] }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toEqual({ - apiKey: { - id: expect.any(String), - name: 'API Key', - permissions: [Permission.All], - createdAt: expect.any(String), - updatedAt: expect.any(String), - }, + id: expect.any(String), + name: 'API Key', + permissions: [Permission.All], + createdAt: expect.any(String), + updatedAt: expect.any(String), secret: expect.any(String), + // TODO: remove in v4 + apiKey: expect.any(Object), }); expect(status).toEqual(201); }); @@ -93,23 +93,30 @@ describe('/api-keys', () => { }); it('should return a list of api keys', async () => { - const [{ apiKey: apiKey1 }, { apiKey: apiKey2 }, { apiKey: apiKey3 }] = await Promise.all([ + const [apiKey1, apiKey2, apiKey3] = await Promise.all([ create(admin.accessToken, [Permission.All]), create(admin.accessToken, [Permission.All]), create(admin.accessToken, [Permission.All]), ]); + const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toHaveLength(3); - expect(body).toEqual(expect.arrayContaining([apiKey1, apiKey2, apiKey3])); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: apiKey1.id }), + expect.objectContaining({ id: apiKey2.id }), + expect.objectContaining({ id: apiKey3.id }), + ]), + ); expect(status).toEqual(200); }); }); describe('GET /api-keys/:id', () => { it('should get api key details', async () => { - const { apiKey } = await create(user.accessToken, [Permission.All]); + const { id } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) - .get(`/api-keys/${apiKey.id}`) + .get(`/api-keys/${id}`) .set('Authorization', `Bearer ${user.accessToken}`); expect(status).toBe(200); expect(body).toEqual({ @@ -124,9 +131,9 @@ describe('/api-keys', () => { describe('PUT /api-keys/:id', () => { it('should update api key details', async () => { - const { apiKey } = await create(user.accessToken, [Permission.All]); + const { id } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) - .put(`/api-keys/${apiKey.id}`) + .put(`/api-keys/${id}`) .send({ name: 'new name', permissions: [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate], @@ -145,9 +152,9 @@ describe('/api-keys', () => { describe('DELETE /api-keys/:id', () => { it('should delete an api key', async () => { - const { apiKey } = await create(user.accessToken, [Permission.All]); + const { id } = await create(user.accessToken, [Permission.All]); const { status } = await request(app) - .delete(`/api-keys/${apiKey.id}`) + .delete(`/api-keys/${id}`) .set('Authorization', `Bearer ${user.accessToken}`); expect(status).toBe(204); }); diff --git a/e2e/src/specs/server/api/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts index f8e49a5813396..ad7f102810f56 100644 --- a/e2e/src/specs/server/api/asset.e2e-spec.ts +++ b/e2e/src/specs/server/api/asset.e2e-spec.ts @@ -130,7 +130,7 @@ describe('/asset', () => { }); await utils.createFace({ assetId: user1Assets[0].id, - personId: person1.id, + personGroupId: person1.id, }); }; beforeAll(setupTests, 30_000); diff --git a/e2e/src/specs/server/api/jobs.e2e-spec.ts b/e2e/src/specs/server/api/jobs.e2e-spec.ts index f9d8b75c46e20..b4228eeb91b06 100644 --- a/e2e/src/specs/server/api/jobs.e2e-spec.ts +++ b/e2e/src/specs/server/api/jobs.e2e-spec.ts @@ -45,7 +45,7 @@ describe('/jobs', () => { config.machineLearning.enabled = false; config.metadata.faces.import = false; config.machineLearning.clip.enabled = false; - await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) }); + await updateConfig({ adminConfigDto: config }, { headers: asBearerAuth(admin.accessToken) }); }); it('should queue metadata extraction for missing assets', async () => { diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts index 3b85e9e4c8f22..10d3343a789a5 100644 --- a/e2e/src/specs/server/api/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -1,7 +1,7 @@ import { OAuthClient, OAuthUser, generateLogoutToken } from '@immich/e2e-auth-server'; import { + AdminConfigOAuthDto, LoginResponseDto, - SystemConfigOAuthDto, getConfigDefaults, getMyUser, getSessions, @@ -70,7 +70,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => return { url: redirectUrl, state, codeVerifier }; }; -const setupOAuth = async (token: string, dto: Partial) => { +const setupOAuth = async (token: string, dto: Partial) => { const options = { headers: asBearerAuth(token) }; const defaults = await getConfigDefaults(options); const merged = { @@ -80,7 +80,7 @@ const setupOAuth = async (token: string, dto: Partial) => allowInsecureRequests: true, ...dto, }; - await updateConfig({ systemConfigDto: { ...defaults, oauth: merged } }, options); + await updateConfig({ adminConfigDto: { ...defaults, oauth: merged } }, options); }; describe(`/oauth`, () => { diff --git a/e2e/src/specs/server/api/person.e2e-spec.ts b/e2e/src/specs/server/api/person.e2e-spec.ts index 20290cd941ba4..7f5518862ff3e 100644 --- a/e2e/src/specs/server/api/person.e2e-spec.ts +++ b/e2e/src/specs/server/api/person.e2e-spec.ts @@ -82,32 +82,32 @@ describe('/people', () => { const asset4 = await utils.createAsset(admin.accessToken); await Promise.all([ - utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }), - utils.createFace({ assetId: asset1.id, personId: hiddenPerson.id }), - utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }), - utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }), - utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }), - utils.createFace({ assetId: asset3.id, personId: multipleAssetsPerson.id }), // 4 assets + utils.createFace({ assetId: asset1.id, personGroupId: visiblePerson.id }), + utils.createFace({ assetId: asset1.id, personGroupId: hiddenPerson.id }), + utils.createFace({ assetId: asset1.id, personGroupId: multipleAssetsPerson.id }), + utils.createFace({ assetId: asset1.id, personGroupId: multipleAssetsPerson.id }), + utils.createFace({ assetId: asset2.id, personGroupId: multipleAssetsPerson.id }), + utils.createFace({ assetId: asset3.id, personGroupId: multipleAssetsPerson.id }), // 4 assets // Named persons - utils.createFace({ assetId: asset1.id, personId: nameCharliePerson.id }), // 1 asset - utils.createFace({ assetId: asset1.id, personId: nameBobPerson.id }), - utils.createFace({ assetId: asset2.id, personId: nameBobPerson.id }), // 2 assets - utils.createFace({ assetId: asset1.id, personId: nameAlicePerson.id }), // 1 asset + utils.createFace({ assetId: asset1.id, personGroupId: nameCharliePerson.id }), // 1 asset + utils.createFace({ assetId: asset1.id, personGroupId: nameBobPerson.id }), + utils.createFace({ assetId: asset2.id, personGroupId: nameBobPerson.id }), // 2 assets + utils.createFace({ assetId: asset1.id, personGroupId: nameAlicePerson.id }), // 1 asset // Null-named person 4 assets - utils.createFace({ assetId: asset1.id, personId: nameNullPerson4Assets.id }), - utils.createFace({ assetId: asset2.id, personId: nameNullPerson4Assets.id }), - utils.createFace({ assetId: asset3.id, personId: nameNullPerson4Assets.id }), - utils.createFace({ assetId: asset4.id, personId: nameNullPerson4Assets.id }), // 4 assets + utils.createFace({ assetId: asset1.id, personGroupId: nameNullPerson4Assets.id }), + utils.createFace({ assetId: asset2.id, personGroupId: nameNullPerson4Assets.id }), + utils.createFace({ assetId: asset3.id, personGroupId: nameNullPerson4Assets.id }), + utils.createFace({ assetId: asset4.id, personGroupId: nameNullPerson4Assets.id }), // 4 assets // Null-named person 3 assets - utils.createFace({ assetId: asset1.id, personId: nameNullPerson3Assets.id }), - utils.createFace({ assetId: asset2.id, personId: nameNullPerson3Assets.id }), - utils.createFace({ assetId: asset3.id, personId: nameNullPerson3Assets.id }), // 3 assets + utils.createFace({ assetId: asset1.id, personGroupId: nameNullPerson3Assets.id }), + utils.createFace({ assetId: asset2.id, personGroupId: nameNullPerson3Assets.id }), + utils.createFace({ assetId: asset3.id, personGroupId: nameNullPerson3Assets.id }), // 3 assets // Null-named person 1 asset - utils.createFace({ assetId: asset3.id, personId: nameNullPerson1Asset.id }), + utils.createFace({ assetId: asset3.id, personGroupId: nameNullPerson1Asset.id }), // Favourite People - utils.createFace({ assetId: asset1.id, personId: nameFreddyPersonFavourite.id }), - utils.createFace({ assetId: asset2.id, personId: nameFreddyPersonFavourite.id }), - utils.createFace({ assetId: asset1.id, personId: nameBillPersonFavourite.id }), + utils.createFace({ assetId: asset1.id, personGroupId: nameFreddyPersonFavourite.id }), + utils.createFace({ assetId: asset2.id, personGroupId: nameFreddyPersonFavourite.id }), + utils.createFace({ assetId: asset1.id, personGroupId: nameBillPersonFavourite.id }), ]); }); diff --git a/e2e/src/specs/server/cli/login.e2e-spec.ts b/e2e/src/specs/server/cli/login.e2e-spec.ts index caf5550b6e3cf..98a28be448c85 100644 --- a/e2e/src/specs/server/cli/login.e2e-spec.ts +++ b/e2e/src/specs/server/cli/login.e2e-spec.ts @@ -30,8 +30,8 @@ describe(`immich login`, () => { it('should login and save auth.yml with 600', async () => { const admin = await utils.adminSetup(); - const key = await utils.createApiKey(admin.accessToken, [Permission.All]); - const { stdout, stderr, exitCode } = await immichCli(['login', app, key.secret]); + const apiKey = await utils.createApiKey(admin.accessToken, [Permission.All]); + const { stdout, stderr, exitCode } = await immichCli(['login', app, apiKey.secret]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2285/api', 'Logged in as admin@immich.cloud', @@ -47,8 +47,8 @@ describe(`immich login`, () => { it('should login without /api in the url', async () => { const admin = await utils.adminSetup(); - const key = await utils.createApiKey(admin.accessToken, [Permission.All]); - const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), key.secret]); + const apiKey = await utils.createApiKey(admin.accessToken, [Permission.All]); + const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), apiKey.secret]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2285', 'Discovered API at http://127.0.0.1:2285/api', diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 3124dd0609f77..131297bb89221 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -184,6 +184,8 @@ export const utils = { 'library', 'shared_link', 'person', + 'person_group', + 'cluster_group', 'album', 'asset', 'asset_face', @@ -434,12 +436,12 @@ export const utils = { return person; }, - createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => { + createFace: async ({ assetId, personGroupId }: { assetId: string; personGroupId: string }) => { if (!client) { return; } - await client.query('INSERT INTO asset_face ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]); + await client.query('INSERT INTO asset_face ("assetId", "personGroupId") VALUES ($1, $2)', [assetId, personGroupId]); }, setPersonThumbnail: async (personId: string) => { @@ -447,7 +449,9 @@ export const utils = { return; } - await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]); + await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "personGroupId" = $1`, [ + personId, + ]); }, createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) => @@ -647,7 +651,7 @@ export const utils = { resetAdminConfig: async (accessToken: string) => { const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) }); - await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); + await updateConfig({ adminConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) }); }, isQueueEmpty: async (accessToken: string, queue: keyof QueuesResponseLegacyDto) => { @@ -675,9 +679,9 @@ export const utils = { }, cliLogin: async (accessToken: string) => { - const key = await utils.createApiKey(accessToken, [Permission.All]); - await immichCli(['login', app, key.secret]); - return key.secret; + const { secret } = await utils.createApiKey(accessToken, [Permission.All]); + await immichCli(['login', app, secret]); + return secret; }, scan: async (accessToken: string, id: string) => { diff --git a/i18n/en.json b/i18n/en.json index b00d36cb09da3..92bc2d1318a7c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,5 +1,6 @@ { "about": "About", + "accept": "Accept", "account": "Account", "account_settings": "Account Settings", "acknowledge": "Acknowledge", @@ -36,6 +37,7 @@ "add_to_bottom_bar": "Add to", "add_upload_to_stack": "Add upload to stack", "add_url": "Add URL", + "add_user": "Add user", "added_to_archive": "Added to archive", "added_to_favorites": "Added to favorites", "added_to_favorites_count": "Added {count, number} to favorites", @@ -734,6 +736,9 @@ "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate import/removal is available only before login", "client_cert_title": "SSL client certificate [EXPERIMENTAL]", "close": "Close", + "cluster_group": "Cluster group", + "cluster_group_description": "People are recognized across the photos of everyone in the group", + "cluster_group_invite_description": "You have been invited to join this cluster group.", "collapse": "Collapse", "collapse_all": "Collapse all", "color": "Color", @@ -831,6 +836,7 @@ "date_time_original": "Date/Time Original", "day": "Day", "days": "Days", + "decline": "Decline", "deduplicate_all": "Deduplicate All", "default_quality_subtitle": "Quality used when tapping share. Long press the share button to choose each time.", "default_share_quality": "Default share quality", @@ -1041,8 +1047,10 @@ "unable_to_get_comments_number": "Unable to get number of comments", "unable_to_get_shared_link": "Failed to get shared link", "unable_to_hide_person": "Unable to hide person", + "unable_to_leave_cluster_group": "Unable to leave cluster group", "unable_to_link_motion_video": "Unable to link motion video", "unable_to_link_oauth_account": "Unable to link OAuth account", + "unable_to_load_cluster_group": "Unable to load cluster group", "unable_to_load_map": "Unable to load map", "unable_to_load_map_description": "The map requires WebGL to work properly.", "unable_to_log_out_all_devices": "Unable to log out all devices", @@ -1263,6 +1271,8 @@ "latitude": "Latitude", "leave": "Leave", "leave_album": "Leave album", + "leave_group": "Leave group", + "leave_group_description": "People will no longer be recognized across the photos of everyone in the group. Are you sure you want to continue?", "lens_model": "Lens model", "less": "Less", "let_others_respond": "Let others respond", @@ -1369,6 +1379,7 @@ "manage_media_access_settings": "Open settings", "manage_media_access_subtitle": "Allow the Immich app to manage and move media files.", "manage_media_access_title": "Media Management Access", + "manage_sharing_with_other_users": "Manage sharing with other users", "manage_sharing_with_partners": "Manage sharing with partners", "manage_the_app_settings": "Manage the app settings", "manage_your_account": "Manage your account", @@ -1775,6 +1786,7 @@ "removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}", "rename": "Rename", "repository": "Repository", + "request_received_description": "You have been invited to another group", "require_password": "Require password", "rescan": "Rescan", "reset": "Reset", @@ -2268,6 +2280,7 @@ "view_all_users": "View all users", "view_asset_owners": "View asset owners", "view_details": "View Details", + "view_group": "View group", "view_in_timeline": "View in timeline", "view_link": "View link", "view_name": "View", @@ -2318,6 +2331,7 @@ "year": "Year", "years_ago": "{years, plural, one {# year} other {# years}} ago", "yes": "Yes", + "you": "You", "you_dont_have_any_shared_links": "You don't have any shared links", "your_wifi_name": "Your Wi-Fi name", "zero_to_clear_rating": "press 0 to clear asset rating", diff --git a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved index da0f59f87df58..4cf0fac8900e5 100644 --- a/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/mobile/ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git", "state" : { - "revision" : "84a79bc375a301169390ac110c868f06c857b83f", - "version" : "6.27.0" + "revision" : "5ee345ca5d65238a6fce29bba87816204be7df20", + "version" : "6.28.0" } }, { diff --git a/mobile/ios/scripts/xcode_flutter_patch.sh b/mobile/ios/scripts/xcode_flutter_patch.sh new file mode 100755 index 0000000000000..95c92d0b49e37 --- /dev/null +++ b/mobile/ios/scripts/xcode_flutter_patch.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Makes Flutter not incorrectly append `-sdk` arguments to simulator builds +# This breaks macros that must build for the host's platform +# +# Flutter was informed of this (https://github.com/flutter/flutter/issues/146122) but has not fixed it in 2 years + +set -eu +sdk="${MISE_TOOL_INSTALL_PATH:-$(mise where aqua:flutter/flutter)}/flutter" +mac_dart="$sdk/packages/flutter_tools/lib/src/ios/mac.dart" + +pattern="buildCommands.addAll(['-sdk', XcodeSdk.IPhoneSimulator.platformName]);" + +# Filter out the sdk arg pattern +if awk -v pat="$pattern" 'index($0, pat) { found=1; next } { print } END { exit !found }' "$mac_dart" > "$mac_dart.tmp"; then + # If it was filtered out, apply it + mv "$mac_dart.tmp" "$mac_dart" + + # Force flutter itself to rebuild + rm -f "$sdk/bin/cache/flutter_tools.snapshot" "$sdk/bin/cache/flutter_tools.stamp" + + echo "flutter postinstall: removed simulator -sdk flag from xcodebuild invocation" +else + rm -f "$mac_dart.tmp" +fi diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 9df2eea4623ac..7afd3c8276b84 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -35,7 +35,7 @@ final Map> openApiPatches = { }, 'UserResponseDto': {'profileChangedAt': _now}, 'AssetResponseDto': {'visibility': 'timeline', 'createdAt': _now, 'isEdited': false}, - 'UserAdminResponseDto': {'profileChangedAt': _now}, + 'UserAdminResponseDto': {'profileChangedAt': _now, 'clusterGroupId': ''}, 'LoginResponseDto': {'isOnboarded': false}, 'SyncUserV1': {'profileChangedAt': _now, 'hasProfileImage': false}, 'SyncAssetV1': {'isEdited': false}, diff --git a/mobile/mise.toml b/mobile/mise.toml index d8ddc8ab211fa..d16578f142faa 100644 --- a/mobile/mise.toml +++ b/mobile/mise.toml @@ -1,7 +1,10 @@ [tools] -"aqua:flutter/flutter" = "3.44.9" java = "21.0.2" +[tools."aqua:flutter/flutter"] +version = "3.44.9" +postinstall = "bash {{config_root}}/ios/scripts/xcode_flutter_patch.sh" + [tools."github:CQLabs/homebrew-dcm"] version = "1.37.0" bin = "dcm" diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 34676069dd459..65cd03c3ae340 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1072,26 +1072,26 @@ packages: dependency: "direct main" description: name: maplibre_gl - sha256: b676f124a2fcf88c4dafedc7b462155e6396d61ce7af46354b4de11b45c9812f + sha256: "8b24dce6a050ba44779154c550e0318654ee12c572fa5fd1ad0c12e90d061356" url: "https://pub.dev" source: hosted - version: "0.26.2" + version: "0.27.0" maplibre_gl_platform_interface: dependency: transitive description: name: maplibre_gl_platform_interface - sha256: "1f0ca8a99f03fa9434618ee21f4e42dd615830a7dd973e632b11c73ffec993a8" + sha256: "8a1952b77ce841162fcd2601dc857f5b3f8eea46d34025b901ef803bb15d2adc" url: "https://pub.dev" source: hosted - version: "0.26.2" + version: "0.27.0" maplibre_gl_web: dependency: transitive description: name: maplibre_gl_web - sha256: bbf022f29ceef26d73f63e584819fbd02fbaf4eb1facf5234206c78936cf8f1c + sha256: c9c7bb1183cc9c6baafcef0b5b741420db869ca45e474ca80edf732168a286ee url: "https://pub.dev" source: hosted - version: "0.26.2" + version: "0.27.0" matcher: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 5a83ad7556e9d..9bda1b20ffd52 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -44,7 +44,7 @@ dependencies: intl: ^0.20.2 local_auth: ^2.3.0 logging: ^1.3.0 - maplibre_gl: ^0.26.0 + maplibre_gl: ^0.27.0 native_video_player: git: url: https://github.com/immich-app/native_video_player diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 60623a4a8d830..584510e364fb8 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -333,6 +333,155 @@ "x-immich-state": "Stable" } }, + "/admin/config": { + "get": { + "description": "Retrieve admin configuration.", + "operationId": "getAdminConfig", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminConfigDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get the admin configuration", + "tags": [ + "Config (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-permission": "adminConfig.read", + "x-immich-state": "Alpha" + }, + "put": { + "description": "Update the system configuration with a new system configuration.", + "operationId": "updateAdminConfig", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminConfigDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminConfigDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Update the system configuration", + "tags": [ + "Config (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-permission": "adminConfig.update", + "x-immich-state": "Alpha" + } + }, + "/admin/config/defaults": { + "get": { + "description": "Retrieve the default value of every system configuration property.", + "operationId": "getAdminConfigDefaults", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminConfigDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get the system configuration defaults", + "tags": [ + "Config (admin)" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + }, + { + "version": "v3.2.0", + "state": "Alpha" + } + ], + "x-immich-permission": "adminConfig.read", + "x-immich-state": "Alpha" + } + }, "/admin/database-backups": { "delete": { "description": "Delete a backup by its filename", @@ -1169,7 +1318,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigSmtpDto" + "$ref": "#/components/schemas/AdminConfigSmtpDto" } } }, @@ -5712,45 +5861,20 @@ "x-immich-state": "Stable" } }, - "/download/archive": { - "post": { - "description": "Download a ZIP archive containing the specified assets. The assets must have been previously requested via the \"getDownloadInfo\" endpoint.", - "operationId": "downloadArchive", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DownloadArchiveDto" - } - } - }, - "required": true - }, + "/cluster-groups/requests": { + "get": { + "description": "Retrieve the pending requests for the current user to join a cluster group.", + "operationId": "getClusterGroupRequests", + "parameters": [], "responses": { "200": { "content": { - "application/octet-stream": { + "application/json": { "schema": { - "format": "binary", - "type": "string" + "items": { + "$ref": "#/components/schemas/ClusterGroupRequestResponseDto" + }, + "type": "array" } } }, @@ -5768,69 +5892,37 @@ "api_key": [] } ], - "summary": "Download asset archive", + "summary": "Retrieve cluster group requests", "tags": [ - "Download" + "Cluster groups" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.2.0", "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" } ], - "x-immich-permission": "asset.download", - "x-immich-state": "Stable" + "x-immich-permission": "clusterGroupRequest.read" } }, - "/download/info": { - "post": { - "description": "Retrieve information about how to request a download for the specified assets or album. The response includes groups of assets that can be downloaded together.", - "operationId": "getDownloadInfo", + "/cluster-groups/requests/{id}": { + "delete": { + "description": "Delete a pending request to join a cluster group.", + "operationId": "deleteClusterGroupRequest", "parameters": [ { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", + "name": "id", + "required": true, + "in": "path", "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DownloadInfoDto" - } - } - }, - "required": true - }, "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DownloadResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -5845,43 +5937,35 @@ "api_key": [] } ], - "summary": "Retrieve download information", + "summary": "Decline a cluster group request", "tags": [ - "Download" + "Cluster groups" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.2.0", "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" } ], - "x-immich-permission": "asset.download", - "x-immich-state": "Stable" + "x-immich-permission": "clusterGroupRequest.delete" } }, - "/duplicates": { - "delete": { - "description": "Delete multiple duplicate assets specified by their IDs.", - "operationId": "deleteDuplicates", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkIdsDto" - } + "/cluster-groups/requests/{id}/accept": { + "post": { + "description": "Join the cluster group the request was created for.", + "operationId": "acceptClusterGroupRequest", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "204": { "description": "" @@ -5898,38 +5982,87 @@ "api_key": [] } ], - "summary": "Delete duplicates", + "summary": "Accept a cluster group request", "tags": [ - "Duplicates" + "Cluster groups" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.2.0", "state": "Added" + } + ], + "x-immich-permission": "clusterGroupRequest.create" + } + }, + "/cluster-groups/{id}/leave": { + "post": { + "description": "Move the current user into a new cluster group of their own.", + "operationId": "leaveClusterGroup", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] }, { - "version": "v1", - "state": "Beta" + "cookie": [] }, { - "version": "v2", - "state": "Stable" + "api_key": [] } ], - "x-immich-permission": "duplicate.delete", - "x-immich-state": "Stable" - }, + "summary": "Leave a cluster group", + "tags": [ + "Cluster groups" + ], + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + } + ], + "x-immich-permission": "clusterGroup.leave" + } + }, + "/cluster-groups/{id}/requests": { "get": { - "description": "Retrieve a list of duplicate assets available to the authenticated user.", - "operationId": "getAssetDuplicates", - "parameters": [], + "description": "Retrieve the pending requests for other users to join the cluster group.", + "operationId": "getClusterGroupRequestsForGroup", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/DuplicateResponseDto" + "$ref": "#/components/schemas/ClusterGroupRequestResponseDto" }, "type": "array" } @@ -5949,38 +6082,38 @@ "api_key": [] } ], - "summary": "Retrieve duplicates", + "summary": "Retrieve the requests sent by a cluster group", "tags": [ - "Duplicates" + "Cluster groups" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.2.0", "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, + } + ], + "x-immich-permission": "clusterGroupRequest.read" + }, + "put": { + "description": "Ask another user to join the cluster group of the current user.", + "operationId": "createClusterGroupRequest", + "parameters": [ { - "version": "v2", - "state": "Stable" + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } } ], - "x-immich-permission": "duplicate.read", - "x-immich-state": "Stable" - } - }, - "/duplicates/resolve": { - "post": { - "description": "Resolve duplicate groups by synchronizing metadata across assets and deleting/trashing duplicates.", - "operationId": "resolveDuplicates", - "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DuplicateResolveDto" + "$ref": "#/components/schemas/ClusterGroupRequestCreateDto" } } }, @@ -5991,10 +6124,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/BulkIdResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/ClusterGroupRequestResponseDto" } } }, @@ -6012,28 +6142,23 @@ "api_key": [] } ], - "summary": "Resolve duplicate groups", + "summary": "Create a cluster group request", "tags": [ - "Duplicates" + "Cluster groups" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v3.2.0", "state": "Added" - }, - { - "version": "v3.0.0", - "state": "Alpha" } ], - "x-immich-permission": "duplicate.delete", - "x-immich-state": "Alpha" + "x-immich-permission": "clusterGroupRequest.create" } }, - "/duplicates/{id}": { - "delete": { - "description": "Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.", - "operationId": "deleteDuplicate", + "/cluster-groups/{id}/users": { + "get": { + "description": "Retrieve the users that are a member of the cluster group.", + "operationId": "getClusterGroupUsers", "parameters": [ { "name": "id", @@ -6047,7 +6172,17 @@ } ], "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/UserResponseDto" + }, + "type": "array" + } + } + }, "description": "" } }, @@ -6062,54 +6197,30 @@ "api_key": [] } ], - "summary": "Dismiss a duplicate group", + "summary": "Retrieve the users of a cluster group", "tags": [ - "Duplicates" + "Cluster groups" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.2.0", "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" } ], - "x-immich-permission": "duplicate.delete", - "x-immich-state": "Stable" + "x-immich-permission": "clusterGroup.read" } }, - "/faces": { + "/config": { "get": { - "description": "Retrieve all faces belonging to an asset.", - "operationId": "getFaces", - "parameters": [ - { - "name": "id", - "required": true, - "in": "query", - "description": "Face ID", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "description": "Retrieve the system configuration properties that are visible to logged in users.", + "operationId": "getUserConfig", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/AssetFaceResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/UserConfigDto" } } }, @@ -6127,43 +6238,38 @@ "api_key": [] } ], - "summary": "Retrieve faces for asset", + "summary": "Get the configuration with user visibility", "tags": [ - "Faces" + "Config (user)" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.2.0", "state": "Added" }, { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" + "version": "v3.2.0", + "state": "Alpha" } ], - "x-immich-permission": "face.read", - "x-immich-state": "Stable" - }, - "post": { - "description": "Create a new face that has not been discovered by facial recognition. The content of the bounding box is considered a face.", - "operationId": "createFace", + "x-immich-permission": "userConfig.read", + "x-immich-state": "Alpha" + } + }, + "/config/defaults": { + "get": { + "description": "Retrieve the default value of the configuration properties that are visible to logged in users.", + "operationId": "getUserConfigDefaults", "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetFaceCreateDto" - } - } - }, - "required": true - }, "responses": { - "201": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserConfigDto" + } + } + }, "description": "" } }, @@ -6178,40 +6284,42 @@ "api_key": [] } ], - "summary": "Create a face", + "summary": "Get the default configuration with user visibility", "tags": [ - "Faces" + "Config (user)" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.2.0", "state": "Added" }, { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" + "version": "v3.2.0", + "state": "Alpha" } ], - "x-immich-permission": "face.create", - "x-immich-state": "Stable" + "x-immich-permission": "userConfig.read", + "x-immich-state": "Alpha" } }, - "/faces/{id}": { - "delete": { - "description": "Delete a face identified by the id. Optionally can be force deleted.", - "operationId": "deleteFace", + "/download/archive": { + "post": { + "description": "Download a ZIP archive containing the specified assets. The assets must have been previously requested via the \"getDownloadInfo\" endpoint.", + "operationId": "downloadArchive", "parameters": [ { - "name": "id", - "required": true, - "in": "path", + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6220,14 +6328,22 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetFaceDeleteDto" + "$ref": "#/components/schemas/DownloadArchiveDto" } } }, "required": true }, "responses": { - "204": { + "200": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, "description": "" } }, @@ -6242,9 +6358,9 @@ "api_key": [] } ], - "summary": "Delete a face", + "summary": "Download asset archive", "tags": [ - "Faces" + "Download" ], "x-immich-history": [ { @@ -6260,20 +6376,28 @@ "state": "Stable" } ], - "x-immich-permission": "face.delete", + "x-immich-permission": "asset.download", "x-immich-state": "Stable" - }, - "put": { - "description": "Re-assign the face provided in the body to the person identified by the id in the path parameter.", - "operationId": "reassignFacesById", + } + }, + "/download/info": { + "post": { + "description": "Retrieve information about how to request a download for the specified assets or album. The response includes groups of assets that can be downloaded together.", + "operationId": "getDownloadInfo", "parameters": [ { - "name": "id", - "required": true, - "in": "path", + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6282,18 +6406,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/FaceDto" + "$ref": "#/components/schemas/DownloadInfoDto" } } }, "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PersonResponseDto" + "$ref": "#/components/schemas/DownloadResponseDto" } } }, @@ -6311,9 +6435,9 @@ "api_key": [] } ], - "summary": "Re-assign a face to another person", + "summary": "Retrieve download information", "tags": [ - "Faces" + "Download" ], "x-immich-history": [ { @@ -6329,22 +6453,75 @@ "state": "Stable" } ], - "x-immich-permission": "face.update", + "x-immich-permission": "asset.download", "x-immich-state": "Stable" } }, - "/jobs": { + "/duplicates": { + "delete": { + "description": "Delete multiple duplicate assets specified by their IDs.", + "operationId": "deleteDuplicates", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Delete duplicates", + "tags": [ + "Duplicates" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "duplicate.delete", + "x-immich-state": "Stable" + }, "get": { - "deprecated": true, - "description": "Retrieve the counts of the current queue, as well as the current status.", - "operationId": "getQueuesLegacy", + "description": "Retrieve a list of duplicate assets available to the authenticated user.", + "operationId": "getAssetDuplicates", "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueuesResponseLegacyDto" + "items": { + "$ref": "#/components/schemas/DuplicateResponseDto" + }, + "type": "array" } } }, @@ -6362,12 +6539,10 @@ "api_key": [] } ], - "summary": "Retrieve queue counts and status", + "summary": "Retrieve duplicates", "tags": [ - "Jobs", - "Deprecated" + "Duplicates" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -6380,31 +6555,39 @@ { "version": "v2", "state": "Stable" - }, - { - "version": "v2.4.0", - "state": "Deprecated" } ], - "x-immich-permission": "job.read", - "x-immich-state": "Deprecated" - }, + "x-immich-permission": "duplicate.read", + "x-immich-state": "Stable" + } + }, + "/duplicates/resolve": { "post": { - "description": "Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.", - "operationId": "createJob", + "description": "Resolve duplicate groups by synchronizing metadata across assets and deleting/trashing duplicates.", + "operationId": "resolveDuplicates", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/JobCreateDto" + "$ref": "#/components/schemas/DuplicateResolveDto" } } }, "required": true }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" + } + } + }, "description": "" } }, @@ -6419,63 +6602,42 @@ "api_key": [] } ], - "summary": "Create a manual job", + "summary": "Resolve duplicate groups", "tags": [ - "Jobs" + "Duplicates" ], - "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v1", + "version": "v3.0.0", "state": "Added" }, { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" + "version": "v3.0.0", + "state": "Alpha" } ], - "x-immich-permission": "job.create", - "x-immich-state": "Stable" + "x-immich-permission": "duplicate.delete", + "x-immich-state": "Alpha" } }, - "/jobs/{name}": { - "put": { - "deprecated": true, - "description": "Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.", - "operationId": "runQueueCommandLegacy", + "/duplicates/{id}": { + "delete": { + "description": "Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.", + "operationId": "deleteDuplicate", "parameters": [ { - "name": "name", + "name": "id", "required": true, "in": "path", "schema": { - "$ref": "#/components/schemas/QueueName" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QueueCommandDto" - } - } - }, - "required": true - }, "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - } - } - }, + "204": { "description": "" } }, @@ -6490,12 +6652,10 @@ "api_key": [] } ], - "summary": "Run jobs", + "summary": "Dismiss a duplicate group", "tags": [ - "Jobs", - "Deprecated" + "Duplicates" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -6508,28 +6668,36 @@ { "version": "v2", "state": "Stable" - }, - { - "version": "v2.4.0", - "state": "Deprecated" } ], - "x-immich-permission": "job.create", - "x-immich-state": "Deprecated" + "x-immich-permission": "duplicate.delete", + "x-immich-state": "Stable" } }, - "/libraries": { + "/faces": { "get": { - "description": "Retrieve a list of external libraries.", - "operationId": "getAllLibraries", - "parameters": [], + "description": "Retrieve all faces belonging to an asset.", + "operationId": "getFaces", + "parameters": [ + { + "name": "id", + "required": true, + "in": "query", + "description": "Face ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/LibraryResponseDto" + "$ref": "#/components/schemas/AssetFaceResponseDto" }, "type": "array" } @@ -6549,11 +6717,10 @@ "api_key": [] } ], - "summary": "Retrieve libraries", + "summary": "Retrieve faces for asset", "tags": [ - "Libraries" + "Faces" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -6568,18 +6735,18 @@ "state": "Stable" } ], - "x-immich-permission": "library.read", + "x-immich-permission": "face.read", "x-immich-state": "Stable" }, "post": { - "description": "Create a new external library.", - "operationId": "createLibrary", + "description": "Create a new face that has not been discovered by facial recognition. The content of the bounding box is considered a face.", + "operationId": "createFace", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateLibraryDto" + "$ref": "#/components/schemas/AssetFaceCreateDto" } } }, @@ -6587,13 +6754,6 @@ }, "responses": { "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LibraryResponseDto" - } - } - }, "description": "" } }, @@ -6608,11 +6768,10 @@ "api_key": [] } ], - "summary": "Create a library", + "summary": "Create a face", "tags": [ - "Libraries" + "Faces" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -6627,14 +6786,14 @@ "state": "Stable" } ], - "x-immich-permission": "library.create", + "x-immich-permission": "face.create", "x-immich-state": "Stable" } }, - "/libraries/{id}": { + "/faces/{id}": { "delete": { - "description": "Delete an external library by its ID.", - "operationId": "deleteLibrary", + "description": "Delete a face identified by the id. Optionally can be force deleted.", + "operationId": "deleteFace", "parameters": [ { "name": "id", @@ -6647,7 +6806,17 @@ } } ], - "responses": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetFaceDeleteDto" + } + } + }, + "required": true + }, + "responses": { "204": { "description": "" } @@ -6663,11 +6832,10 @@ "api_key": [] } ], - "summary": "Delete a library", + "summary": "Delete a face", "tags": [ - "Libraries" + "Faces" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -6682,12 +6850,12 @@ "state": "Stable" } ], - "x-immich-permission": "library.delete", + "x-immich-permission": "face.delete", "x-immich-state": "Stable" }, - "get": { - "description": "Retrieve an external library by its ID.", - "operationId": "getLibrary", + "put": { + "description": "Re-assign the face provided in the body to the person identified by the id in the path parameter.", + "operationId": "reassignFacesById", "parameters": [ { "name": "id", @@ -6700,12 +6868,22 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FaceDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LibraryResponseDto" + "$ref": "#/components/schemas/PersonResponseDto" } } }, @@ -6723,11 +6901,10 @@ "api_key": [] } ], - "summary": "Retrieve a library", + "summary": "Re-assign a face to another person", "tags": [ - "Libraries" + "Faces" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -6742,41 +6919,22 @@ "state": "Stable" } ], - "x-immich-permission": "library.read", + "x-immich-permission": "face.update", "x-immich-state": "Stable" - }, - "put": { + } + }, + "/jobs": { + "get": { "deprecated": true, - "description": "Update an existing external library.", - "operationId": "updateLibrary", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateLibraryDto" - } - } - }, - "required": true - }, + "description": "Retrieve the counts of the current queue, as well as the current status.", + "operationId": "getQueuesLegacy", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LibraryResponseDto" + "$ref": "#/components/schemas/QueuesResponseLegacyDto" } } }, @@ -6794,9 +6952,9 @@ "api_key": [] } ], - "summary": "Update a library", + "summary": "Retrieve queue counts and status", "tags": [ - "Libraries", + "Jobs", "Deprecated" ], "x-immich-admin-only": true, @@ -6814,31 +6972,27 @@ "state": "Stable" }, { - "version": "v3", - "state": "Deprecated", - "replacementId": "updateLibrary" + "version": "v2.4.0", + "state": "Deprecated" } ], - "x-immich-permission": "library.update", + "x-immich-permission": "job.read", "x-immich-state": "Deprecated" - } - }, - "/libraries/{id}/scan": { + }, "post": { - "description": "Queue a scan for the external library to find and import new assets.", - "operationId": "scanLibrary", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "description": "Run a specific job. Most jobs are queued automatically, but this endpoint allows for manual creation of a handful of jobs, including various cleanup tasks, as well as creating a new database backup.", + "operationId": "createJob", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JobCreateDto" + } } - } - ], + }, + "required": true + }, "responses": { "204": { "description": "" @@ -6855,9 +7009,9 @@ "api_key": [] } ], - "summary": "Scan a library", + "summary": "Create a manual job", "tags": [ - "Libraries" + "Jobs" ], "x-immich-admin-only": true, "x-immich-history": [ @@ -6874,32 +7028,41 @@ "state": "Stable" } ], - "x-immich-permission": "library.update", + "x-immich-permission": "job.create", "x-immich-state": "Stable" } }, - "/libraries/{id}/statistics": { - "get": { - "description": "Retrieve statistics for a specific external library, including number of videos, images, and storage usage.", - "operationId": "getLibraryStatistics", + "/jobs/{name}": { + "put": { + "deprecated": true, + "description": "Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets.", + "operationId": "runQueueCommandLegacy", "parameters": [ { - "name": "id", + "name": "name", "required": true, "in": "path", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "$ref": "#/components/schemas/QueueName" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/QueueCommandDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LibraryStatsResponseDto" + "$ref": "#/components/schemas/QueueResponseLegacyDto" } } }, @@ -6917,9 +7080,10 @@ "api_key": [] } ], - "summary": "Retrieve library statistics", + "summary": "Run jobs", "tags": [ - "Libraries" + "Jobs", + "Deprecated" ], "x-immich-admin-only": true, "x-immich-history": [ @@ -6934,44 +7098,89 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v2.4.0", + "state": "Deprecated" } ], - "x-immich-permission": "library.statistics", - "x-immich-state": "Stable" + "x-immich-permission": "job.create", + "x-immich-state": "Deprecated" } }, - "/libraries/{id}/validate": { - "post": { - "description": "Validate the settings of an external library.", - "operationId": "validate", - "parameters": [ + "/libraries": { + "get": { + "description": "Retrieve a list of external libraries.", + "operationId": "getAllLibraries", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/LibraryResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve libraries", + "tags": [ + "Libraries" + ], + "x-immich-admin-only": true, + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], + "x-immich-permission": "library.read", + "x-immich-state": "Stable" + }, + "post": { + "description": "Create a new external library.", + "operationId": "createLibrary", + "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidateLibraryDto" + "$ref": "#/components/schemas/CreateLibraryDto" } } }, "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ValidateLibraryResponseDto" + "$ref": "#/components/schemas/LibraryResponseDto" } } }, @@ -6989,7 +7198,7 @@ "api_key": [] } ], - "summary": "Validate library settings", + "summary": "Create a library", "tags": [ "Libraries" ], @@ -7008,87 +7217,28 @@ "state": "Stable" } ], + "x-immich-permission": "library.create", "x-immich-state": "Stable" } }, - "/map/markers": { - "get": { - "description": "Retrieve a list of latitude and longitude coordinates for every asset with location data.", - "operationId": "getMapMarkers", + "/libraries/{id}": { + "delete": { + "description": "Delete an external library by its ID.", + "operationId": "deleteLibrary", "parameters": [ { - "name": "fileCreatedAfter", - "required": false, - "in": "query", - "description": "Filter assets created after this date", + "name": "id", + "required": true, + "in": "path", "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", - "type": "string" - } - }, - { - "name": "fileCreatedBefore", - "required": false, - "in": "query", - "description": "Filter assets created before this date", - "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } - }, - { - "name": "isArchived", - "required": false, - "in": "query", - "description": "Filter by archived status", - "schema": { - "type": "boolean" - } - }, - { - "name": "isFavorite", - "required": false, - "in": "query", - "description": "Filter by favorite status", - "schema": { - "type": "boolean" - } - }, - { - "name": "withPartners", - "required": false, - "in": "query", - "description": "Include partner assets", - "schema": { - "type": "boolean" - } - }, - { - "name": "withSharedAlbums", - "required": false, - "in": "query", - "description": "Include shared album assets", - "schema": { - "type": "boolean" - } } ], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/MapMarkerResponseDto" - }, - "type": "array" - } - } - }, + "204": { "description": "" } }, @@ -7103,10 +7253,11 @@ "api_key": [] } ], - "summary": "Retrieve map markers", + "summary": "Delete a library", "tags": [ - "Map" + "Libraries" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -7121,33 +7272,21 @@ "state": "Stable" } ], - "x-immich-permission": "map.read", + "x-immich-permission": "library.delete", "x-immich-state": "Stable" - } - }, - "/map/reverse-geocode": { + }, "get": { - "description": "Retrieve location information (e.g., city, country) for given latitude and longitude coordinates.", - "operationId": "reverseGeocode", + "description": "Retrieve an external library by its ID.", + "operationId": "getLibrary", "parameters": [ { - "name": "lat", - "required": true, - "in": "query", - "description": "Latitude (-90 to 90)", - "schema": { - "format": "double", - "type": "number" - } - }, - { - "name": "lon", + "name": "id", "required": true, - "in": "query", - "description": "Longitude (-180 to 180)", + "in": "path", "schema": { - "format": "double", - "type": "number" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } } ], @@ -7156,10 +7295,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/MapReverseGeocodeResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/LibraryResponseDto" } } }, @@ -7177,10 +7313,11 @@ "api_key": [] } ], - "summary": "Reverse geocode coordinates", + "summary": "Retrieve a library", "tags": [ - "Map" + "Libraries" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -7195,82 +7332,41 @@ "state": "Stable" } ], - "x-immich-permission": "map.search", + "x-immich-permission": "library.read", "x-immich-state": "Stable" - } - }, - "/memories": { - "get": { - "description": "Retrieve a list of memories. Memories are sorted descending by creation date by default, although they can also be sorted in ascending order, or randomly.", - "operationId": "searchMemories", + }, + "put": { + "deprecated": true, + "description": "Update an existing external library.", + "operationId": "updateLibrary", "parameters": [ { - "name": "for", - "required": false, - "in": "query", - "description": "Filter by date", + "name": "id", + "required": true, + "in": "path", "schema": { - "format": "date", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", - "example": "2024-01-01", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } - }, - { - "name": "isSaved", - "required": false, - "in": "query", - "description": "Filter by saved status", - "schema": { - "type": "boolean" - } - }, - { - "name": "isTrashed", - "required": false, - "in": "query", - "description": "Include trashed memories", - "schema": { - "type": "boolean" - } - }, - { - "name": "order", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/MemorySearchOrder" - } - }, - { - "name": "size", - "required": false, - "in": "query", - "description": "Number of memories to return", - "schema": { - "minimum": 1, - "maximum": 9007199254740991, - "type": "integer" - } - }, - { - "name": "type", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/MemoryType" - } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateLibraryDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/MemoryResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/LibraryResponseDto" } } }, @@ -7288,10 +7384,12 @@ "api_key": [] } ], - "summary": "Retrieve memories", + "summary": "Update a library", "tags": [ - "Memories" + "Libraries", + "Deprecated" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -7304,34 +7402,35 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3", + "state": "Deprecated", + "replacementId": "updateLibrary" } ], - "x-immich-permission": "memory.read", - "x-immich-state": "Stable" - }, + "x-immich-permission": "library.update", + "x-immich-state": "Deprecated" + } + }, + "/libraries/{id}/scan": { "post": { - "description": "Create a new memory by providing a name, description, and a list of asset IDs to include in the memory.", - "operationId": "createMemory", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MemoryCreateDto" - } + "description": "Queue a scan for the external library to find and import new assets.", + "operationId": "scanLibrary", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } - }, - "required": true - }, + } + ], "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MemoryResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -7346,10 +7445,11 @@ "api_key": [] } ], - "summary": "Create a memory", + "summary": "Scan a library", "tags": [ - "Memories" + "Libraries" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -7364,71 +7464,24 @@ "state": "Stable" } ], - "x-immich-permission": "memory.create", + "x-immich-permission": "library.update", "x-immich-state": "Stable" } }, - "/memories/statistics": { + "/libraries/{id}/statistics": { "get": { - "description": "Retrieve statistics about memories, such as total count and other relevant metrics.", - "operationId": "memoriesStatistics", + "description": "Retrieve statistics for a specific external library, including number of videos, images, and storage usage.", + "operationId": "getLibraryStatistics", "parameters": [ { - "name": "for", - "required": false, - "in": "query", - "description": "Filter by date", + "name": "id", + "required": true, + "in": "path", "schema": { - "format": "date", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", - "example": "2024-01-01", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } - }, - { - "name": "isSaved", - "required": false, - "in": "query", - "description": "Filter by saved status", - "schema": { - "type": "boolean" - } - }, - { - "name": "isTrashed", - "required": false, - "in": "query", - "description": "Include trashed memories", - "schema": { - "type": "boolean" - } - }, - { - "name": "order", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/MemorySearchOrder" - } - }, - { - "name": "size", - "required": false, - "in": "query", - "description": "Number of memories to return", - "schema": { - "minimum": 1, - "maximum": 9007199254740991, - "type": "integer" - } - }, - { - "name": "type", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/MemoryType" - } } ], "responses": { @@ -7436,7 +7489,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MemoryStatisticsResponseDto" + "$ref": "#/components/schemas/LibraryStatsResponseDto" } } }, @@ -7454,10 +7507,11 @@ "api_key": [] } ], - "summary": "Retrieve memories statistics", + "summary": "Retrieve library statistics", "tags": [ - "Memories" + "Libraries" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -7472,14 +7526,14 @@ "state": "Stable" } ], - "x-immich-permission": "memory.statistics", + "x-immich-permission": "library.statistics", "x-immich-state": "Stable" } }, - "/memories/{id}": { - "delete": { - "description": "Delete a specific memory by its ID.", - "operationId": "deleteMemory", + "/libraries/{id}/validate": { + "post": { + "description": "Validate the settings of an external library.", + "operationId": "validate", "parameters": [ { "name": "id", @@ -7492,8 +7546,25 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateLibraryDto" + } + } + }, + "required": true + }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ValidateLibraryResponseDto" + } + } + }, "description": "" } }, @@ -7508,10 +7579,11 @@ "api_key": [] } ], - "summary": "Delete a memory", + "summary": "Validate library settings", "tags": [ - "Memories" + "Libraries" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -7526,22 +7598,73 @@ "state": "Stable" } ], - "x-immich-permission": "memory.delete", "x-immich-state": "Stable" - }, + } + }, + "/map/markers": { "get": { - "description": "Retrieve a specific memory by its ID.", - "operationId": "getMemory", + "description": "Retrieve a list of latitude and longitude coordinates for every asset with location data.", + "operationId": "getMapMarkers", "parameters": [ { - "name": "id", - "required": true, - "in": "path", + "name": "fileCreatedAfter", + "required": false, + "in": "query", + "description": "Filter assets created after this date", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } + }, + { + "name": "fileCreatedBefore", + "required": false, + "in": "query", + "description": "Filter assets created before this date", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } + }, + { + "name": "isArchived", + "required": false, + "in": "query", + "description": "Filter by archived status", + "schema": { + "type": "boolean" + } + }, + { + "name": "isFavorite", + "required": false, + "in": "query", + "description": "Filter by favorite status", + "schema": { + "type": "boolean" + } + }, + { + "name": "withPartners", + "required": false, + "in": "query", + "description": "Include partner assets", + "schema": { + "type": "boolean" + } + }, + { + "name": "withSharedAlbums", + "required": false, + "in": "query", + "description": "Include shared album assets", + "schema": { + "type": "boolean" + } } ], "responses": { @@ -7549,7 +7672,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MemoryResponseDto" + "items": { + "$ref": "#/components/schemas/MapMarkerResponseDto" + }, + "type": "array" } } }, @@ -7567,9 +7693,9 @@ "api_key": [] } ], - "summary": "Retrieve a memory", + "summary": "Retrieve map markers", "tags": [ - "Memories" + "Map" ], "x-immich-history": [ { @@ -7585,41 +7711,45 @@ "state": "Stable" } ], - "x-immich-permission": "memory.read", + "x-immich-permission": "map.read", "x-immich-state": "Stable" - }, - "put": { - "deprecated": true, - "description": "Update an existing memory by its ID.", - "operationId": "updateMemory", + } + }, + "/map/reverse-geocode": { + "get": { + "description": "Retrieve location information (e.g., city, country) for given latitude and longitude coordinates.", + "operationId": "reverseGeocode", "parameters": [ { - "name": "id", + "name": "lat", "required": true, - "in": "path", + "in": "query", + "description": "Latitude (-90 to 90)", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "format": "double", + "type": "number" + } + }, + { + "name": "lon", + "required": true, + "in": "query", + "description": "Longitude (-180 to 180)", + "schema": { + "format": "double", + "type": "number" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MemoryUpdateDto" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MemoryResponseDto" + "items": { + "$ref": "#/components/schemas/MapReverseGeocodeResponseDto" + }, + "type": "array" } } }, @@ -7637,10 +7767,9 @@ "api_key": [] } ], - "summary": "Update a memory", + "summary": "Reverse geocode coordinates", "tags": [ - "Memories", - "Deprecated" + "Map" ], "x-immich-history": [ { @@ -7654,122 +7783,82 @@ { "version": "v2", "state": "Stable" - }, - { - "version": "v3", - "state": "Deprecated", - "replacementId": "updateMemory" } ], - "x-immich-permission": "memory.update", - "x-immich-state": "Deprecated" + "x-immich-permission": "map.search", + "x-immich-state": "Stable" } }, - "/memories/{id}/assets": { - "delete": { - "description": "Remove a list of asset IDs from a specific memory.", - "operationId": "removeMemoryAssets", + "/memories": { + "get": { + "description": "Retrieve a list of memories. Memories are sorted descending by creation date by default, although they can also be sorted in ascending order, or randomly.", + "operationId": "searchMemories", "parameters": [ { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "name": "for", + "required": false, + "in": "query", + "description": "Filter by date", + "schema": { + "format": "date", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", + "example": "2024-01-01", "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkIdsDto" - } - } }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/BulkIdResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ { - "bearer": [] + "name": "isSaved", + "required": false, + "in": "query", + "description": "Filter by saved status", + "schema": { + "type": "boolean" + } }, { - "cookie": [] + "name": "isTrashed", + "required": false, + "in": "query", + "description": "Include trashed memories", + "schema": { + "type": "boolean" + } }, { - "api_key": [] - } - ], - "summary": "Remove assets from a memory", - "tags": [ - "Memories" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/MemorySearchOrder" + } }, { - "version": "v1", - "state": "Beta" + "name": "size", + "required": false, + "in": "query", + "description": "Number of memories to return", + "schema": { + "minimum": 1, + "maximum": 9007199254740991, + "type": "integer" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "memoryAsset.delete", - "x-immich-state": "Stable" - }, - "put": { - "description": "Add a list of asset IDs to a specific memory.", - "operationId": "addMemoryAssets", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", + "name": "type", + "required": false, + "in": "query", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "$ref": "#/components/schemas/MemoryType" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkIdsDto" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/BulkIdResponseDto" + "$ref": "#/components/schemas/MemoryResponseDto" }, "type": "array" } @@ -7789,7 +7878,7 @@ "api_key": [] } ], - "summary": "Add assets to a memory", + "summary": "Retrieve memories", "tags": [ "Memories" ], @@ -7807,27 +7896,32 @@ "state": "Stable" } ], - "x-immich-permission": "memoryAsset.create", + "x-immich-permission": "memory.read", "x-immich-state": "Stable" - } - }, - "/notifications": { - "delete": { - "description": "Delete a list of notifications at once.", - "operationId": "deleteNotifications", + }, + "post": { + "description": "Create a new memory by providing a name, description, and a list of asset IDs to include in the memory.", + "operationId": "createMemory", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationDeleteAllDto" + "$ref": "#/components/schemas/MemoryCreateDto" } } }, "required": true }, "responses": { - "204": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryResponseDto" + } + } + }, "description": "" } }, @@ -7842,9 +7936,9 @@ "api_key": [] } ], - "summary": "Delete notifications", + "summary": "Create a memory", "tags": [ - "Notifications" + "Memories" ], "x-immich-history": [ { @@ -7860,47 +7954,70 @@ "state": "Stable" } ], - "x-immich-permission": "notification.delete", + "x-immich-permission": "memory.create", "x-immich-state": "Stable" - }, + } + }, + "/memories/statistics": { "get": { - "description": "Retrieve a list of notifications.", - "operationId": "getNotifications", + "description": "Retrieve statistics about memories, such as total count and other relevant metrics.", + "operationId": "memoriesStatistics", "parameters": [ { - "name": "id", + "name": "for", "required": false, "in": "query", - "description": "Filter by notification ID", + "description": "Filter by date", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "format": "date", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", + "example": "2024-01-01", "type": "string" } }, { - "name": "level", + "name": "isSaved", "required": false, "in": "query", + "description": "Filter by saved status", "schema": { - "$ref": "#/components/schemas/NotificationLevel" + "type": "boolean" } }, { - "name": "type", + "name": "isTrashed", "required": false, "in": "query", + "description": "Include trashed memories", "schema": { - "$ref": "#/components/schemas/NotificationType" + "type": "boolean" } }, { - "name": "unread", + "name": "order", "required": false, "in": "query", - "description": "Filter by unread status", "schema": { - "type": "boolean" + "$ref": "#/components/schemas/MemorySearchOrder" + } + }, + { + "name": "size", + "required": false, + "in": "query", + "description": "Number of memories to return", + "schema": { + "minimum": 1, + "maximum": 9007199254740991, + "type": "integer" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/MemoryType" } } ], @@ -7909,10 +8026,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/NotificationDto" - }, - "type": "array" + "$ref": "#/components/schemas/MemoryStatisticsResponseDto" } } }, @@ -7930,9 +8044,9 @@ "api_key": [] } ], - "summary": "Retrieve notifications", + "summary": "Retrieve memories statistics", "tags": [ - "Notifications" + "Memories" ], "x-immich-history": [ { @@ -7948,23 +8062,26 @@ "state": "Stable" } ], - "x-immich-permission": "notification.read", + "x-immich-permission": "memory.statistics", "x-immich-state": "Stable" - }, - "put": { - "description": "Update a list of notifications. Allows to bulk-set the read status of notifications.", - "operationId": "updateNotifications", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotificationUpdateAllDto" - } + } + }, + "/memories/{id}": { + "delete": { + "description": "Delete a specific memory by its ID.", + "operationId": "deleteMemory", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "204": { "description": "" @@ -7981,9 +8098,9 @@ "api_key": [] } ], - "summary": "Update notifications", + "summary": "Delete a memory", "tags": [ - "Notifications" + "Memories" ], "x-immich-history": [ { @@ -7999,14 +8116,12 @@ "state": "Stable" } ], - "x-immich-permission": "notification.update", + "x-immich-permission": "memory.delete", "x-immich-state": "Stable" - } - }, - "/notifications/{id}": { - "delete": { - "description": "Delete a specific notification.", - "operationId": "deleteNotification", + }, + "get": { + "description": "Retrieve a specific memory by its ID.", + "operationId": "getMemory", "parameters": [ { "name": "id", @@ -8020,9 +8135,16 @@ } ], "responses": { - "204": { - "description": "" - } + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryResponseDto" + } + } + }, + "description": "" + } }, "security": [ { @@ -8035,9 +8157,9 @@ "api_key": [] } ], - "summary": "Delete a notification", + "summary": "Retrieve a memory", "tags": [ - "Notifications" + "Memories" ], "x-immich-history": [ { @@ -8053,12 +8175,13 @@ "state": "Stable" } ], - "x-immich-permission": "notification.delete", + "x-immich-permission": "memory.read", "x-immich-state": "Stable" }, - "get": { - "description": "Retrieve a specific notification identified by id.", - "operationId": "getNotification", + "put": { + "deprecated": true, + "description": "Update an existing memory by its ID.", + "operationId": "updateMemory", "parameters": [ { "name": "id", @@ -8071,12 +8194,22 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryUpdateDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationDto" + "$ref": "#/components/schemas/MemoryResponseDto" } } }, @@ -8094,9 +8227,10 @@ "api_key": [] } ], - "summary": "Get a notification", + "summary": "Update a memory", "tags": [ - "Notifications" + "Memories", + "Deprecated" ], "x-immich-history": [ { @@ -8110,14 +8244,21 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3", + "state": "Deprecated", + "replacementId": "updateMemory" } ], - "x-immich-permission": "notification.read", - "x-immich-state": "Stable" - }, - "put": { - "description": "Update a specific notification to set its read status.", - "operationId": "updateNotification", + "x-immich-permission": "memory.update", + "x-immich-state": "Deprecated" + } + }, + "/memories/{id}/assets": { + "delete": { + "description": "Remove a list of asset IDs from a specific memory.", + "operationId": "removeMemoryAssets", "parameters": [ { "name": "id", @@ -8134,7 +8275,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationUpdateDto" + "$ref": "#/components/schemas/BulkIdsDto" } } }, @@ -8145,7 +8286,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/NotificationDto" + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" } } }, @@ -8163,9 +8307,9 @@ "api_key": [] } ], - "summary": "Update a notification", + "summary": "Remove assets from a memory", "tags": [ - "Notifications" + "Memories" ], "x-immich-history": [ { @@ -8181,40 +8325,63 @@ "state": "Stable" } ], - "x-immich-permission": "notification.update", + "x-immich-permission": "memoryAsset.delete", "x-immich-state": "Stable" - } - }, - "/oauth/authorize": { - "post": { - "description": "Initiate the OAuth authorization process.", - "operationId": "startOAuth", - "parameters": [], + }, + "put": { + "description": "Add a list of asset IDs to a specific memory.", + "operationId": "addMemoryAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OAuthConfigDto" + "$ref": "#/components/schemas/BulkIdsDto" } } }, "required": true }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OAuthAuthorizeResponseDto" + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" } } }, "description": "" } }, - "summary": "Start OAuth", + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Add assets to a memory", "tags": [ - "Authentication" + "Memories" ], "x-immich-history": [ { @@ -8230,71 +8397,44 @@ "state": "Stable" } ], + "x-immich-permission": "memoryAsset.create", "x-immich-state": "Stable" } }, - "/oauth/backchannel-logout": { - "post": { - "description": "Logout the OAuth account and invalidate the session specified by the sid claim or all sessions if the sid claim is not present.", - "operationId": "logoutOAuth", + "/notifications": { + "delete": { + "description": "Delete a list of notifications at once.", + "operationId": "deleteNotifications", "parameters": [], "requestBody": { "content": { - "application/x-www-form-urlencoded": { + "application/json": { "schema": { - "$ref": "#/components/schemas/OAuthBackchannelLogoutDto" + "$ref": "#/components/schemas/NotificationDeleteAllDto" } } }, "required": true }, "responses": { - "200": { + "204": { "description": "" } }, - "summary": "Backchannel OAuth logout", - "tags": [ - "Authentication" - ], - "x-immich-history": [ + "security": [ { - "version": "v2", - "state": "Added" - } - ] - } - }, - "/oauth/callback": { - "post": { - "description": "Complete the OAuth authorization process by exchanging the authorization code for a session token.", - "operationId": "finishOAuth", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthCallbackDto" - } - } + "bearer": [] }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LoginResponseDto" - } - } - }, - "description": "" + { + "cookie": [] + }, + { + "api_key": [] } - }, - "summary": "Finish OAuth", + ], + "summary": "Delete notifications", "tags": [ - "Authentication" + "Notifications" ], "x-immich-history": [ { @@ -8310,30 +8450,59 @@ "state": "Stable" } ], + "x-immich-permission": "notification.delete", "x-immich-state": "Stable" - } - }, - "/oauth/link": { - "post": { - "description": "Link an OAuth account to the authenticated user.", - "operationId": "linkOAuthAccount", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OAuthCallbackDto" - } + }, + "get": { + "description": "Retrieve a list of notifications.", + "operationId": "getNotifications", + "parameters": [ + { + "name": "id", + "required": false, + "in": "query", + "description": "Filter by notification ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, - "required": true - }, + { + "name": "level", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/NotificationLevel" + } + }, + { + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/NotificationType" + } + }, + { + "name": "unread", + "required": false, + "in": "query", + "description": "Filter by unread status", + "schema": { + "type": "boolean" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserAdminResponseDto" + "items": { + "$ref": "#/components/schemas/NotificationDto" + }, + "type": "array" } } }, @@ -8351,9 +8520,9 @@ "api_key": [] } ], - "summary": "Link OAuth account", + "summary": "Retrieve notifications", "tags": [ - "Authentication" + "Notifications" ], "x-immich-history": [ { @@ -8369,173 +8538,25 @@ "state": "Stable" } ], - "x-immich-state": "Stable" - } - }, - "/oauth/mobile-redirect": { - "get": { - "description": "Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting.", - "operationId": "redirectOAuthToMobile", - "parameters": [], - "responses": { - "200": { - "description": "" - } - }, - "summary": "Redirect OAuth to mobile", - "tags": [ - "Authentication" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" - } - }, - "/oauth/unlink": { - "post": { - "description": "Unlink the OAuth account from the authenticated user.", - "operationId": "unlinkOAuthAccount", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserAdminResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Unlink OAuth account", - "tags": [ - "Authentication" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" - } - }, - "/partners": { - "get": { - "description": "Retrieve a list of partners with whom assets are shared.", - "operationId": "getPartners", - "parameters": [ - { - "name": "direction", - "required": true, - "in": "query", - "schema": { - "$ref": "#/components/schemas/PartnerDirection" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/PartnerResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Retrieve partners", - "tags": [ - "Partners" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "partner.read", + "x-immich-permission": "notification.read", "x-immich-state": "Stable" }, - "post": { - "description": "Create a new partner to share assets with.", - "operationId": "createPartner", + "put": { + "description": "Update a list of notifications. Allows to bulk-set the read status of notifications.", + "operationId": "updateNotifications", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PartnerCreateDto" + "$ref": "#/components/schemas/NotificationUpdateAllDto" } } }, "required": true }, "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PartnerResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -8550,9 +8571,9 @@ "api_key": [] } ], - "summary": "Create a partner", + "summary": "Update notifications", "tags": [ - "Partners" + "Notifications" ], "x-immich-history": [ { @@ -8568,14 +8589,14 @@ "state": "Stable" } ], - "x-immich-permission": "partner.create", + "x-immich-permission": "notification.update", "x-immich-state": "Stable" } }, - "/partners/{id}": { + "/notifications/{id}": { "delete": { - "description": "Stop sharing assets with a partner.", - "operationId": "removePartner", + "description": "Delete a specific notification.", + "operationId": "deleteNotification", "parameters": [ { "name": "id", @@ -8604,9 +8625,9 @@ "api_key": [] } ], - "summary": "Remove a partner", + "summary": "Delete a notification", "tags": [ - "Partners" + "Notifications" ], "x-immich-history": [ { @@ -8622,13 +8643,12 @@ "state": "Stable" } ], - "x-immich-permission": "partner.delete", + "x-immich-permission": "notification.delete", "x-immich-state": "Stable" }, - "post": { - "deprecated": true, - "description": "Create a new partner to share assets with.", - "operationId": "createPartnerDeprecated", + "get": { + "description": "Retrieve a specific notification identified by id.", + "operationId": "getNotification", "parameters": [ { "name": "id", @@ -8642,11 +8662,11 @@ } ], "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PartnerResponseDto" + "$ref": "#/components/schemas/NotificationDto" } } }, @@ -8664,10 +8684,9 @@ "api_key": [] } ], - "summary": "Create a partner", + "summary": "Get a notification", "tags": [ - "Partners", - "Deprecated" + "Notifications" ], "x-immich-history": [ { @@ -8676,16 +8695,19 @@ }, { "version": "v1", - "state": "Deprecated", - "replacementId": "createPartner" + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "partner.create", - "x-immich-state": "Deprecated" + "x-immich-permission": "notification.read", + "x-immich-state": "Stable" }, "put": { - "description": "Specify whether a partner's assets should appear in the user's timeline.", - "operationId": "updatePartner", + "description": "Update a specific notification to set its read status.", + "operationId": "updateNotification", "parameters": [ { "name": "id", @@ -8702,7 +8724,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PartnerUpdateDto" + "$ref": "#/components/schemas/NotificationUpdateDto" } } }, @@ -8713,7 +8735,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PartnerResponseDto" + "$ref": "#/components/schemas/NotificationDto" } } }, @@ -8731,9 +8753,9 @@ "api_key": [] } ], - "summary": "Update a partner", + "summary": "Update a notification", "tags": [ - "Partners" + "Notifications" ], "x-immich-history": [ { @@ -8749,44 +8771,40 @@ "state": "Stable" } ], - "x-immich-permission": "partner.update", + "x-immich-permission": "notification.update", "x-immich-state": "Stable" } }, - "/people": { - "delete": { - "description": "Bulk delete a list of people at once.", - "operationId": "deletePeople", + "/oauth/authorize": { + "post": { + "description": "Initiate the OAuth authorization process.", + "operationId": "startOAuth", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkIdsDto" + "$ref": "#/components/schemas/OAuthConfigDto" } } }, "required": true }, "responses": { - "204": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthAuthorizeResponseDto" + } + } + }, "description": "" } }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Delete people", + "summary": "Start OAuth", "tags": [ - "People" + "Authentication" ], "x-immich-history": [ { @@ -8802,95 +8820,71 @@ "state": "Stable" } ], - "x-immich-permission": "person.delete", "x-immich-state": "Stable" - }, - "get": { - "description": "Retrieve a list of all people.", - "operationId": "getAllPeople", - "parameters": [ - { - "name": "closestAssetId", - "required": false, - "in": "query", - "description": "Closest asset ID for similarity search", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - { - "name": "closestPersonId", - "required": false, - "in": "query", - "description": "Closest person ID for similarity search", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - { - "name": "page", - "required": false, - "in": "query", - "description": "Page number for pagination", - "schema": { - "minimum": 1, - "maximum": 9007199254740991, - "default": 1, - "type": "integer" + } + }, + "/oauth/backchannel-logout": { + "post": { + "description": "Logout the OAuth account and invalidate the session specified by the sid claim or all sessions if the sid claim is not present.", + "operationId": "logoutOAuth", + "parameters": [], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/OAuthBackchannelLogoutDto" + } } }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "summary": "Backchannel OAuth logout", + "tags": [ + "Authentication" + ], + "x-immich-history": [ { - "name": "size", - "required": false, - "in": "query", - "description": "Number of items per page", - "schema": { - "minimum": 1, - "maximum": 1000, - "default": 500, - "type": "integer" + "version": "v2", + "state": "Added" + } + ] + } + }, + "/oauth/callback": { + "post": { + "description": "Complete the OAuth authorization process by exchanging the authorization code for a session token.", + "operationId": "finishOAuth", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuthCallbackDto" + } } }, - { - "name": "withHidden", - "required": false, - "in": "query", - "description": "Include hidden people", - "schema": { - "type": "boolean" - } - } - ], + "required": true + }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PeopleResponseDto" + "$ref": "#/components/schemas/LoginResponseDto" } } }, "description": "" } }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Get all people", + "summary": "Finish OAuth", "tags": [ - "People" + "Authentication" ], "x-immich-history": [ { @@ -8906,29 +8900,30 @@ "state": "Stable" } ], - "x-immich-permission": "person.read", "x-immich-state": "Stable" - }, + } + }, + "/oauth/link": { "post": { - "description": "Create a new person that can have multiple faces assigned to them.", - "operationId": "createPerson", + "description": "Link an OAuth account to the authenticated user.", + "operationId": "linkOAuthAccount", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PersonCreateDto" + "$ref": "#/components/schemas/OAuthCallbackDto" } } }, "required": true }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PersonResponseDto" + "$ref": "#/components/schemas/UserAdminResponseDto" } } }, @@ -8946,9 +8941,9 @@ "api_key": [] } ], - "summary": "Create a person", + "summary": "Link OAuth account", "tags": [ - "People" + "Authentication" ], "x-immich-history": [ { @@ -8964,32 +8959,51 @@ "state": "Stable" } ], - "x-immich-permission": "person.create", "x-immich-state": "Stable" - }, - "put": { - "description": "Bulk update multiple people at once.", - "operationId": "updatePeople", + } + }, + "/oauth/mobile-redirect": { + "get": { + "description": "Requests to this URL are automatically forwarded to the mobile app, and is used in some cases for OAuth redirecting.", + "operationId": "redirectOAuthToMobile", "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PeopleUpdateDto" - } - } - }, - "required": true + "responses": { + "200": { + "description": "" + } }, + "summary": "Redirect OAuth to mobile", + "tags": [ + "Authentication" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-state": "Stable" + } + }, + "/oauth/unlink": { + "post": { + "description": "Unlink the OAuth account from the authenticated user.", + "operationId": "unlinkOAuthAccount", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/BulkIdResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/UserAdminResponseDto" } } }, @@ -9007,9 +9021,9 @@ "api_key": [] } ], - "summary": "Update people", + "summary": "Unlink OAuth account", "tags": [ - "People" + "Authentication" ], "x-immich-history": [ { @@ -9025,28 +9039,35 @@ "state": "Stable" } ], - "x-immich-permission": "person.update", "x-immich-state": "Stable" } }, - "/people/{id}": { - "delete": { - "description": "Delete an individual person.", - "operationId": "deletePerson", + "/partners": { + "get": { + "description": "Retrieve a list of partners with whom assets are shared.", + "operationId": "getPartners", "parameters": [ { - "name": "id", + "name": "direction", "required": true, - "in": "path", + "in": "query", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "$ref": "#/components/schemas/PartnerDirection" } } ], "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PartnerResponseDto" + }, + "type": "array" + } + } + }, "description": "" } }, @@ -9061,9 +9082,9 @@ "api_key": [] } ], - "summary": "Delete person", + "summary": "Retrieve partners", "tags": [ - "People" + "Partners" ], "x-immich-history": [ { @@ -9079,30 +9100,29 @@ "state": "Stable" } ], - "x-immich-permission": "person.delete", + "x-immich-permission": "partner.read", "x-immich-state": "Stable" }, - "get": { - "description": "Retrieve a person by id.", - "operationId": "getPerson", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "post": { + "description": "Create a new partner to share assets with.", + "operationId": "createPartner", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PartnerCreateDto" + } } - } - ], + }, + "required": true + }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PersonResponseDto" + "$ref": "#/components/schemas/PartnerResponseDto" } } }, @@ -9120,9 +9140,9 @@ "api_key": [] } ], - "summary": "Get a person", + "summary": "Create a partner", "tags": [ - "People" + "Partners" ], "x-immich-history": [ { @@ -9138,13 +9158,14 @@ "state": "Stable" } ], - "x-immich-permission": "person.read", + "x-immich-permission": "partner.create", "x-immich-state": "Stable" - }, - "put": { - "deprecated": true, - "description": "Update an individual person.", - "operationId": "updatePerson", + } + }, + "/partners/{id}": { + "delete": { + "description": "Stop sharing assets with a partner.", + "operationId": "removePartner", "parameters": [ { "name": "id", @@ -9157,25 +9178,8 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PersonUpdateDto" - } - } - }, - "required": true - }, "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PersonResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -9190,10 +9194,9 @@ "api_key": [] } ], - "summary": "Update person", + "summary": "Remove a partner", "tags": [ - "People", - "Deprecated" + "Partners" ], "x-immich-history": [ { @@ -9207,21 +9210,15 @@ { "version": "v2", "state": "Stable" - }, - { - "version": "v3", - "state": "Deprecated", - "replacementId": "updatePerson" } ], - "x-immich-permission": "person.update", - "x-immich-state": "Deprecated" - } - }, - "/people/{id}/merge": { + "x-immich-permission": "partner.delete", + "x-immich-state": "Stable" + }, "post": { - "description": "Merge a list of people into the person specified in the path parameter.", - "operationId": "mergePerson", + "deprecated": true, + "description": "Create a new partner to share assets with.", + "operationId": "createPartnerDeprecated", "parameters": [ { "name": "id", @@ -9234,25 +9231,12 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MergePersonDto" - } - } - }, - "required": true - }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/BulkIdResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/PartnerResponseDto" } } }, @@ -9270,9 +9254,10 @@ "api_key": [] } ], - "summary": "Merge people", + "summary": "Create a partner", "tags": [ - "People" + "Partners", + "Deprecated" ], "x-immich-history": [ { @@ -9281,21 +9266,16 @@ }, { "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" + "state": "Deprecated", + "replacementId": "createPartner" } ], - "x-immich-permission": "person.merge", - "x-immich-state": "Stable" - } - }, - "/people/{id}/reassign": { + "x-immich-permission": "partner.create", + "x-immich-state": "Deprecated" + }, "put": { - "description": "Bulk reassign a list of faces to a different person.", - "operationId": "reassignFaces", + "description": "Specify whether a partner's assets should appear in the user's timeline.", + "operationId": "updatePartner", "parameters": [ { "name": "id", @@ -9312,7 +9292,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetFaceUpdateDto" + "$ref": "#/components/schemas/PartnerUpdateDto" } } }, @@ -9323,10 +9303,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/PersonResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/PartnerResponseDto" } } }, @@ -9344,9 +9321,9 @@ "api_key": [] } ], - "summary": "Reassign faces", + "summary": "Update a partner", "tags": [ - "People" + "Partners" ], "x-immich-history": [ { @@ -9362,35 +9339,27 @@ "state": "Stable" } ], - "x-immich-permission": "person.reassign", + "x-immich-permission": "partner.update", "x-immich-state": "Stable" } }, - "/people/{id}/statistics": { - "get": { - "description": "Retrieve statistics about a specific person.", - "operationId": "getPersonStatistics", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "/people": { + "delete": { + "description": "Bulk delete a list of people at once.", + "operationId": "deletePeople", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } } - } - ], + }, + "required": true + }, "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PersonStatisticsResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -9405,7 +9374,7 @@ "api_key": [] } ], - "summary": "Get person statistics", + "summary": "Delete people", "tags": [ "People" ], @@ -9423,33 +9392,75 @@ "state": "Stable" } ], - "x-immich-permission": "person.statistics", + "x-immich-permission": "person.delete", "x-immich-state": "Stable" - } - }, - "/people/{id}/thumbnail": { + }, "get": { - "description": "Retrieve the thumbnail file for a person.", - "operationId": "getPersonThumbnail", + "description": "Retrieve a list of all people.", + "operationId": "getAllPeople", "parameters": [ { - "name": "id", - "required": true, - "in": "path", + "name": "closestAssetId", + "required": false, + "in": "query", + "description": "Closest asset ID for similarity search", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "closestPersonId", + "required": false, + "in": "query", + "description": "Closest person ID for similarity search", "schema": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } + }, + { + "name": "page", + "required": false, + "in": "query", + "description": "Page number for pagination", + "schema": { + "minimum": 1, + "maximum": 9007199254740991, + "default": 1, + "type": "integer" + } + }, + { + "name": "size", + "required": false, + "in": "query", + "description": "Number of items per page", + "schema": { + "minimum": 1, + "maximum": 1000, + "default": 500, + "type": "integer" + } + }, + { + "name": "withHidden", + "required": false, + "in": "query", + "description": "Include hidden people", + "schema": { + "type": "boolean" + } } ], "responses": { "200": { "content": { - "application/octet-stream": { + "application/json": { "schema": { - "format": "binary", - "type": "string" + "$ref": "#/components/schemas/PeopleResponseDto" } } }, @@ -9467,7 +9478,7 @@ "api_key": [] } ], - "summary": "Get person thumbnail", + "summary": "Get all people", "tags": [ "People" ], @@ -9487,73 +9498,86 @@ ], "x-immich-permission": "person.read", "x-immich-state": "Stable" - } - }, - "/plugins": { - "get": { - "description": "Retrieve a list of plugins available to the authenticated user.", - "operationId": "searchPlugins", - "parameters": [ - { - "name": "description", - "required": false, - "in": "query", - "schema": { - "type": "string" + }, + "post": { + "description": "Create a new person that can have multiple faces assigned to them.", + "operationId": "createPerson", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonCreateDto" + } } }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ { - "name": "enabled", - "required": false, - "in": "query", - "description": "Whether the plugin is enabled", - "schema": { - "type": "boolean" - } + "bearer": [] }, { - "name": "id", - "required": false, - "in": "query", - "description": "Plugin ID", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } + "cookie": [] }, { - "name": "name", - "required": false, - "in": "query", - "schema": { - "type": "string" - } + "api_key": [] + } + ], + "summary": "Create a person", + "tags": [ + "People" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" }, { - "name": "title", - "required": false, - "in": "query", - "schema": { - "type": "string" - } + "version": "v1", + "state": "Beta" }, { - "name": "version", - "required": false, - "in": "query", - "schema": { - "type": "string" - } + "version": "v2", + "state": "Stable" } ], + "x-immich-permission": "person.create", + "x-immich-state": "Stable" + }, + "put": { + "description": "Bulk update multiple people at once.", + "operationId": "updatePeople", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PeopleUpdateDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/PluginResponseDto" + "$ref": "#/components/schemas/BulkIdResponseDto" }, "type": "array" } @@ -9573,117 +9597,46 @@ "api_key": [] } ], - "summary": "List all plugins", + "summary": "Update people", "tags": [ - "Plugins" + "People" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "plugin.read" + "x-immich-permission": "person.update", + "x-immich-state": "Stable" } }, - "/plugins/methods": { - "get": { - "description": "Retrieve a list of plugin methods", - "operationId": "searchPluginMethods", + "/people/{id}": { + "delete": { + "description": "Delete an individual person.", + "operationId": "deletePerson", "parameters": [ - { - "name": "description", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "enabled", - "required": false, - "in": "query", - "description": "Whether the plugin method is enabled", - "schema": { - "type": "boolean" - } - }, { "name": "id", - "required": false, - "in": "query", - "description": "Plugin method ID", + "required": true, + "in": "path", "schema": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } - }, - { - "name": "name", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "pluginName", - "required": false, - "in": "query", - "description": "Plugin name", - "schema": { - "type": "string" - } - }, - { - "name": "pluginVersion", - "required": false, - "in": "query", - "description": "Plugin version", - "schema": { - "type": "string" - } - }, - { - "name": "title", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "trigger", - "required": false, - "in": "query", - "description": "Workflow trigger", - "schema": { - "$ref": "#/components/schemas/WorkflowTrigger" - } - }, - { - "name": "type", - "required": false, - "in": "query", - "description": "Workflow types", - "schema": { - "$ref": "#/components/schemas/WorkflowType" - } } ], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/PluginMethodResponseDto" - }, - "type": "array" - } - } - }, + "204": { "description": "" } }, @@ -9698,67 +9651,30 @@ "api_key": [] } ], - "summary": "Retrieve plugin methods", + "summary": "Delete person", "tags": [ - "Plugins" + "People" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" - } - ], - "x-immich-permission": "plugin.read" - } - }, - "/plugins/templates": { - "get": { - "description": "Retrieve workflow templates provided by installed plugins", - "operationId": "searchPluginTemplates", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/PluginTemplateResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] }, { - "cookie": [] + "version": "v1", + "state": "Beta" }, { - "api_key": [] - } - ], - "summary": "Retrieve workflow templates", - "tags": [ - "Plugins" - ], - "x-immich-history": [ - { - "version": "v3.0.0", - "state": "Added" + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "plugin.read" - } - }, - "/plugins/{id}": { + "x-immich-permission": "person.delete", + "x-immich-state": "Stable" + }, "get": { - "description": "Retrieve information about a specific plugin by its ID.", - "operationId": "getPlugin", + "description": "Retrieve a person by id.", + "operationId": "getPerson", "parameters": [ { "name": "id", @@ -9776,7 +9692,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PluginResponseDto" + "$ref": "#/components/schemas/PersonResponseDto" } } }, @@ -9794,33 +9710,59 @@ "api_key": [] } ], - "summary": "Retrieve a plugin", + "summary": "Get a person", "tags": [ - "Plugins" + "People" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" - } - ], - "x-immich-permission": "plugin.read" - } - }, - "/queues": { - "get": { - "description": "Retrieves a list of queues.", - "operationId": "getQueues", - "parameters": [], + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "person.read", + "x-immich-state": "Stable" + }, + "put": { + "deprecated": true, + "description": "Update an individual person.", + "operationId": "updatePerson", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonUpdateDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/QueueResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/PersonResponseDto" } } }, @@ -9838,45 +9780,69 @@ "api_key": [] } ], - "summary": "List all queues", + "summary": "Update person", "tags": [ - "Queues" + "People", + "Deprecated" ], - "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v2.4.0", + "version": "v1", "state": "Added" }, { - "version": "v2.4.0", - "state": "Alpha" + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v3", + "state": "Deprecated", + "replacementId": "updatePerson" } ], - "x-immich-permission": "queue.read", - "x-immich-state": "Alpha" + "x-immich-permission": "person.update", + "x-immich-state": "Deprecated" } }, - "/queues/{name}": { - "get": { - "description": "Retrieves a specific queue by its name.", - "operationId": "getQueue", + "/people/{id}/merge": { + "post": { + "description": "Merge a list of people into the person specified in the path parameter.", + "operationId": "mergePerson", "parameters": [ { - "name": "name", + "name": "id", "required": true, "in": "path", "schema": { - "$ref": "#/components/schemas/QueueName" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MergePersonDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueueResponseDto" + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" } } }, @@ -9894,34 +9860,41 @@ "api_key": [] } ], - "summary": "Retrieve a queue", + "summary": "Merge people", "tags": [ - "Queues" + "People" ], - "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v2.4.0", + "version": "v1", "state": "Added" }, { - "version": "v2.4.0", - "state": "Alpha" + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "queue.read", - "x-immich-state": "Alpha" - }, + "x-immich-permission": "person.merge", + "x-immich-state": "Stable" + } + }, + "/people/{id}/reassign": { "put": { - "description": "Change the paused status of a specific queue.", - "operationId": "updateQueue", + "description": "Bulk reassign a list of faces to a different person.", + "operationId": "reassignFaces", "parameters": [ { - "name": "name", + "name": "id", "required": true, "in": "path", "schema": { - "$ref": "#/components/schemas/QueueName" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } } ], @@ -9929,7 +9902,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueueUpdateDto" + "$ref": "#/components/schemas/AssetFaceUpdateDto" } } }, @@ -9940,7 +9913,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/QueueResponseDto" + "items": { + "$ref": "#/components/schemas/PersonResponseDto" + }, + "type": "array" } } }, @@ -9958,51 +9934,53 @@ "api_key": [] } ], - "summary": "Update a queue", + "summary": "Reassign faces", "tags": [ - "Queues" + "People" ], - "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v2.4.0", + "version": "v1", "state": "Added" }, { - "version": "v2.4.0", - "state": "Alpha" + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "queue.update", - "x-immich-state": "Alpha" + "x-immich-permission": "person.reassign", + "x-immich-state": "Stable" } }, - "/queues/{name}/jobs": { - "delete": { - "description": "Removes all jobs from the specified queue.", - "operationId": "emptyQueue", + "/people/{id}/statistics": { + "get": { + "description": "Retrieve statistics about a specific person.", + "operationId": "getPersonStatistics", "parameters": [ { - "name": "name", + "name": "id", "required": true, "in": "path", "schema": { - "$ref": "#/components/schemas/QueueName" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/QueueDeleteDto" - } - } - }, - "required": true - }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonStatisticsResponseDto" + } + } + }, "description": "" } }, @@ -10017,58 +9995,51 @@ "api_key": [] } ], - "summary": "Empty a queue", + "summary": "Get person statistics", "tags": [ - "Queues" + "People" ], - "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v2.4.0", + "version": "v1", "state": "Added" }, { - "version": "v2.4.0", - "state": "Alpha" + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "queueJob.delete", - "x-immich-state": "Alpha" - }, + "x-immich-permission": "person.statistics", + "x-immich-state": "Stable" + } + }, + "/people/{id}/thumbnail": { "get": { - "description": "Retrieves a list of queue jobs from the specified queue.", - "operationId": "getQueueJobs", + "description": "Retrieve the thumbnail file for a person.", + "operationId": "getPersonThumbnail", "parameters": [ { - "name": "name", + "name": "id", "required": true, "in": "path", "schema": { - "$ref": "#/components/schemas/QueueName" - } - }, - { - "name": "status", - "required": false, - "in": "query", - "description": "Filter jobs by status", - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QueueJobStatus" - } + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } } ], "responses": { "200": { "content": { - "application/json": { + "application/octet-stream": { "schema": { - "items": { - "$ref": "#/components/schemas/QueueJobResponseDto" - }, - "type": "array" + "format": "binary", + "type": "string" } } }, @@ -10086,90 +10057,93 @@ "api_key": [] } ], - "summary": "Retrieve queue jobs", + "summary": "Get person thumbnail", "tags": [ - "Queues" + "People" ], - "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v2.4.0", + "version": "v1", "state": "Added" }, { - "version": "v2.4.0", - "state": "Alpha" + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "queueJob.read", - "x-immich-state": "Alpha" + "x-immich-permission": "person.read", + "x-immich-state": "Stable" } }, - "/search/cities": { + "/plugins": { "get": { - "description": "Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in.", - "operationId": "getAssetsByCity", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ + "description": "Retrieve a list of plugins available to the authenticated user.", + "operationId": "searchPlugins", + "parameters": [ { - "bearer": [] + "name": "description", + "required": false, + "in": "query", + "schema": { + "type": "string" + } }, { - "cookie": [] + "name": "enabled", + "required": false, + "in": "query", + "description": "Whether the plugin is enabled", + "schema": { + "type": "boolean" + } }, { - "api_key": [] - } - ], - "summary": "Retrieve assets by city", - "tags": [ - "Search" - ], - "x-immich-history": [ + "name": "id", + "required": false, + "in": "query", + "description": "Plugin ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, { - "version": "v1", - "state": "Added" + "name": "name", + "required": false, + "in": "query", + "schema": { + "type": "string" + } }, { - "version": "v1", - "state": "Beta" + "name": "title", + "required": false, + "in": "query", + "schema": { + "type": "string" + } }, { - "version": "v2", - "state": "Stable" + "name": "version", + "required": false, + "in": "query", + "schema": { + "type": "string" + } } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Stable" - } - }, - "/search/explore": { - "get": { - "description": "Retrieve data for the explore section, such as popular people and places.", - "operationId": "getExploreData", - "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/SearchExploreResponseDto" + "$ref": "#/components/schemas/PluginResponseDto" }, "type": "array" } @@ -10189,395 +10163,156 @@ "api_key": [] } ], - "summary": "Retrieve explore data", + "summary": "List all plugins", "tags": [ - "Search" + "Plugins" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.0.0", "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Stable" + "x-immich-permission": "plugin.read" } }, - "/search/large-assets": { - "post": { - "description": "Search for assets that are considered large based on specified criteria.", - "operationId": "searchLargeAssets", + "/plugins/methods": { + "get": { + "description": "Retrieve a list of plugin methods", + "operationId": "searchPluginMethods", "parameters": [ { - "name": "albumIds", - "required": false, - "in": "query", - "description": "Filter by album IDs", - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" - } - } - }, - { - "name": "city", + "name": "description", "required": false, "in": "query", - "description": "Filter by city name", "schema": { - "type": "string", - "nullable": true + "type": "string" } }, { - "name": "country", + "name": "enabled", "required": false, "in": "query", - "description": "Filter by country name", + "description": "Whether the plugin method is enabled", "schema": { - "type": "string", - "nullable": true + "type": "boolean" } }, { - "name": "createdAfter", + "name": "id", "required": false, "in": "query", - "description": "Filter by creation date (after)", + "description": "Plugin method ID", "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, { - "name": "createdBefore", + "name": "name", "required": false, "in": "query", - "description": "Filter by creation date (before)", "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, { - "name": "isEncoded", + "name": "pluginName", "required": false, "in": "query", - "description": "Filter by encoded status", + "description": "Plugin name", "schema": { - "type": "boolean" + "type": "string" } }, { - "name": "isFavorite", + "name": "pluginVersion", "required": false, "in": "query", - "description": "Filter by favorite status", + "description": "Plugin version", "schema": { - "type": "boolean" + "type": "string" } }, { - "name": "isMotion", + "name": "title", "required": false, "in": "query", - "description": "Filter by motion photo status", "schema": { - "type": "boolean" + "type": "string" } }, { - "name": "isNotInAlbum", + "name": "trigger", "required": false, "in": "query", - "description": "Filter assets not in any album", + "description": "Workflow trigger", "schema": { - "type": "boolean" + "$ref": "#/components/schemas/WorkflowTrigger" } }, { - "name": "isOffline", + "name": "type", "required": false, "in": "query", - "description": "Filter by offline status", + "description": "Workflow types", "schema": { - "type": "boolean" + "$ref": "#/components/schemas/WorkflowType" } - }, + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PluginMethodResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ { - "name": "lensModel", - "required": false, - "in": "query", - "description": "Filter by lens model", - "schema": { - "type": "string", - "nullable": true - } + "bearer": [] }, { - "name": "libraryId", - "required": false, - "in": "query", - "description": "Library ID to filter by", - "schema": { - "type": "string", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "nullable": true - } + "cookie": [] }, { - "name": "make", - "required": false, - "in": "query", - "description": "Filter by camera make", - "schema": { - "type": "string", - "nullable": true - } - }, + "api_key": [] + } + ], + "summary": "Retrieve plugin methods", + "tags": [ + "Plugins" + ], + "x-immich-history": [ { - "name": "minFileSize", - "required": false, - "in": "query", - "description": "Minimum file size in bytes", - "schema": { - "minimum": 0, - "maximum": 9007199254740991, - "type": "integer" - } - }, - { - "name": "model", - "required": false, - "in": "query", - "description": "Filter by camera model", - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "ocr", - "required": false, - "in": "query", - "description": "Filter by OCR text content", - "schema": { - "type": "string" - } - }, - { - "name": "personIds", - "required": false, - "in": "query", - "description": "Filter by person IDs", - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" - } - } - }, - { - "name": "rating", - "required": false, - "in": "query", - "description": "Filter by rating [1-5], or null for unrated", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - }, - { - "version": "v2.6.0", - "state": "Updated", - "description": "Using -1 as a rating is deprecated and will be removed in the next major version." - }, - { - "version": "v3", - "state": "Updated", - "description": "Using -1 as a rating is no longer valid." - } - ], - "x-immich-state": "Stable", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 5, - "nullable": true - } - }, - { - "name": "size", - "required": false, - "in": "query", - "description": "Number of results to return", - "schema": { - "minimum": 1, - "maximum": 1000, - "type": "integer" - } - }, - { - "name": "state", - "required": false, - "in": "query", - "description": "Filter by state/province name", - "schema": { - "type": "string", - "nullable": true - } - }, - { - "name": "tagIds", - "required": false, - "in": "query", - "description": "Filter by tag IDs", - "schema": { - "type": "array", - "items": { - "type": "string", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" - }, - "nullable": true - } - }, - { - "name": "takenAfter", - "required": false, - "in": "query", - "description": "Filter by taken date (after)", - "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", - "type": "string" - } - }, - { - "name": "takenBefore", - "required": false, - "in": "query", - "description": "Filter by taken date (before)", - "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", - "type": "string" - } - }, - { - "name": "trashedAfter", - "required": false, - "in": "query", - "description": "Filter by trash date (after)", - "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", - "type": "string" - } - }, - { - "name": "trashedBefore", - "required": false, - "in": "query", - "description": "Filter by trash date (before)", - "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", - "type": "string" - } - }, - { - "name": "type", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/AssetTypeEnum" - } - }, - { - "name": "updatedAfter", - "required": false, - "in": "query", - "description": "Filter by update date (after)", - "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", - "type": "string" - } - }, - { - "name": "updatedBefore", - "required": false, - "in": "query", - "description": "Filter by update date (before)", - "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", - "type": "string" - } - }, - { - "name": "visibility", - "required": false, - "in": "query", - "schema": { - "$ref": "#/components/schemas/AssetVisibility" - } - }, - { - "name": "withDeleted", - "required": false, - "in": "query", - "description": "Include deleted assets", - "schema": { - "type": "boolean" - } - }, - { - "name": "withExif", - "required": false, - "in": "query", - "description": "Include EXIF data in response", - "schema": { - "type": "boolean" - } + "version": "v3.0.0", + "state": "Added" } ], + "x-immich-permission": "plugin.read" + } + }, + "/plugins/templates": { + "get": { + "description": "Retrieve workflow templates provided by installed plugins", + "operationId": "searchPluginTemplates", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/AssetResponseDto" + "$ref": "#/components/schemas/PluginTemplateResponseDto" }, "type": "array" } @@ -10597,66 +10332,41 @@ "api_key": [] } ], - "summary": "Search large assets", + "summary": "Retrieve workflow templates", "tags": [ - "Search" + "Plugins" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.0.0", "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Stable" + "x-immich-permission": "plugin.read" } }, - "/search/metadata": { - "post": { - "description": "Search for assets based on various metadata criteria.", - "operationId": "searchAssets", + "/plugins/{id}": { + "get": { + "description": "Retrieve information about a specific plugin by its ID.", + "operationId": "getPlugin", "parameters": [ { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", + "name": "id", + "required": true, + "in": "path", "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MetadataSearchDto" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SearchResponseDto" + "$ref": "#/components/schemas/PluginResponseDto" } } }, @@ -10674,59 +10384,99 @@ "api_key": [] } ], - "summary": "Search assets by metadata", + "summary": "Retrieve a plugin", "tags": [ - "Search" + "Plugins" ], "x-immich-history": [ { - "version": "v1", + "version": "v3.0.0", "state": "Added" - }, + } + ], + "x-immich-permission": "plugin.read" + } + }, + "/public/config": { + "get": { + "description": "Retrieve the system configuration properties that are visible to everyone.", + "operationId": "getPublicConfig", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicConfigDto" + } + } + }, + "description": "" + } + }, + "summary": "Get the public configuration", + "tags": [ + "Config (public)" + ], + "x-immich-history": [ { - "version": "v1", - "state": "Beta" + "version": "v3.2.0", + "state": "Added" }, { - "version": "v2", - "state": "Stable" + "version": "v3.2.0", + "state": "Alpha" } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Stable" + "x-immich-state": "Alpha" } }, - "/search/person": { + "/public/config/defaults": { "get": { - "description": "Search for people by name.", - "operationId": "searchPerson", - "parameters": [ + "description": "Retrieve the default value of the configuration properties that are visible to everyone.", + "operationId": "getPublicConfigDefaults", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublicConfigDto" + } + } + }, + "description": "" + } + }, + "summary": "Get the public configuration defaults", + "tags": [ + "Config (public)" + ], + "x-immich-history": [ { - "name": "name", - "required": true, - "in": "query", - "description": "Person name to search for", - "schema": { - "type": "string" - } + "version": "v3.2.0", + "state": "Added" }, { - "name": "withHidden", - "required": false, - "in": "query", - "description": "Include hidden people", - "schema": { - "type": "boolean" - } + "version": "v3.2.0", + "state": "Alpha" } ], + "x-immich-state": "Alpha" + } + }, + "/queues": { + "get": { + "description": "Retrieves a list of queues.", + "operationId": "getQueues", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/PersonResponseDto" + "$ref": "#/components/schemas/QueueResponseDto" }, "type": "array" } @@ -10746,40 +10496,36 @@ "api_key": [] } ], - "summary": "Search people", + "summary": "List all queues", "tags": [ - "Search" + "Queues" ], + "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v1", + "version": "v2.4.0", "state": "Added" }, { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" + "version": "v2.4.0", + "state": "Alpha" } ], - "x-immich-permission": "person.read", - "x-immich-state": "Stable" + "x-immich-permission": "queue.read", + "x-immich-state": "Alpha" } }, - "/search/places": { + "/queues/{name}": { "get": { - "description": "Search for places by name.", - "operationId": "searchPlaces", + "description": "Retrieves a specific queue by its name.", + "operationId": "getQueue", "parameters": [ { "name": "name", "required": true, - "in": "query", - "description": "Place name to search for", + "in": "path", "schema": { - "type": "string" + "$ref": "#/components/schemas/QueueName" } } ], @@ -10788,10 +10534,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/PlacesResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/QueueResponseDto" } } }, @@ -10809,38 +10552,42 @@ "api_key": [] } ], - "summary": "Search places", + "summary": "Retrieve a queue", "tags": [ - "Search" + "Queues" ], + "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v1", + "version": "v2.4.0", "state": "Added" }, { - "version": "v1", - "state": "Beta" - }, + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-permission": "queue.read", + "x-immich-state": "Alpha" + }, + "put": { + "description": "Change the paused status of a specific queue.", + "operationId": "updateQueue", + "parameters": [ { - "version": "v2", - "state": "Stable" + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Stable" - } - }, - "/search/random": { - "post": { - "description": "Retrieve a random selection of assets based on the provided criteria.", - "operationId": "searchRandom", - "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RandomSearchDto" + "$ref": "#/components/schemas/QueueUpdateDto" } } }, @@ -10851,10 +10598,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/QueueResponseDto" } } }, @@ -10872,52 +10616,51 @@ "api_key": [] } ], - "summary": "Search random assets", + "summary": "Update a queue", "tags": [ - "Search" + "Queues" ], + "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v1", + "version": "v2.4.0", "state": "Added" }, { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" + "version": "v2.4.0", + "state": "Alpha" } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Stable" + "x-immich-permission": "queue.update", + "x-immich-state": "Alpha" } }, - "/search/smart": { - "post": { - "description": "Perform a smart search for assets by using machine learning vectors to determine relevance.", - "operationId": "searchSmart", - "parameters": [], + "/queues/{name}/jobs": { + "delete": { + "description": "Removes all jobs from the specified queue.", + "operationId": "emptyQueue", + "parameters": [ + { + "name": "name", + "required": true, + "in": "path", + "schema": { + "$ref": "#/components/schemas/QueueName" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SmartSearchDto" + "$ref": "#/components/schemas/QueueDeleteDto" } } }, "required": true }, "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -10932,164 +10675,46 @@ "api_key": [] } ], - "summary": "Smart asset search", + "summary": "Empty a queue", "tags": [ - "Search" + "Queues" ], + "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v1", + "version": "v2.4.0", "state": "Added" }, { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" + "version": "v2.4.0", + "state": "Alpha" } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Stable" - } - }, - "/search/statistics": { - "post": { - "description": "Retrieve statistical data about assets based on search criteria, such as the total matching count.", - "operationId": "searchAssetStatistics", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StatisticsSearchDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchStatisticsResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Search asset statistics", - "tags": [ - "Search" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "asset.statistics", - "x-immich-state": "Stable" - } - }, - "/search/suggestions": { + "x-immich-permission": "queueJob.delete", + "x-immich-state": "Alpha" + }, "get": { - "description": "Retrieve search suggestions based on partial input. This endpoint is used for typeahead search features.", - "operationId": "getSearchSuggestions", + "description": "Retrieves a list of queue jobs from the specified queue.", + "operationId": "getQueueJobs", "parameters": [ { - "name": "country", - "required": false, - "in": "query", - "description": "Filter by country", - "schema": { - "type": "string" - } - }, - { - "name": "includeNull", - "required": false, - "in": "query", - "description": "Include null values in suggestions", - "x-immich-history": [ - { - "version": "v1.111.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable", - "schema": { - "type": "boolean" - } - }, - { - "name": "lensModel", - "required": false, - "in": "query", - "description": "Filter by lens model", - "schema": { - "type": "string" - } - }, - { - "name": "make", - "required": false, - "in": "query", - "description": "Filter by camera make", - "schema": { - "type": "string" - } - }, - { - "name": "model", - "required": false, - "in": "query", - "description": "Filter by camera model", + "name": "name", + "required": true, + "in": "path", "schema": { - "type": "string" + "$ref": "#/components/schemas/QueueName" } }, { - "name": "state", + "name": "status", "required": false, "in": "query", - "description": "Filter by state/province", - "schema": { - "type": "string" - } - }, - { - "name": "type", - "required": true, - "in": "query", + "description": "Filter jobs by status", "schema": { - "$ref": "#/components/schemas/SearchSuggestionType" + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueJobStatus" + } } } ], @@ -11099,7 +10724,7 @@ "application/json": { "schema": { "items": { - "type": "string" + "$ref": "#/components/schemas/QueueJobResponseDto" }, "type": "array" } @@ -11119,39 +10744,39 @@ "api_key": [] } ], - "summary": "Retrieve search suggestions", + "summary": "Retrieve queue jobs", "tags": [ - "Search" + "Queues" ], + "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v1", + "version": "v2.4.0", "state": "Added" }, { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" + "version": "v2.4.0", + "state": "Alpha" } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Stable" + "x-immich-permission": "queueJob.read", + "x-immich-state": "Alpha" } }, - "/server/about": { + "/search/cities": { "get": { - "description": "Retrieve a list of information about the server.", - "operationId": "getAboutInfo", + "description": "Retrieve a list of assets with each asset belonging to a different city. This endpoint is used on the places pages to show a single thumbnail for each city the user has assets in.", + "operationId": "getAssetsByCity", "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServerAboutResponseDto" + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" } } }, @@ -11169,9 +10794,9 @@ "api_key": [] } ], - "summary": "Get server information", + "summary": "Retrieve assets by city", "tags": [ - "Server" + "Search" ], "x-immich-history": [ { @@ -11187,21 +10812,24 @@ "state": "Stable" } ], - "x-immich-permission": "server.about", + "x-immich-permission": "asset.read", "x-immich-state": "Stable" } }, - "/server/apk-links": { + "/search/explore": { "get": { - "description": "Retrieve links to the APKs for the current server version.", - "operationId": "getApkLinks", + "description": "Retrieve data for the explore section, such as popular people and places.", + "operationId": "getExploreData", "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServerApkLinksDto" + "items": { + "$ref": "#/components/schemas/SearchExploreResponseDto" + }, + "type": "array" } } }, @@ -11219,9 +10847,9 @@ "api_key": [] } ], - "summary": "Get APK links", + "summary": "Retrieve explore data", "tags": [ - "Server" + "Search" ], "x-immich-history": [ { @@ -11237,328 +10865,379 @@ "state": "Stable" } ], - "x-immich-permission": "server.apkLinks", + "x-immich-permission": "asset.read", "x-immich-state": "Stable" } }, - "/server/config": { - "get": { - "description": "Retrieve the current server configuration.", - "operationId": "getServerConfig", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServerConfigDto" - } + "/search/large-assets": { + "post": { + "description": "Search for assets that are considered large based on specified criteria.", + "operationId": "searchLargeAssets", + "parameters": [ + { + "name": "albumIds", + "required": false, + "in": "query", + "description": "Filter by album IDs", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" } - }, - "description": "" - } - }, - "summary": "Get config", - "tags": [ - "Server" - ], - "x-immich-history": [ + } + }, { - "version": "v1", - "state": "Added" + "name": "city", + "required": false, + "in": "query", + "description": "Filter by city name", + "schema": { + "type": "string", + "nullable": true + } }, { - "version": "v1", - "state": "Beta" + "name": "country", + "required": false, + "in": "query", + "description": "Filter by country name", + "schema": { + "type": "string", + "nullable": true + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" - } - }, - "/server/features": { - "get": { - "description": "Retrieve available features supported by this server.", - "operationId": "getServerFeatures", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServerFeaturesDto" - } - } - }, - "description": "" - } - }, - "summary": "Get features", - "tags": [ - "Server" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" + "name": "createdAfter", + "required": false, + "in": "query", + "description": "Filter by creation date (after)", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } }, { - "version": "v1", - "state": "Beta" + "name": "createdBefore", + "required": false, + "in": "query", + "description": "Filter by creation date (before)", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" - } - }, - "/server/license": { - "delete": { - "description": "Delete the currently set server product key.", - "operationId": "deleteServerLicense", - "parameters": [], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] + "name": "isEncoded", + "required": false, + "in": "query", + "description": "Filter by encoded status", + "schema": { + "type": "boolean" + } }, { - "cookie": [] + "name": "isFavorite", + "required": false, + "in": "query", + "description": "Filter by favorite status", + "schema": { + "type": "boolean" + } }, { - "api_key": [] - } - ], - "summary": "Delete server product key", - "tags": [ - "Server" - ], - "x-immich-admin-only": true, - "x-immich-history": [ + "name": "isMotion", + "required": false, + "in": "query", + "description": "Filter by motion photo status", + "schema": { + "type": "boolean" + } + }, { - "version": "v1", - "state": "Added" + "name": "isNotInAlbum", + "required": false, + "in": "query", + "description": "Filter assets not in any album", + "schema": { + "type": "boolean" + } }, { - "version": "v1", - "state": "Beta" + "name": "isOffline", + "required": false, + "in": "query", + "description": "Filter by offline status", + "schema": { + "type": "boolean" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "serverLicense.delete", - "x-immich-state": "Stable" - }, - "get": { - "description": "Retrieve information about whether the server currently has a product key registered.", - "operationId": "getServerLicense", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LicenseResponseDto" - } - } - }, - "description": "" + "name": "lensModel", + "required": false, + "in": "query", + "description": "Filter by lens model", + "schema": { + "type": "string", + "nullable": true + } }, - "404": { - "description": "" - } - }, - "security": [ { - "bearer": [] + "name": "libraryId", + "required": false, + "in": "query", + "description": "Library ID to filter by", + "schema": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "nullable": true + } }, { - "cookie": [] + "name": "make", + "required": false, + "in": "query", + "description": "Filter by camera make", + "schema": { + "type": "string", + "nullable": true + } }, { - "api_key": [] - } - ], - "summary": "Get product key", - "tags": [ - "Server" - ], - "x-immich-admin-only": true, - "x-immich-history": [ + "name": "minFileSize", + "required": false, + "in": "query", + "description": "Minimum file size in bytes", + "schema": { + "minimum": 0, + "maximum": 9007199254740991, + "type": "integer" + } + }, { - "version": "v1", - "state": "Added" + "name": "model", + "required": false, + "in": "query", + "description": "Filter by camera model", + "schema": { + "type": "string", + "nullable": true + } }, { - "version": "v1", - "state": "Beta" + "name": "ocr", + "required": false, + "in": "query", + "description": "Filter by OCR text content", + "schema": { + "type": "string" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "serverLicense.read", - "x-immich-state": "Stable" - }, - "put": { - "description": "Validate and set the server product key if successful.", - "operationId": "setServerLicense", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LicenseKeyDto" + "name": "personIds", + "required": false, + "in": "query", + "description": "Filter by person IDs", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" } } }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/LicenseResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ { - "bearer": [] - }, + "name": "rating", + "required": false, + "in": "query", + "description": "Filter by rating [1-5], or null for unrated", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + }, + { + "version": "v3", + "state": "Updated", + "description": "Using -1 as a rating is no longer valid." + } + ], + "x-immich-state": "Stable", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 5, + "nullable": true + } + }, { - "cookie": [] + "name": "size", + "required": false, + "in": "query", + "description": "Number of results to return", + "schema": { + "minimum": 1, + "maximum": 1000, + "type": "integer" + } }, { - "api_key": [] - } - ], - "summary": "Set server product key", - "tags": [ - "Server" - ], - "x-immich-admin-only": true, - "x-immich-history": [ + "name": "state", + "required": false, + "in": "query", + "description": "Filter by state/province name", + "schema": { + "type": "string", + "nullable": true + } + }, { - "version": "v1", - "state": "Added" + "name": "tagIds", + "required": false, + "in": "query", + "description": "Filter by tag IDs", + "schema": { + "type": "array", + "items": { + "type": "string", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" + }, + "nullable": true + } }, { - "version": "v1", - "state": "Beta" + "name": "takenAfter", + "required": false, + "in": "query", + "description": "Filter by taken date (after)", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "serverLicense.update", - "x-immich-state": "Stable" - } - }, - "/server/media-types": { - "get": { - "description": "Retrieve all media types supported by the server.", - "operationId": "getSupportedMediaTypes", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServerMediaTypesResponseDto" - } - } - }, - "description": "" - } - }, - "summary": "Get supported media types", - "tags": [ - "Server" - ], - "x-immich-history": [ + "name": "takenBefore", + "required": false, + "in": "query", + "description": "Filter by taken date (before)", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } + }, { - "version": "v1", - "state": "Added" + "name": "trashedAfter", + "required": false, + "in": "query", + "description": "Filter by trash date (after)", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } }, { - "version": "v1", - "state": "Beta" + "name": "trashedBefore", + "required": false, + "in": "query", + "description": "Filter by trash date (before)", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" - } - }, - "/server/ping": { - "get": { - "description": "Pong", - "operationId": "pingServer", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ServerPingResponse" - } - } - }, - "description": "" - } - }, - "summary": "Ping", - "tags": [ - "Server" - ], - "x-immich-history": [ + "name": "type", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetTypeEnum" + } + }, { - "version": "v1", - "state": "Added" + "name": "updatedAfter", + "required": false, + "in": "query", + "description": "Filter by update date (after)", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } }, { - "version": "v1", - "state": "Beta" + "name": "updatedBefore", + "required": false, + "in": "query", + "description": "Filter by update date (before)", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } }, { - "version": "v2", - "state": "Stable" + "name": "visibility", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetVisibility" + } + }, + { + "name": "withDeleted", + "required": false, + "in": "query", + "description": "Include deleted assets", + "schema": { + "type": "boolean" + } + }, + { + "name": "withExif", + "required": false, + "in": "query", + "description": "Include EXIF data in response", + "schema": { + "type": "boolean" + } } ], - "x-immich-state": "Stable" - } - }, - "/server/statistics": { - "get": { - "description": "Retrieve statistics about the entire Immich instance such as asset counts.", - "operationId": "getServerStatistics", - "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServerStatsResponseDto" + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" } } }, @@ -11576,11 +11255,10 @@ "api_key": [] } ], - "summary": "Get statistics", + "summary": "Search large assets", "tags": [ - "Server" + "Search" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -11595,21 +11273,48 @@ "state": "Stable" } ], - "x-immich-permission": "server.statistics", + "x-immich-permission": "asset.read", "x-immich-state": "Stable" } }, - "/server/storage": { - "get": { - "description": "Retrieve the current storage utilization information of the server.", - "operationId": "getStorage", - "parameters": [], + "/search/metadata": { + "post": { + "description": "Search for assets based on various metadata criteria.", + "operationId": "searchAssets", + "parameters": [ + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MetadataSearchDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServerStorageResponseDto" + "$ref": "#/components/schemas/SearchResponseDto" } } }, @@ -11627,9 +11332,9 @@ "api_key": [] } ], - "summary": "Get storage", + "summary": "Search assets by metadata", "tags": [ - "Server" + "Search" ], "x-immich-history": [ { @@ -11645,30 +11350,63 @@ "state": "Stable" } ], - "x-immich-permission": "server.storage", + "x-immich-permission": "asset.read", "x-immich-state": "Stable" } }, - "/server/version": { + "/search/person": { "get": { - "description": "Retrieve the current server version in semantic versioning (semver) format.", - "operationId": "getServerVersion", - "parameters": [], + "description": "Search for people by name.", + "operationId": "searchPerson", + "parameters": [ + { + "name": "name", + "required": true, + "in": "query", + "description": "Person name to search for", + "schema": { + "type": "string" + } + }, + { + "name": "withHidden", + "required": false, + "in": "query", + "description": "Include hidden people", + "schema": { + "type": "boolean" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServerVersionResponseDto" + "items": { + "$ref": "#/components/schemas/PersonResponseDto" + }, + "type": "array" } } }, "description": "" } }, - "summary": "Get server version", + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Search people", "tags": [ - "Server" + "Search" ], "x-immich-history": [ { @@ -11684,20 +11422,34 @@ "state": "Stable" } ], + "x-immich-permission": "person.read", "x-immich-state": "Stable" } }, - "/server/version-check": { + "/search/places": { "get": { - "description": "Retrieve information about the last time the version check ran.", - "operationId": "getVersionCheck", - "parameters": [], + "description": "Search for places by name.", + "operationId": "searchPlaces", + "parameters": [ + { + "name": "name", + "required": true, + "in": "query", + "description": "Place name to search for", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VersionCheckStateResponseDto" + "items": { + "$ref": "#/components/schemas/PlacesResponseDto" + }, + "type": "array" } } }, @@ -11715,9 +11467,9 @@ "api_key": [] } ], - "summary": "Get version check status", + "summary": "Search places", "tags": [ - "Server" + "Search" ], "x-immich-history": [ { @@ -11733,22 +11485,32 @@ "state": "Stable" } ], - "x-immich-permission": "server.versionCheck", + "x-immich-permission": "asset.read", "x-immich-state": "Stable" } }, - "/server/version-history": { - "get": { - "description": "Retrieve a list of past versions the server has been on.", - "operationId": "getVersionHistory", + "/search/random": { + "post": { + "description": "Retrieve a random selection of assets based on the provided criteria.", + "operationId": "searchRandom", "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RandomSearchDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/ServerVersionHistoryResponseDto" + "$ref": "#/components/schemas/AssetResponseDto" }, "type": "array" } @@ -11757,37 +11519,6 @@ "description": "" } }, - "summary": "Get version history", - "tags": [ - "Server" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" - } - }, - "/sessions": { - "delete": { - "description": "Delete all sessions for the user. This will not delete the current session.", - "operationId": "deleteAllSessions", - "parameters": [], - "responses": { - "204": { - "description": "" - } - }, "security": [ { "bearer": [] @@ -11799,9 +11530,9 @@ "api_key": [] } ], - "summary": "Delete all sessions", + "summary": "Search random assets", "tags": [ - "Sessions" + "Search" ], "x-immich-history": [ { @@ -11817,22 +11548,31 @@ "state": "Stable" } ], - "x-immich-permission": "session.delete", + "x-immich-permission": "asset.read", "x-immich-state": "Stable" - }, - "get": { - "description": "Retrieve a list of sessions for the user.", - "operationId": "getSessions", + } + }, + "/search/smart": { + "post": { + "description": "Perform a smart search for assets by using machine learning vectors to determine relevance.", + "operationId": "searchSmart", "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SmartSearchDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/SessionResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/SearchResponseDto" } } }, @@ -11850,9 +11590,9 @@ "api_key": [] } ], - "summary": "Retrieve sessions", + "summary": "Smart asset search", "tags": [ - "Sessions" + "Search" ], "x-immich-history": [ { @@ -11868,29 +11608,31 @@ "state": "Stable" } ], - "x-immich-permission": "session.read", + "x-immich-permission": "asset.read", "x-immich-state": "Stable" - }, + } + }, + "/search/statistics": { "post": { - "description": "Create a session as a child to the current session. This endpoint is used for casting.", - "operationId": "createSession", + "description": "Retrieve statistical data about assets based on search criteria, such as the total matching count.", + "operationId": "searchAssetStatistics", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionCreateDto" + "$ref": "#/components/schemas/StatisticsSearchDto" } } }, "required": true }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionCreateResponseDto" + "$ref": "#/components/schemas/SearchStatisticsResponseDto" } } }, @@ -11908,9 +11650,9 @@ "api_key": [] } ], - "summary": "Create a session", + "summary": "Search asset statistics", "tags": [ - "Sessions" + "Search" ], "x-immich-history": [ { @@ -11926,95 +11668,98 @@ "state": "Stable" } ], - "x-immich-permission": "session.create", + "x-immich-permission": "asset.statistics", "x-immich-state": "Stable" } }, - "/sessions/{id}": { - "delete": { - "description": "Delete a specific session by id.", - "operationId": "deleteSession", + "/search/suggestions": { + "get": { + "description": "Retrieve search suggestions based on partial input. This endpoint is used for typeahead search features.", + "operationId": "getSearchSuggestions", "parameters": [ { - "name": "id", - "required": true, - "in": "path", + "name": "country", + "required": false, + "in": "query", + "description": "Filter by country", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } - } - ], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] }, { - "cookie": [] + "name": "includeNull", + "required": false, + "in": "query", + "description": "Include null values in suggestions", + "x-immich-history": [ + { + "version": "v1.111.0", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-state": "Stable", + "schema": { + "type": "boolean" + } }, { - "api_key": [] - } - ], - "summary": "Delete a session", - "tags": [ - "Sessions" - ], - "x-immich-history": [ + "name": "lensModel", + "required": false, + "in": "query", + "description": "Filter by lens model", + "schema": { + "type": "string" + } + }, { - "version": "v1", - "state": "Added" + "name": "make", + "required": false, + "in": "query", + "description": "Filter by camera make", + "schema": { + "type": "string" + } }, { - "version": "v1", - "state": "Beta" + "name": "model", + "required": false, + "in": "query", + "description": "Filter by camera model", + "schema": { + "type": "string" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "session.delete", - "x-immich-state": "Stable" - }, - "put": { - "deprecated": true, - "description": "Update a specific session identified by id.", - "operationId": "updateSession", - "parameters": [ + "name": "state", + "required": false, + "in": "query", + "description": "Filter by state/province", + "schema": { + "type": "string" + } + }, { - "name": "id", + "name": "type", "required": true, - "in": "path", + "in": "query", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "$ref": "#/components/schemas/SearchSuggestionType" } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SessionUpdateDto" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SessionResponseDto" + "items": { + "type": "string" + }, + "type": "array" } } }, @@ -12032,10 +11777,9 @@ "api_key": [] } ], - "summary": "Update a session", + "summary": "Retrieve search suggestions", "tags": [ - "Sessions", - "Deprecated" + "Search" ], "x-immich-history": [ { @@ -12049,35 +11793,26 @@ { "version": "v2", "state": "Stable" - }, - { - "version": "v3", - "state": "Deprecated", - "replacementId": "updateSession" } ], - "x-immich-permission": "session.update", - "x-immich-state": "Deprecated" + "x-immich-permission": "asset.read", + "x-immich-state": "Stable" } }, - "/sessions/{id}/lock": { - "post": { - "description": "Lock a specific session by id.", - "operationId": "lockSession", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "/server/about": { + "get": { + "description": "Retrieve a list of information about the server.", + "operationId": "getAboutInfo", + "parameters": [], "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerAboutResponseDto" + } + } + }, "description": "" } }, @@ -12092,9 +11827,9 @@ "api_key": [] } ], - "summary": "Lock a session", + "summary": "Get server information", "tags": [ - "Sessions" + "Server" ], "x-immich-history": [ { @@ -12110,53 +11845,21 @@ "state": "Stable" } ], - "x-immich-permission": "session.lock", + "x-immich-permission": "server.about", "x-immich-state": "Stable" } }, - "/shared-links": { + "/server/apk-links": { "get": { - "description": "Retrieve a list of all shared links.", - "operationId": "getAllSharedLinks", - "parameters": [ - { - "name": "albumId", - "required": false, - "in": "query", - "description": "Filter by album ID", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - { - "name": "id", - "required": false, - "in": "query", - "description": "Filter by shared link ID", - "x-immich-history": [ - { - "version": "v2.5.0", - "state": "Added" - } - ], - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "description": "Retrieve links to the APKs for the current server version.", + "operationId": "getApkLinks", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/SharedLinkResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/ServerApkLinksDto" } } }, @@ -12174,9 +11877,9 @@ "api_key": [] } ], - "summary": "Retrieve all shared links", + "summary": "Get APK links", "tags": [ - "Shared links" + "Server" ], "x-immich-history": [ { @@ -12192,49 +11895,32 @@ "state": "Stable" } ], - "x-immich-permission": "sharedLink.read", + "x-immich-permission": "server.apkLinks", "x-immich-state": "Stable" - }, - "post": { - "description": "Create a new shared link.", - "operationId": "createSharedLink", + } + }, + "/server/config": { + "get": { + "deprecated": true, + "description": "Retrieve the current server configuration.", + "operationId": "getServerConfig", "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SharedLinkCreateDto" - } - } - }, - "required": true - }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SharedLinkResponseDto" + "$ref": "#/components/schemas/ServerConfigDto" } } }, "description": "" } }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Create a shared link", + "summary": "Get config", "tags": [ - "Shared links" + "Server", + "Deprecated" ], "x-immich-history": [ { @@ -12248,132 +11934,38 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "getPublicConfig" } ], - "x-immich-permission": "sharedLink.create", - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" } }, - "/shared-links/login": { - "post": { - "description": "Login to a password protected shared link", - "operationId": "sharedLinkLogin", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SharedLinkLoginDto" - } - } - }, - "required": true - }, - "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SharedLinkResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Shared link login", - "tags": [ - "Shared links" - ], - "x-immich-history": [ - { - "version": "v2.6.0", - "state": "Added" - }, - { - "version": "v2.6.0", - "state": "Beta" - } - ], - "x-immich-state": "Beta" - } - }, - "/shared-links/me": { + "/server/features": { "get": { - "description": "Retrieve the current shared link associated with authentication method.", - "operationId": "getMySharedLink", - "parameters": [ - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], + "deprecated": true, + "description": "Retrieve available features supported by this server.", + "operationId": "getServerFeatures", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SharedLinkResponseDto" + "$ref": "#/components/schemas/ServerFeaturesDto" } } }, "description": "" } }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Retrieve current shared link", + "summary": "Get features", "tags": [ - "Shared links" + "Server", + "Deprecated" ], "x-immich-history": [ { @@ -12387,27 +11979,21 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "getPublicConfig" } ], - "x-immich-state": "Stable" + "x-immich-state": "Deprecated" } }, - "/shared-links/{id}": { + "/server/license": { "delete": { - "description": "Delete a specific shared link by its ID.", - "operationId": "removeSharedLink", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "description": "Delete the currently set server product key.", + "operationId": "deleteServerLicense", + "parameters": [], "responses": { "204": { "description": "" @@ -12424,10 +12010,11 @@ "api_key": [] } ], - "summary": "Delete a shared link", + "summary": "Delete server product key", "tags": [ - "Shared links" + "Server" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -12442,34 +12029,26 @@ "state": "Stable" } ], - "x-immich-permission": "sharedLink.delete", + "x-immich-permission": "serverLicense.delete", "x-immich-state": "Stable" }, "get": { - "description": "Retrieve a specific shared link by its ID.", - "operationId": "getSharedLinkById", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "description": "Retrieve information about whether the server currently has a product key registered.", + "operationId": "getServerLicense", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SharedLinkResponseDto" + "$ref": "#/components/schemas/LicenseResponseDto" } } }, "description": "" + }, + "404": { + "description": "" } }, "security": [ @@ -12483,10 +12062,11 @@ "api_key": [] } ], - "summary": "Retrieve a shared link", + "summary": "Get product key", "tags": [ - "Shared links" + "Server" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -12501,29 +12081,18 @@ "state": "Stable" } ], - "x-immich-permission": "sharedLink.read", + "x-immich-permission": "serverLicense.read", "x-immich-state": "Stable" }, - "patch": { - "description": "Update an existing shared link by its ID.", - "operationId": "updateSharedLink", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "put": { + "description": "Validate and set the server product key if successful.", + "operationId": "setServerLicense", + "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SharedLinkEditDto" + "$ref": "#/components/schemas/LicenseKeyDto" } } }, @@ -12534,7 +12103,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SharedLinkResponseDto" + "$ref": "#/components/schemas/LicenseResponseDto" } } }, @@ -12552,10 +12121,11 @@ "api_key": [] } ], - "summary": "Update a shared link", + "summary": "Set server product key", "tags": [ - "Shared links" + "Server" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -12570,65 +12140,30 @@ "state": "Stable" } ], - "x-immich-permission": "sharedLink.update", + "x-immich-permission": "serverLicense.update", "x-immich-state": "Stable" } }, - "/shared-links/{id}/assets": { - "delete": { - "description": "Remove assets from a specific shared link by its ID. This endpoint is only relevant for shared link of type individual.", - "operationId": "removeSharedLinkAssets", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetIdsDto" - } - } - }, - "required": true - }, + "/server/media-types": { + "get": { + "description": "Retrieve all media types supported by the server.", + "operationId": "getSupportedMediaTypes", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/AssetIdsResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/ServerMediaTypesResponseDto" } } }, "description": "" } }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Remove assets from a shared link", + "summary": "Get supported media types", "tags": [ - "Shared links" + "Server" ], "x-immich-history": [ { @@ -12644,43 +12179,58 @@ "state": "Stable" } ], - "x-immich-permission": "sharedLink.update", "x-immich-state": "Stable" - }, - "put": { - "description": "Add assets to a specific shared link by its ID. This endpoint is only relevant for shared link of type individual.", - "operationId": "addSharedLinkAssets", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } + } + }, + "/server/ping": { + "get": { + "description": "Pong", + "operationId": "pingServer", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerPingResponse" + } + } + }, + "description": "" } + }, + "summary": "Ping", + "tags": [ + "Server" ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetIdsDto" - } - } + "x-immich-history": [ + { + "version": "v1", + "state": "Added" }, - "required": true - }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-state": "Stable" + } + }, + "/server/statistics": { + "get": { + "description": "Retrieve statistics about the entire Immich instance such as asset counts.", + "operationId": "getServerStatistics", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/AssetIdsResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/ServerStatsResponseDto" } } }, @@ -12698,10 +12248,11 @@ "api_key": [] } ], - "summary": "Add assets to a shared link", + "summary": "Get statistics", "tags": [ - "Shared links" + "Server" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -12716,27 +12267,24 @@ "state": "Stable" } ], - "x-immich-permission": "sharedLink.update", + "x-immich-permission": "server.statistics", "x-immich-state": "Stable" } }, - "/stacks": { - "delete": { - "description": "Delete multiple stacks by providing a list of stack IDs.", - "operationId": "deleteStacks", + "/server/storage": { + "get": { + "description": "Retrieve the current storage utilization information of the server.", + "operationId": "getStorage", "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkIdsDto" - } - } - }, - "required": true - }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerStorageResponseDto" + } + } + }, "description": "" } }, @@ -12751,9 +12299,9 @@ "api_key": [] } ], - "summary": "Delete stacks", + "summary": "Get storage", "tags": [ - "Stacks" + "Server" ], "x-immich-history": [ { @@ -12769,34 +12317,59 @@ "state": "Stable" } ], - "x-immich-permission": "stack.delete", + "x-immich-permission": "server.storage", "x-immich-state": "Stable" - }, + } + }, + "/server/version": { "get": { - "description": "Retrieve a list of stacks.", - "operationId": "searchStacks", - "parameters": [ + "description": "Retrieve the current server version in semantic versioning (semver) format.", + "operationId": "getServerVersion", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerVersionResponseDto" + } + } + }, + "description": "" + } + }, + "summary": "Get server version", + "tags": [ + "Server" + ], + "x-immich-history": [ { - "name": "primaryAssetId", - "required": false, - "in": "query", - "description": "Filter by primary asset ID", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], + "x-immich-state": "Stable" + } + }, + "/server/version-check": { + "get": { + "description": "Retrieve information about the last time the version check ran.", + "operationId": "getVersionCheck", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/StackResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/VersionCheckStateResponseDto" } } }, @@ -12814,9 +12387,9 @@ "api_key": [] } ], - "summary": "Retrieve stacks", + "summary": "Get version check status", "tags": [ - "Stacks" + "Server" ], "x-immich-history": [ { @@ -12832,49 +12405,33 @@ "state": "Stable" } ], - "x-immich-permission": "stack.read", + "x-immich-permission": "server.versionCheck", "x-immich-state": "Stable" - }, - "post": { - "description": "Create a new stack by providing a name and a list of asset IDs to include in the stack. If any of the provided asset IDs are primary assets of an existing stack, the existing stack will be merged into the newly created stack.", - "operationId": "createStack", + } + }, + "/server/version-history": { + "get": { + "description": "Retrieve a list of past versions the server has been on.", + "operationId": "getVersionHistory", "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/StackCreateDto" - } - } - }, - "required": true - }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StackResponseDto" + "items": { + "$ref": "#/components/schemas/ServerVersionHistoryResponseDto" + }, + "type": "array" } } }, "description": "" } }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Create a stack", + "summary": "Get version history", "tags": [ - "Stacks" + "Server" ], "x-immich-history": [ { @@ -12890,26 +12447,14 @@ "state": "Stable" } ], - "x-immich-permission": "stack.create", "x-immich-state": "Stable" } }, - "/stacks/{id}": { + "/sessions": { "delete": { - "description": "Delete a specific stack by its ID.", - "operationId": "deleteStack", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "description": "Delete all sessions for the user. This will not delete the current session.", + "operationId": "deleteAllSessions", + "parameters": [], "responses": { "204": { "description": "" @@ -12926,9 +12471,9 @@ "api_key": [] } ], - "summary": "Delete a stack", + "summary": "Delete all sessions", "tags": [ - "Stacks" + "Sessions" ], "x-immich-history": [ { @@ -12944,30 +12489,22 @@ "state": "Stable" } ], - "x-immich-permission": "stack.delete", + "x-immich-permission": "session.delete", "x-immich-state": "Stable" }, "get": { - "description": "Retrieve a specific stack by its ID.", - "operationId": "getStack", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "description": "Retrieve a list of sessions for the user.", + "operationId": "getSessions", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StackResponseDto" + "items": { + "$ref": "#/components/schemas/SessionResponseDto" + }, + "type": "array" } } }, @@ -12985,9 +12522,9 @@ "api_key": [] } ], - "summary": "Retrieve a stack", + "summary": "Retrieve sessions", "tags": [ - "Stacks" + "Sessions" ], "x-immich-history": [ { @@ -13003,41 +12540,29 @@ "state": "Stable" } ], - "x-immich-permission": "stack.read", + "x-immich-permission": "session.read", "x-immich-state": "Stable" }, - "put": { - "deprecated": true, - "description": "Update an existing stack by its ID.", - "operationId": "updateStack", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "post": { + "description": "Create a session as a child to the current session. This endpoint is used for casting.", + "operationId": "createSession", + "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StackUpdateDto" + "$ref": "#/components/schemas/SessionCreateDto" } } }, "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/StackResponseDto" + "$ref": "#/components/schemas/SessionCreateResponseDto" } } }, @@ -13055,10 +12580,9 @@ "api_key": [] } ], - "summary": "Update a stack", + "summary": "Create a session", "tags": [ - "Stacks", - "Deprecated" + "Sessions" ], "x-immich-history": [ { @@ -13072,32 +12596,17 @@ { "version": "v2", "state": "Stable" - }, - { - "version": "v3", - "state": "Deprecated", - "replacementId": "updateStack" } ], - "x-immich-permission": "stack.update", - "x-immich-state": "Deprecated" + "x-immich-permission": "session.create", + "x-immich-state": "Stable" } }, - "/stacks/{id}/assets/{assetId}": { + "/sessions/{id}": { "delete": { - "description": "Remove a specific asset from a stack by providing the stack ID and asset ID.", - "operationId": "removeAssetFromStack", + "description": "Delete a specific session by id.", + "operationId": "deleteSession", "parameters": [ - { - "name": "assetId", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, { "name": "id", "required": true, @@ -13125,9 +12634,9 @@ "api_key": [] } ], - "summary": "Remove an asset from a stack", + "summary": "Delete a session", "tags": [ - "Stacks" + "Sessions" ], "x-immich-history": [ { @@ -13143,27 +12652,44 @@ "state": "Stable" } ], - "x-immich-permission": "stack.update", + "x-immich-permission": "session.delete", "x-immich-state": "Stable" - } - }, - "/sync/ack": { - "delete": { - "description": "Delete specific synchronization acknowledgments.", - "operationId": "deleteSyncAck", - "parameters": [], + }, + "put": { + "deprecated": true, + "description": "Update a specific session identified by id.", + "operationId": "updateSession", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SyncAckDeleteDto" + "$ref": "#/components/schemas/SessionUpdateDto" } } }, "required": true }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionResponseDto" + } + } + }, "description": "" } }, @@ -13178,9 +12704,10 @@ "api_key": [] } ], - "summary": "Delete acknowledgements", + "summary": "Update a session", "tags": [ - "Sync" + "Sessions", + "Deprecated" ], "x-immich-history": [ { @@ -13194,27 +12721,35 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3", + "state": "Deprecated", + "replacementId": "updateSession" + } + ], + "x-immich-permission": "session.update", + "x-immich-state": "Deprecated" + } + }, + "/sessions/{id}/lock": { + "post": { + "description": "Lock a specific session by id.", + "operationId": "lockSession", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } } ], - "x-immich-permission": "syncCheckpoint.delete", - "x-immich-state": "Stable" - }, - "get": { - "description": "Retrieve the synchronization acknowledgments for the current session.", - "operationId": "getSyncAck", - "parameters": [], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/SyncAckDto" - }, - "type": "array" - } - } - }, + "204": { "description": "" } }, @@ -13229,9 +12764,9 @@ "api_key": [] } ], - "summary": "Retrieve acknowledgements", + "summary": "Lock a session", "tags": [ - "Sync" + "Sessions" ], "x-immich-history": [ { @@ -13247,25 +12782,56 @@ "state": "Stable" } ], - "x-immich-permission": "syncCheckpoint.read", + "x-immich-permission": "session.lock", "x-immich-state": "Stable" - }, - "post": { - "description": "Send a list of synchronization acknowledgements to confirm that the latest changes have been received.", - "operationId": "sendSyncAck", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SyncAckSetDto" - } + } + }, + "/shared-links": { + "get": { + "description": "Retrieve a list of all shared links.", + "operationId": "getAllSharedLinks", + "parameters": [ + { + "name": "albumId", + "required": false, + "in": "query", + "description": "Filter by album ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, - "required": true - }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Filter by shared link ID", + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + } + ], + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/SharedLinkResponseDto" + }, + "type": "array" + } + } + }, "description": "" } }, @@ -13280,9 +12846,9 @@ "api_key": [] } ], - "summary": "Acknowledge changes", + "summary": "Retrieve all shared links", "tags": [ - "Sync" + "Shared links" ], "x-immich-history": [ { @@ -13298,27 +12864,32 @@ "state": "Stable" } ], - "x-immich-permission": "syncCheckpoint.update", + "x-immich-permission": "sharedLink.read", "x-immich-state": "Stable" - } - }, - "/sync/stream": { + }, "post": { - "description": "Retrieve a JSON lines streamed response of changes for synchronization. This endpoint is used by the mobile app to efficiently stay up to date with changes.", - "operationId": "getSyncStream", + "description": "Create a new shared link.", + "operationId": "createSharedLink", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SyncStreamDto" + "$ref": "#/components/schemas/SharedLinkCreateDto" } } }, "required": true }, "responses": { - "200": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SharedLinkResponseDto" + } + } + }, "description": "" } }, @@ -13333,9 +12904,9 @@ "api_key": [] } ], - "summary": "Stream sync changes", + "summary": "Create a shared link", "tags": [ - "Sync" + "Shared links" ], "x-immich-history": [ { @@ -13351,80 +12922,48 @@ "state": "Stable" } ], - "x-immich-permission": "sync.stream", + "x-immich-permission": "sharedLink.create", "x-immich-state": "Stable" } }, - "/system-config": { - "get": { - "description": "Retrieve the current system configuration.", - "operationId": "getConfig", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SystemConfigDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Get system configuration", - "tags": [ - "System config" - ], - "x-immich-admin-only": true, - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, + "/shared-links/login": { + "post": { + "description": "Login to a password protected shared link", + "operationId": "sharedLinkLogin", + "parameters": [ { - "version": "v1", - "state": "Beta" + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } }, { - "version": "v2", - "state": "Stable" + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } } ], - "x-immich-permission": "systemConfig.read", - "x-immich-state": "Stable" - }, - "put": { - "description": "Update the system configuration with a new system configuration.", - "operationId": "updateConfig", - "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigDto" + "$ref": "#/components/schemas/SharedLinkLoginDto" } } }, "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigDto" + "$ref": "#/components/schemas/SharedLinkResponseDto" } } }, @@ -13442,40 +12981,51 @@ "api_key": [] } ], - "summary": "Update system configuration", + "summary": "Shared link login", "tags": [ - "System config" + "Shared links" ], - "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v1", + "version": "v2.6.0", "state": "Added" }, { - "version": "v1", + "version": "v2.6.0", "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" } ], - "x-immich-permission": "systemConfig.update", - "x-immich-state": "Stable" + "x-immich-state": "Beta" } }, - "/system-config/defaults": { + "/shared-links/me": { "get": { - "description": "Retrieve the default values for the system configuration.", - "operationId": "getConfigDefaults", - "parameters": [], + "description": "Retrieve the current shared link associated with authentication method.", + "operationId": "getMySharedLink", + "parameters": [ + { + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SystemConfigDto" + "$ref": "#/components/schemas/SharedLinkResponseDto" } } }, @@ -13493,11 +13043,10 @@ "api_key": [] } ], - "summary": "Get system configuration defaults", + "summary": "Retrieve current shared link", "tags": [ - "System config" + "Shared links" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -13512,24 +13061,27 @@ "state": "Stable" } ], - "x-immich-permission": "systemConfig.read", "x-immich-state": "Stable" } }, - "/system-config/storage-template-options": { - "get": { - "description": "Retrieve exemplary storage template options.", - "operationId": "getStorageTemplateOptions", - "parameters": [], + "/shared-links/{id}": { + "delete": { + "description": "Delete a specific shared link by its ID.", + "operationId": "removeSharedLink", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SystemConfigTemplateStorageOptionDto" - } - } - }, + "204": { "description": "" } }, @@ -13544,11 +13096,10 @@ "api_key": [] } ], - "summary": "Get storage template options", + "summary": "Delete a shared link", "tags": [ - "System config" + "Shared links" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -13563,21 +13114,30 @@ "state": "Stable" } ], - "x-immich-permission": "systemConfig.read", + "x-immich-permission": "sharedLink.delete", "x-immich-state": "Stable" - } - }, - "/system-metadata/admin-onboarding": { + }, "get": { - "description": "Retrieve the current admin onboarding status.", - "operationId": "getAdminOnboarding", - "parameters": [], + "description": "Retrieve a specific shared link by its ID.", + "operationId": "getSharedLinkById", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminOnboardingUpdateDto" + "$ref": "#/components/schemas/SharedLinkResponseDto" } } }, @@ -13595,11 +13155,10 @@ "api_key": [] } ], - "summary": "Retrieve admin onboarding", + "summary": "Retrieve a shared link", "tags": [ - "System metadata" + "Shared links" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -13614,25 +13173,43 @@ "state": "Stable" } ], - "x-immich-permission": "systemMetadata.read", + "x-immich-permission": "sharedLink.read", "x-immich-state": "Stable" }, - "post": { - "description": "Update the admin onboarding status.", - "operationId": "updateAdminOnboarding", - "parameters": [], + "patch": { + "description": "Update an existing shared link by its ID.", + "operationId": "updateSharedLink", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AdminOnboardingUpdateDto" + "$ref": "#/components/schemas/SharedLinkEditDto" } } }, "required": true }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SharedLinkResponseDto" + } + } + }, "description": "" } }, @@ -13647,11 +13224,10 @@ "api_key": [] } ], - "summary": "Update admin onboarding", + "summary": "Update a shared link", "tags": [ - "System metadata" + "Shared links" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -13666,21 +13242,45 @@ "state": "Stable" } ], - "x-immich-permission": "systemMetadata.update", + "x-immich-permission": "sharedLink.update", "x-immich-state": "Stable" } }, - "/system-metadata/reverse-geocoding-state": { - "get": { - "description": "Retrieve the current state of the reverse geocoding import.", - "operationId": "getReverseGeocodingState", - "parameters": [], + "/shared-links/{id}/assets": { + "delete": { + "description": "Remove assets from a specific shared link by its ID. This endpoint is only relevant for shared link of type individual.", + "operationId": "removeSharedLinkAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetIdsDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ReverseGeocodingStateResponseDto" + "items": { + "$ref": "#/components/schemas/AssetIdsResponseDto" + }, + "type": "array" } } }, @@ -13698,11 +13298,10 @@ "api_key": [] } ], - "summary": "Retrieve reverse geocoding state", + "summary": "Remove assets from a shared link", "tags": [ - "System metadata" + "Shared links" ], - "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -13717,73 +13316,41 @@ "state": "Stable" } ], - "x-immich-permission": "systemMetadata.read", + "x-immich-permission": "sharedLink.update", "x-immich-state": "Stable" - } - }, - "/system-metadata/version-check-state": { - "get": { - "description": "Retrieve the current state of the version check process.", - "operationId": "getVersionCheckState", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VersionCheckStateResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, + }, + "put": { + "description": "Add assets to a specific shared link by its ID. This endpoint is only relevant for shared link of type individual.", + "operationId": "addSharedLinkAssets", + "parameters": [ { - "api_key": [] + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } } ], - "summary": "Retrieve version check state", - "tags": [ - "System metadata" - ], - "x-immich-admin-only": true, - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Beta" + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssetIdsDto" + } + } }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "systemMetadata.read", - "x-immich-state": "Stable" - } - }, - "/tags": { - "get": { - "description": "Retrieve a list of all tags.", - "operationId": "getAllTags", - "parameters": [], + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/TagResponseDto" + "$ref": "#/components/schemas/AssetIdsResponseDto" }, "type": "array" } @@ -13803,9 +13370,9 @@ "api_key": [] } ], - "summary": "Retrieve tags", + "summary": "Add assets to a shared link", "tags": [ - "Tags" + "Shared links" ], "x-immich-history": [ { @@ -13821,32 +13388,27 @@ "state": "Stable" } ], - "x-immich-permission": "tag.read", + "x-immich-permission": "sharedLink.update", "x-immich-state": "Stable" - }, - "post": { - "description": "Create a new tag by providing a name and optional color.", - "operationId": "createTag", + } + }, + "/stacks": { + "delete": { + "description": "Delete multiple stacks by providing a list of stack IDs.", + "operationId": "deleteStacks", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TagCreateDto" + "$ref": "#/components/schemas/BulkIdsDto" } } }, "required": true }, "responses": { - "201": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TagResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -13861,9 +13423,9 @@ "api_key": [] } ], - "summary": "Create a tag", + "summary": "Delete stacks", "tags": [ - "Tags" + "Stacks" ], "x-immich-history": [ { @@ -13879,30 +13441,32 @@ "state": "Stable" } ], - "x-immich-permission": "tag.create", + "x-immich-permission": "stack.delete", "x-immich-state": "Stable" }, - "put": { - "description": "Create or update multiple tags in a single request.", - "operationId": "upsertTags", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TagUpsertDto" - } + "get": { + "description": "Retrieve a list of stacks.", + "operationId": "searchStacks", + "parameters": [ + { + "name": "primaryAssetId", + "required": false, + "in": "query", + "description": "Filter by primary asset ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } - }, - "required": true - }, + } + ], "responses": { "200": { "content": { "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/TagResponseDto" + "$ref": "#/components/schemas/StackResponseDto" }, "type": "array" } @@ -13922,9 +13486,9 @@ "api_key": [] } ], - "summary": "Upsert tags", + "summary": "Retrieve stacks", "tags": [ - "Tags" + "Stacks" ], "x-immich-history": [ { @@ -13940,31 +13504,29 @@ "state": "Stable" } ], - "x-immich-permission": "tag.create", + "x-immich-permission": "stack.read", "x-immich-state": "Stable" - } - }, - "/tags/assets": { - "put": { - "description": "Add multiple tags to multiple assets in a single request.", - "operationId": "bulkTagAssets", + }, + "post": { + "description": "Create a new stack by providing a name and a list of asset IDs to include in the stack. If any of the provided asset IDs are primary assets of an existing stack, the existing stack will be merged into the newly created stack.", + "operationId": "createStack", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TagBulkAssetsDto" + "$ref": "#/components/schemas/StackCreateDto" } } }, "required": true }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TagBulkAssetsResponseDto" + "$ref": "#/components/schemas/StackResponseDto" } } }, @@ -13982,9 +13544,9 @@ "api_key": [] } ], - "summary": "Tag assets", + "summary": "Create a stack", "tags": [ - "Tags" + "Stacks" ], "x-immich-history": [ { @@ -14000,14 +13562,14 @@ "state": "Stable" } ], - "x-immich-permission": "tag.asset", + "x-immich-permission": "stack.create", "x-immich-state": "Stable" } }, - "/tags/{id}": { + "/stacks/{id}": { "delete": { - "description": "Delete a specific tag by its ID.", - "operationId": "deleteTag", + "description": "Delete a specific stack by its ID.", + "operationId": "deleteStack", "parameters": [ { "name": "id", @@ -14036,9 +13598,9 @@ "api_key": [] } ], - "summary": "Delete a tag", + "summary": "Delete a stack", "tags": [ - "Tags" + "Stacks" ], "x-immich-history": [ { @@ -14054,12 +13616,12 @@ "state": "Stable" } ], - "x-immich-permission": "tag.delete", + "x-immich-permission": "stack.delete", "x-immich-state": "Stable" }, "get": { - "description": "Retrieve a specific tag by its ID.", - "operationId": "getTagById", + "description": "Retrieve a specific stack by its ID.", + "operationId": "getStack", "parameters": [ { "name": "id", @@ -14077,7 +13639,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TagResponseDto" + "$ref": "#/components/schemas/StackResponseDto" } } }, @@ -14095,9 +13657,9 @@ "api_key": [] } ], - "summary": "Retrieve a tag", + "summary": "Retrieve a stack", "tags": [ - "Tags" + "Stacks" ], "x-immich-history": [ { @@ -14113,13 +13675,13 @@ "state": "Stable" } ], - "x-immich-permission": "tag.read", + "x-immich-permission": "stack.read", "x-immich-state": "Stable" }, "put": { "deprecated": true, - "description": "Update an existing tag identified by its ID.", - "operationId": "updateTag", + "description": "Update an existing stack by its ID.", + "operationId": "updateStack", "parameters": [ { "name": "id", @@ -14136,7 +13698,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TagUpdateDto" + "$ref": "#/components/schemas/StackUpdateDto" } } }, @@ -14147,7 +13709,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TagResponseDto" + "$ref": "#/components/schemas/StackResponseDto" } } }, @@ -14165,9 +13727,9 @@ "api_key": [] } ], - "summary": "Update a tag", + "summary": "Update a stack", "tags": [ - "Tags", + "Stacks", "Deprecated" ], "x-immich-history": [ @@ -14186,18 +13748,28 @@ { "version": "v3", "state": "Deprecated", - "replacementId": "updateTag" + "replacementId": "updateStack" } ], - "x-immich-permission": "tag.update", + "x-immich-permission": "stack.update", "x-immich-state": "Deprecated" } }, - "/tags/{id}/assets": { + "/stacks/{id}/assets/{assetId}": { "delete": { - "description": "Remove a tag from all the specified assets.", - "operationId": "untagAssets", + "description": "Remove a specific asset from a stack by providing the stack ID and asset ID.", + "operationId": "removeAssetFromStack", "parameters": [ + { + "name": "assetId", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, { "name": "id", "required": true, @@ -14209,28 +13781,8 @@ } } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkIdsDto" - } - } - }, - "required": true - }, "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/BulkIdResponseDto" - }, - "type": "array" - } - } - }, + "204": { "description": "" } }, @@ -14245,9 +13797,9 @@ "api_key": [] } ], - "summary": "Untag assets", + "summary": "Remove an asset from a stack", "tags": [ - "Tags" + "Stacks" ], "x-immich-history": [ { @@ -14263,46 +13815,27 @@ "state": "Stable" } ], - "x-immich-permission": "tag.asset", + "x-immich-permission": "stack.update", "x-immich-state": "Stable" - }, - "put": { - "description": "Add a tag to all the specified assets.", - "operationId": "tagAssets", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + } + }, + "/sync/ack": { + "delete": { + "description": "Delete specific synchronization acknowledgments.", + "operationId": "deleteSyncAck", + "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/BulkIdsDto" + "$ref": "#/components/schemas/SyncAckDeleteDto" } } }, "required": true }, "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/BulkIdResponseDto" - }, - "type": "array" - } - } - }, + "204": { "description": "" } }, @@ -14317,9 +13850,9 @@ "api_key": [] } ], - "summary": "Tag assets", + "summary": "Delete acknowledgements", "tags": [ - "Tags" + "Sync" ], "x-immich-history": [ { @@ -14335,177 +13868,76 @@ "state": "Stable" } ], - "x-immich-permission": "tag.asset", + "x-immich-permission": "syncCheckpoint.delete", "x-immich-state": "Stable" - } - }, - "/timeline/bucket": { + }, "get": { - "description": "Retrieve a string of all asset ids in a given time bucket.", - "operationId": "getTimeBucket", - "parameters": [ - { - "name": "albumId", - "required": false, - "in": "query", - "description": "Filter assets belonging to a specific album", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - { - "name": "bbox", - "required": false, - "in": "query", - "description": "Bounding box coordinates as west,south,east,north (WGS84)", - "schema": { - "example": "11.075683,49.416711,11.117589,49.454875", - "type": "string" - } - }, - { - "name": "isFavorite", - "required": false, - "in": "query", - "description": "Filter by favorite status (true for favorites only, false for non-favorites only)", - "schema": { - "type": "boolean" - } - }, - { - "name": "isTrashed", - "required": false, - "in": "query", - "description": "Filter by trash status (true for trashed assets only, false for non-trashed only)", - "schema": { - "type": "boolean" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "order", - "required": false, - "in": "query", - "description": "Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)", - "schema": { - "$ref": "#/components/schemas/AssetOrder" - } - }, - { - "name": "orderBy", - "required": false, - "in": "query", - "description": "Date to group and order assets by (takenAt for date taken, createdAt for date added to Immich)", - "schema": { - "$ref": "#/components/schemas/AssetOrderBy" - } - }, - { - "name": "personId", - "required": false, - "in": "query", - "description": "Filter assets containing a specific person (face recognition)", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "tagId", - "required": false, - "in": "query", - "description": "Filter assets with a specific tag", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, + "description": "Retrieve the synchronization acknowledgments for the current session.", + "operationId": "getSyncAck", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/SyncAckDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ { - "name": "timeBucket", - "required": true, - "in": "query", - "description": "Time bucket identifier in YYYY-MM-DD format", - "schema": { - "example": "2024-01-01", - "type": "string" - } + "bearer": [] }, { - "name": "userId", - "required": false, - "in": "query", - "description": "Filter assets by specific user ID", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } + "cookie": [] }, { - "name": "visibility", - "required": false, - "in": "query", - "description": "Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)", - "schema": { - "$ref": "#/components/schemas/AssetVisibility" - } - }, + "api_key": [] + } + ], + "summary": "Retrieve acknowledgements", + "tags": [ + "Sync" + ], + "x-immich-history": [ { - "name": "withCoordinates", - "required": false, - "in": "query", - "description": "Include location data in the response", - "schema": { - "type": "boolean" - } + "version": "v1", + "state": "Added" }, { - "name": "withPartners", - "required": false, - "in": "query", - "description": "Include assets shared by partners", - "schema": { - "type": "boolean" - } + "version": "v1", + "state": "Beta" }, { - "name": "withStacked", - "required": false, - "in": "query", - "description": "Include stacked assets in the response. When true, only primary assets from stacks are returned.", - "schema": { - "type": "boolean" - } + "version": "v2", + "state": "Stable" } ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/TimeBucketAssetResponseDto" - } + "x-immich-permission": "syncCheckpoint.read", + "x-immich-state": "Stable" + }, + "post": { + "description": "Send a list of synchronization acknowledgements to confirm that the latest changes have been received.", + "operationId": "sendSyncAck", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncAckSetDto" } - }, + } + }, + "required": true + }, + "responses": { + "204": { "description": "" } }, @@ -14520,9 +13952,9 @@ "api_key": [] } ], - "summary": "Get time bucket", + "summary": "Acknowledge changes", "tags": [ - "Timeline" + "Sync" ], "x-immich-history": [ { @@ -14531,170 +13963,82 @@ }, { "version": "v1", - "state": "Internal" + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Internal" + "x-immich-permission": "syncCheckpoint.update", + "x-immich-state": "Stable" } }, - "/timeline/buckets": { - "get": { - "description": "Retrieve a list of all minimal time buckets.", - "operationId": "getTimeBuckets", - "parameters": [ - { - "name": "albumId", - "required": false, - "in": "query", - "description": "Filter assets belonging to a specific album", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - { - "name": "bbox", - "required": false, - "in": "query", - "description": "Bounding box coordinates as west,south,east,north (WGS84)", - "schema": { - "example": "11.075683,49.416711,11.117589,49.454875", - "type": "string" - } - }, - { - "name": "isFavorite", - "required": false, - "in": "query", - "description": "Filter by favorite status (true for favorites only, false for non-favorites only)", - "schema": { - "type": "boolean" - } - }, - { - "name": "isTrashed", - "required": false, - "in": "query", - "description": "Filter by trash status (true for trashed assets only, false for non-trashed only)", - "schema": { - "type": "boolean" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "order", - "required": false, - "in": "query", - "description": "Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)", - "schema": { - "$ref": "#/components/schemas/AssetOrder" - } - }, - { - "name": "orderBy", - "required": false, - "in": "query", - "description": "Date to group and order assets by (takenAt for date taken, createdAt for date added to Immich)", - "schema": { - "$ref": "#/components/schemas/AssetOrderBy" - } - }, - { - "name": "personId", - "required": false, - "in": "query", - "description": "Filter assets containing a specific person (face recognition)", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" + "/sync/stream": { + "post": { + "description": "Retrieve a JSON lines streamed response of changes for synchronization. This endpoint is used by the mobile app to efficiently stay up to date with changes.", + "operationId": "getSyncStream", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SyncStreamDto" + } } }, + "required": true + }, + "responses": { + "200": { + "description": "" + } + }, + "security": [ { - "name": "tagId", - "required": false, - "in": "query", - "description": "Filter assets with a specific tag", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } + "bearer": [] }, { - "name": "userId", - "required": false, - "in": "query", - "description": "Filter assets by specific user ID", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } + "cookie": [] }, { - "name": "visibility", - "required": false, - "in": "query", - "description": "Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)", - "schema": { - "$ref": "#/components/schemas/AssetVisibility" - } - }, + "api_key": [] + } + ], + "summary": "Stream sync changes", + "tags": [ + "Sync" + ], + "x-immich-history": [ { - "name": "withCoordinates", - "required": false, - "in": "query", - "description": "Include location data in the response", - "schema": { - "type": "boolean" - } + "version": "v1", + "state": "Added" }, { - "name": "withPartners", - "required": false, - "in": "query", - "description": "Include assets shared by partners", - "schema": { - "type": "boolean" - } + "version": "v1", + "state": "Beta" }, { - "name": "withStacked", - "required": false, - "in": "query", - "description": "Include stacked assets in the response. When true, only primary assets from stacks are returned.", - "schema": { - "type": "boolean" - } + "version": "v2", + "state": "Stable" } ], + "x-immich-permission": "sync.stream", + "x-immich-state": "Stable" + } + }, + "/system-config": { + "get": { + "deprecated": true, + "description": "Retrieve the current system configuration.", + "operationId": "getConfig", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/TimeBucketsResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/AdminConfigDto" } } }, @@ -14712,10 +14056,12 @@ "api_key": [] } ], - "summary": "Get time buckets", + "summary": "Get system configuration", "tags": [ - "Timeline" + "System config", + "Deprecated" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -14723,24 +14069,42 @@ }, { "version": "v1", - "state": "Internal" + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "getAdminConfig" } ], - "x-immich-permission": "asset.read", - "x-immich-state": "Internal" - } - }, - "/trash/empty": { - "post": { - "description": "Permanently delete all items in the trash.", - "operationId": "emptyTrash", + "x-immich-permission": "systemConfig.read", + "x-immich-state": "Deprecated" + }, + "put": { + "deprecated": true, + "description": "Update the system configuration with a new system configuration.", + "operationId": "updateConfig", "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminConfigDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrashResponseDto" + "$ref": "#/components/schemas/AdminConfigDto" } } }, @@ -14758,10 +14122,12 @@ "api_key": [] } ], - "summary": "Empty trash", + "summary": "Update system configuration", "tags": [ - "Trash" + "System config", + "Deprecated" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -14774,23 +14140,29 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "updateAdminConfig" } ], - "x-immich-permission": "asset.delete", - "x-immich-state": "Stable" + "x-immich-permission": "systemConfig.update", + "x-immich-state": "Deprecated" } }, - "/trash/restore": { - "post": { - "description": "Restore all items in the trash.", - "operationId": "restoreTrash", + "/system-config/defaults": { + "get": { + "deprecated": true, + "description": "Retrieve the default values for the system configuration.", + "operationId": "getConfigDefaults", "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrashResponseDto" + "$ref": "#/components/schemas/AdminConfigDto" } } }, @@ -14808,10 +14180,12 @@ "api_key": [] } ], - "summary": "Restore trash", + "summary": "Get system configuration defaults", "tags": [ - "Trash" + "System config", + "Deprecated" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -14824,33 +14198,28 @@ { "version": "v2", "state": "Stable" + }, + { + "version": "v3.2.0", + "state": "Deprecated", + "replacementId": "getAdminConfigDefaults" } ], - "x-immich-permission": "asset.delete", - "x-immich-state": "Stable" + "x-immich-permission": "systemConfig.read", + "x-immich-state": "Deprecated" } }, - "/trash/restore/assets": { - "post": { - "description": "Restore specific assets from the trash.", - "operationId": "restoreAssets", + "/system-config/storage-template-options": { + "get": { + "description": "Retrieve exemplary storage template options.", + "operationId": "getStorageTemplateOptions", "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BulkIdsDto" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TrashResponseDto" + "$ref": "#/components/schemas/SystemConfigTemplateStorageOptionDto" } } }, @@ -14868,10 +14237,11 @@ "api_key": [] } ], - "summary": "Restore assets", + "summary": "Get storage template options", "tags": [ - "Trash" + "System config" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -14886,24 +14256,21 @@ "state": "Stable" } ], - "x-immich-permission": "asset.delete", + "x-immich-permission": "systemConfig.read", "x-immich-state": "Stable" } }, - "/users": { + "/system-metadata/admin-onboarding": { "get": { - "description": "Retrieve a list of all users on the server.", - "operationId": "searchUsers", + "description": "Retrieve the current admin onboarding status.", + "operationId": "getAdminOnboarding", "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/UserResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/AdminOnboardingUpdateDto" } } }, @@ -14921,10 +14288,11 @@ "api_key": [] } ], - "summary": "Get all users", + "summary": "Retrieve admin onboarding", "tags": [ - "Users" + "System metadata" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -14939,24 +14307,25 @@ "state": "Stable" } ], - "x-immich-permission": "user.read", + "x-immich-permission": "systemMetadata.read", "x-immich-state": "Stable" - } - }, - "/users/me": { - "get": { - "description": "Retrieve information about the user making the API request.", - "operationId": "getMyUser", + }, + "post": { + "description": "Update the admin onboarding status.", + "operationId": "updateAdminOnboarding", "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserAdminResponseDto" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminOnboardingUpdateDto" } - }, + } + }, + "required": true + }, + "responses": { + "204": { "description": "" } }, @@ -14971,10 +14340,11 @@ "api_key": [] } ], - "summary": "Get current user", + "summary": "Update admin onboarding", "tags": [ - "Users" + "System metadata" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -14989,30 +14359,21 @@ "state": "Stable" } ], - "x-immich-permission": "user.read", + "x-immich-permission": "systemMetadata.update", "x-immich-state": "Stable" - }, - "put": { - "deprecated": true, - "description": "Update the current user making the API request.", - "operationId": "updateMyUser", + } + }, + "/system-metadata/reverse-geocoding-state": { + "get": { + "description": "Retrieve the current state of the reverse geocoding import.", + "operationId": "getReverseGeocodingState", "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserUpdateMeDto" - } - } - }, - "required": true - }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserAdminResponseDto" + "$ref": "#/components/schemas/ReverseGeocodingStateResponseDto" } } }, @@ -15030,11 +14391,11 @@ "api_key": [] } ], - "summary": "Update current user", + "summary": "Retrieve reverse geocoding state", "tags": [ - "Users", - "Deprecated" + "System metadata" ], + "x-immich-admin-only": true, "x-immich-history": [ { "version": "v1", @@ -15047,62 +14408,23 @@ { "version": "v2", "state": "Stable" - }, - { - "version": "v3", - "state": "Deprecated", - "replacementId": "updateMyUser" } ], - "x-immich-permission": "user.update", - "x-immich-state": "Deprecated" + "x-immich-permission": "systemMetadata.read", + "x-immich-state": "Stable" } }, - "/users/me/calendar-heatmap": { + "/system-metadata/version-check-state": { "get": { - "description": "Retrieve activity counts for a specified period, in a calendar heatmap format.", - "operationId": "getMyCalendarHeatmap", - "parameters": [ - { - "name": "from", - "required": false, - "in": "query", - "description": "Start date in UTC", - "schema": { - "format": "date", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", - "example": "2024-01-01", - "type": "string" - } - }, - { - "name": "to", - "required": false, - "in": "query", - "description": "End date in UTC", - "schema": { - "format": "date", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", - "example": "2024-01-01", - "type": "string" - } - }, - { - "name": "type", - "required": false, - "in": "query", - "schema": { - "default": "Upload", - "$ref": "#/components/schemas/CalendarHeatmapType" - } - } - ], + "description": "Retrieve the current state of the version check process.", + "operationId": "getVersionCheckState", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CalendarHeatmapResponseDto" + "$ref": "#/components/schemas/VersionCheckStateResponseDto" } } }, @@ -15120,31 +14442,46 @@ "api_key": [] } ], - "summary": "Retrieve calendar heatmap activity", + "summary": "Retrieve version check state", "tags": [ - "Users" + "System metadata" ], + "x-immich-admin-only": true, "x-immich-history": [ { - "version": "v3", + "version": "v1", "state": "Added" }, { - "version": "v3", + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", "state": "Stable" } ], - "x-immich-permission": "user.read", + "x-immich-permission": "systemMetadata.read", "x-immich-state": "Stable" } }, - "/users/me/license": { - "delete": { - "description": "Delete the registered product key for the current user.", - "operationId": "deleteUserLicense", + "/tags": { + "get": { + "description": "Retrieve a list of all tags.", + "operationId": "getAllTags", "parameters": [], "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TagResponseDto" + }, + "type": "array" + } + } + }, "description": "" } }, @@ -15159,9 +14496,9 @@ "api_key": [] } ], - "summary": "Delete user product key", + "summary": "Retrieve tags", "tags": [ - "Users" + "Tags" ], "x-immich-history": [ { @@ -15177,19 +14514,29 @@ "state": "Stable" } ], - "x-immich-permission": "userLicense.delete", + "x-immich-permission": "tag.read", "x-immich-state": "Stable" }, - "get": { - "description": "Retrieve information about whether the current user has a registered product key.", - "operationId": "getUserLicense", + "post": { + "description": "Create a new tag by providing a name and optional color.", + "operationId": "createTag", "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagCreateDto" + } + } + }, + "required": true + }, "responses": { - "200": { + "201": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LicenseResponseDto" + "$ref": "#/components/schemas/TagResponseDto" } } }, @@ -15207,9 +14554,9 @@ "api_key": [] } ], - "summary": "Retrieve user product key", + "summary": "Create a tag", "tags": [ - "Users" + "Tags" ], "x-immich-history": [ { @@ -15225,18 +14572,18 @@ "state": "Stable" } ], - "x-immich-permission": "userLicense.read", + "x-immich-permission": "tag.create", "x-immich-state": "Stable" }, "put": { - "description": "Register a product key for the current user.", - "operationId": "setUserLicense", + "description": "Create or update multiple tags in a single request.", + "operationId": "upsertTags", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LicenseKeyDto" + "$ref": "#/components/schemas/TagUpsertDto" } } }, @@ -15247,7 +14594,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LicenseResponseDto" + "items": { + "$ref": "#/components/schemas/TagResponseDto" + }, + "type": "array" } } }, @@ -15265,9 +14615,9 @@ "api_key": [] } ], - "summary": "Set user product key", + "summary": "Upsert tags", "tags": [ - "Users" + "Tags" ], "x-immich-history": [ { @@ -15283,62 +14633,31 @@ "state": "Stable" } ], - "x-immich-permission": "userLicense.update", + "x-immich-permission": "tag.create", "x-immich-state": "Stable" } }, - "/users/me/onboarding": { - "delete": { - "description": "Delete the onboarding status of the current user.", - "operationId": "deleteUserOnboarding", - "parameters": [], - "responses": { - "204": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Delete user onboarding", - "tags": [ - "Users" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "userOnboarding.delete", - "x-immich-state": "Stable" - }, - "get": { - "description": "Retrieve the onboarding status of the current user.", - "operationId": "getUserOnboarding", + "/tags/assets": { + "put": { + "description": "Add multiple tags to multiple assets in a single request.", + "operationId": "bulkTagAssets", "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagBulkAssetsDto" + } + } + }, + "required": true + }, "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/OnboardingResponseDto" + "$ref": "#/components/schemas/TagBulkAssetsResponseDto" } } }, @@ -15356,9 +14675,9 @@ "api_key": [] } ], - "summary": "Retrieve user onboarding", + "summary": "Tag assets", "tags": [ - "Users" + "Tags" ], "x-immich-history": [ { @@ -15374,32 +14693,28 @@ "state": "Stable" } ], - "x-immich-permission": "userOnboarding.read", + "x-immich-permission": "tag.asset", "x-immich-state": "Stable" - }, - "put": { - "description": "Update the onboarding status of the current user.", - "operationId": "setUserOnboarding", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OnboardingDto" - } + } + }, + "/tags/{id}": { + "delete": { + "description": "Delete a specific tag by its ID.", + "operationId": "deleteTag", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } - }, - "required": true - }, + } + ], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/OnboardingResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -15414,9 +14729,9 @@ "api_key": [] } ], - "summary": "Update user onboarding", + "summary": "Delete a tag", "tags": [ - "Users" + "Tags" ], "x-immich-history": [ { @@ -15432,21 +14747,30 @@ "state": "Stable" } ], - "x-immich-permission": "userOnboarding.update", + "x-immich-permission": "tag.delete", "x-immich-state": "Stable" - } - }, - "/users/me/preferences": { + }, "get": { - "description": "Retrieve the preferences for the current user.", - "operationId": "getMyPreferences", - "parameters": [], + "description": "Retrieve a specific tag by its ID.", + "operationId": "getTagById", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserPreferencesResponseDto" + "$ref": "#/components/schemas/TagResponseDto" } } }, @@ -15464,9 +14788,9 @@ "api_key": [] } ], - "summary": "Get my preferences", + "summary": "Retrieve a tag", "tags": [ - "Users" + "Tags" ], "x-immich-history": [ { @@ -15482,19 +14806,30 @@ "state": "Stable" } ], - "x-immich-permission": "userPreference.read", + "x-immich-permission": "tag.read", "x-immich-state": "Stable" }, "put": { "deprecated": true, - "description": "Update the preferences of the current user.", - "operationId": "updateMyPreferences", - "parameters": [], + "description": "Update an existing tag identified by its ID.", + "operationId": "updateTag", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserPreferencesUpdateDto" + "$ref": "#/components/schemas/TagUpdateDto" } } }, @@ -15505,7 +14840,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserPreferencesResponseDto" + "$ref": "#/components/schemas/TagResponseDto" } } }, @@ -15523,9 +14858,9 @@ "api_key": [] } ], - "summary": "Update my preferences", + "summary": "Update a tag", "tags": [ - "Users", + "Tags", "Deprecated" ], "x-immich-history": [ @@ -15544,20 +14879,51 @@ { "version": "v3", "state": "Deprecated", - "replacementId": "updateMyPreferences" + "replacementId": "updateTag" } ], - "x-immich-permission": "userPreference.update", + "x-immich-permission": "tag.update", "x-immich-state": "Deprecated" } }, - "/users/profile-image": { + "/tags/{id}/assets": { "delete": { - "description": "Delete the profile image of the current user.", - "operationId": "deleteProfileImage", - "parameters": [], + "description": "Remove a tag from all the specified assets.", + "operationId": "untagAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" + } + } + }, "description": "" } }, @@ -15572,9 +14938,9 @@ "api_key": [] } ], - "summary": "Delete user profile image", + "summary": "Untag assets", "tags": [ - "Users" + "Tags" ], "x-immich-history": [ { @@ -15590,30 +14956,43 @@ "state": "Stable" } ], - "x-immich-permission": "userProfileImage.delete", + "x-immich-permission": "tag.asset", "x-immich-state": "Stable" }, - "post": { - "description": "Upload and set a new profile image for the current user.", - "operationId": "createProfileImage", - "parameters": [], + "put": { + "description": "Add a tag to all the specified assets.", + "operationId": "tagAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], "requestBody": { "content": { - "multipart/form-data": { + "application/json": { "schema": { - "$ref": "#/components/schemas/CreateProfileImageDto" + "$ref": "#/components/schemas/BulkIdsDto" } } }, - "description": "A new avatar for the user", "required": true }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateProfileImageResponseDto" + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" } } }, @@ -15631,9 +15010,9 @@ "api_key": [] } ], - "summary": "Create user profile image", + "summary": "Tag assets", "tags": [ - "Users" + "Tags" ], "x-immich-history": [ { @@ -15649,144 +15028,165 @@ "state": "Stable" } ], - "x-immich-permission": "userProfileImage.update", + "x-immich-permission": "tag.asset", "x-immich-state": "Stable" } }, - "/users/{id}": { + "/timeline/bucket": { "get": { - "description": "Retrieve a specific user by their ID.", - "operationId": "getUser", + "description": "Retrieve a string of all asset ids in a given time bucket.", + "operationId": "getTimeBucket", "parameters": [ { - "name": "id", - "required": true, - "in": "path", + "name": "albumId", + "required": false, + "in": "query", + "description": "Filter assets belonging to a specific album", "schema": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserResponseDto" - } - } - }, - "description": "" - } - }, - "security": [ + }, { - "bearer": [] + "name": "bbox", + "required": false, + "in": "query", + "description": "Bounding box coordinates as west,south,east,north (WGS84)", + "schema": { + "example": "11.075683,49.416711,11.117589,49.454875", + "type": "string" + } }, { - "cookie": [] + "name": "isFavorite", + "required": false, + "in": "query", + "description": "Filter by favorite status (true for favorites only, false for non-favorites only)", + "schema": { + "type": "boolean" + } }, { - "api_key": [] - } - ], - "summary": "Retrieve a user", - "tags": [ - "Users" - ], - "x-immich-history": [ + "name": "isTrashed", + "required": false, + "in": "query", + "description": "Filter by trash status (true for trashed assets only, false for non-trashed only)", + "schema": { + "type": "boolean" + } + }, { - "version": "v1", - "state": "Added" + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } }, { - "version": "v1", - "state": "Beta" + "name": "order", + "required": false, + "in": "query", + "description": "Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "user.read", - "x-immich-state": "Stable" - } - }, - "/users/{id}/profile-image": { - "get": { - "description": "Retrieve the profile image file for a user.", - "operationId": "getProfileImage", - "parameters": [ + "name": "orderBy", + "required": false, + "in": "query", + "description": "Date to group and order assets by (takenAt for date taken, createdAt for date added to Immich)", + "schema": { + "$ref": "#/components/schemas/AssetOrderBy" + } + }, { - "name": "id", - "required": true, - "in": "path", + "name": "personId", + "required": false, + "in": "query", + "description": "Filter assets containing a specific person (face recognition)", "schema": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } - } - ], - "responses": { - "200": { - "content": { - "application/octet-stream": { - "schema": { - "format": "binary", - "type": "string" - } - } - }, - "description": "" - } - }, - "security": [ + }, { - "bearer": [] + "name": "slug", + "required": false, + "in": "query", + "schema": { + "type": "string" + } }, { - "cookie": [] + "name": "tagId", + "required": false, + "in": "query", + "description": "Filter assets with a specific tag", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } }, { - "api_key": [] - } - ], - "summary": "Retrieve user profile image", - "tags": [ - "Users" - ], - "x-immich-history": [ + "name": "timeBucket", + "required": true, + "in": "query", + "description": "Time bucket identifier in YYYY-MM-DD format", + "schema": { + "example": "2024-01-01", + "type": "string" + } + }, { - "version": "v1", - "state": "Added" + "name": "userId", + "required": false, + "in": "query", + "description": "Filter assets by specific user ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } }, { - "version": "v1", - "state": "Beta" + "name": "visibility", + "required": false, + "in": "query", + "description": "Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)", + "schema": { + "$ref": "#/components/schemas/AssetVisibility" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "userProfileImage.read", - "x-immich-state": "Stable" - } - }, - "/view/folder": { - "get": { - "description": "Retrieve assets that are children of a specific folder.", - "operationId": "getAssetsByOriginalPath", - "parameters": [ + "name": "withCoordinates", + "required": false, + "in": "query", + "description": "Include location data in the response", + "schema": { + "type": "boolean" + } + }, { - "name": "path", - "required": true, + "name": "withPartners", + "required": false, "in": "query", + "description": "Include assets shared by partners", "schema": { - "type": "string" + "type": "boolean" + } + }, + { + "name": "withStacked", + "required": false, + "in": "query", + "description": "Include stacked assets in the response. When true, only primary assets from stacks are returned.", + "schema": { + "type": "boolean" } } ], @@ -15795,10 +15195,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/TimeBucketAssetResponseDto" } } }, @@ -15816,9 +15213,9 @@ "api_key": [] } ], - "summary": "Retrieve assets by original path", + "summary": "Get time bucket", "tags": [ - "Views" + "Timeline" ], "x-immich-history": [ { @@ -15827,98 +15224,107 @@ }, { "version": "v1", - "state": "Beta" - }, - { - "version": "v2", - "state": "Stable" + "state": "Internal" } ], - "x-immich-permission": "folder.read", - "x-immich-state": "Stable" + "x-immich-permission": "asset.read", + "x-immich-state": "Internal" } }, - "/view/folder/unique-paths": { + "/timeline/buckets": { "get": { - "description": "Retrieve a list of unique folder paths from asset original paths.", - "operationId": "getUniqueOriginalPaths", - "parameters": [], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "type": "string" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, + "description": "Retrieve a list of all minimal time buckets.", + "operationId": "getTimeBuckets", + "parameters": [ { - "cookie": [] + "name": "albumId", + "required": false, + "in": "query", + "description": "Filter assets belonging to a specific album", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } }, { - "api_key": [] - } - ], - "summary": "Retrieve unique paths", - "tags": [ - "Views" - ], - "x-immich-history": [ + "name": "bbox", + "required": false, + "in": "query", + "description": "Bounding box coordinates as west,south,east,north (WGS84)", + "schema": { + "example": "11.075683,49.416711,11.117589,49.454875", + "type": "string" + } + }, { - "version": "v1", - "state": "Added" + "name": "isFavorite", + "required": false, + "in": "query", + "description": "Filter by favorite status (true for favorites only, false for non-favorites only)", + "schema": { + "type": "boolean" + } }, { - "version": "v1", - "state": "Beta" + "name": "isTrashed", + "required": false, + "in": "query", + "description": "Filter by trash status (true for trashed assets only, false for non-trashed only)", + "schema": { + "type": "boolean" + } }, { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-permission": "folder.read", - "x-immich-state": "Stable" - } - }, - "/workflows": { - "get": { - "description": "Retrieve a list of workflows available to the authenticated user.", - "operationId": "searchWorkflows", - "parameters": [ + "name": "key", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { - "name": "description", + "name": "order", "required": false, "in": "query", - "description": "Workflow description", + "description": "Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, + { + "name": "orderBy", + "required": false, + "in": "query", + "description": "Date to group and order assets by (takenAt for date taken, createdAt for date added to Immich)", + "schema": { + "$ref": "#/components/schemas/AssetOrderBy" + } + }, + { + "name": "personId", + "required": false, + "in": "query", + "description": "Filter assets containing a specific person (face recognition)", "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, { - "name": "enabled", + "name": "slug", "required": false, "in": "query", - "description": "Workflow enabled", "schema": { - "type": "boolean" + "type": "string" } }, { - "name": "id", + "name": "tagId", "required": false, "in": "query", - "description": "Workflow ID", + "description": "Filter assets with a specific tag", "schema": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", @@ -15926,30 +15332,50 @@ } }, { - "name": "logging", + "name": "userId", "required": false, "in": "query", - "description": "Workflow logs run results", + "description": "Filter assets by specific user ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "visibility", + "required": false, + "in": "query", + "description": "Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)", + "schema": { + "$ref": "#/components/schemas/AssetVisibility" + } + }, + { + "name": "withCoordinates", + "required": false, + "in": "query", + "description": "Include location data in the response", "schema": { "type": "boolean" } }, { - "name": "name", + "name": "withPartners", "required": false, "in": "query", - "description": "Workflow name", + "description": "Include assets shared by partners", "schema": { - "type": "string" + "type": "boolean" } }, { - "name": "trigger", + "name": "withStacked", "required": false, "in": "query", - "description": "Workflow trigger type", + "description": "Include stacked assets in the response. When true, only primary assets from stacks are returned.", "schema": { - "$ref": "#/components/schemas/WorkflowTrigger" + "type": "boolean" } } ], @@ -15959,7 +15385,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/WorkflowResponseDto" + "$ref": "#/components/schemas/TimeBucketsResponseDto" }, "type": "array" } @@ -15979,38 +15405,35 @@ "api_key": [] } ], - "summary": "List all workflows", + "summary": "Get time buckets", "tags": [ - "Workflows" + "Timeline" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" + }, + { + "version": "v1", + "state": "Internal" } ], - "x-immich-permission": "workflow.read" - }, + "x-immich-permission": "asset.read", + "x-immich-state": "Internal" + } + }, + "/trash/empty": { "post": { - "description": "Create a new workflow, the workflow can also be created with empty filters and actions.", - "operationId": "createWorkflow", + "description": "Permanently delete all items in the trash.", + "operationId": "emptyTrash", "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkflowCreateDto" - } - } - }, - "required": true - }, "responses": { - "201": { + "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowResponseDto" + "$ref": "#/components/schemas/TrashResponseDto" } } }, @@ -16028,33 +15451,39 @@ "api_key": [] } ], - "summary": "Create a workflow", + "summary": "Empty trash", "tags": [ - "Workflows" + "Trash" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "workflow.create" + "x-immich-permission": "asset.delete", + "x-immich-state": "Stable" } }, - "/workflows/triggers": { - "get": { - "description": "Retrieve a list of all available workflow triggers.", - "operationId": "getWorkflowTriggers", + "/trash/restore": { + "post": { + "description": "Restore all items in the trash.", + "operationId": "restoreTrash", "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/WorkflowTriggerResponseDto" - }, - "type": "array" + "$ref": "#/components/schemas/TrashResponseDto" } } }, @@ -16072,36 +15501,52 @@ "api_key": [] } ], - "summary": "List all workflow triggers", + "summary": "Restore trash", "tags": [ - "Workflows" + "Trash" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } - ] + ], + "x-immich-permission": "asset.delete", + "x-immich-state": "Stable" } }, - "/workflows/{id}": { - "delete": { - "description": "Delete a workflow by its ID.", - "operationId": "deleteWorkflow", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "/trash/restore/assets": { + "post": { + "description": "Restore specific assets from the trash.", + "operationId": "restoreAssets", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } } - } - ], + }, + "required": true + }, "responses": { - "204": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TrashResponseDto" + } + } + }, "description": "" } }, @@ -16116,39 +15561,92 @@ "api_key": [] } ], - "summary": "Delete a workflow", + "summary": "Restore assets", "tags": [ - "Workflows" + "Trash" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "workflow.delete" - }, + "x-immich-permission": "asset.delete", + "x-immich-state": "Stable" + } + }, + "/users": { "get": { - "description": "Retrieve information about a specific workflow by its ID.", - "operationId": "getWorkflow", - "parameters": [ + "description": "Retrieve a list of all users on the server.", + "operationId": "searchUsers", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/UserResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get all users", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], + "x-immich-permission": "user.read", + "x-immich-state": "Stable" + } + }, + "/users/me": { + "get": { + "description": "Retrieve information about the user making the API request.", + "operationId": "getMyUser", + "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowResponseDto" + "$ref": "#/components/schemas/UserAdminResponseDto" } } }, @@ -16166,39 +15664,37 @@ "api_key": [] } ], - "summary": "Retrieve a workflow", + "summary": "Get current user", "tags": [ - "Workflows" + "Users" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "workflow.read" + "x-immich-permission": "user.read", + "x-immich-state": "Stable" }, "put": { "deprecated": true, - "description": "Update the information of a specific workflow by its ID. This endpoint can be used to update the workflow name, description, trigger type, filters and actions order, etc.", - "operationId": "updateWorkflow", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "description": "Update the current user making the API request.", + "operationId": "updateMyUser", + "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowUpdateDto" + "$ref": "#/components/schemas/UserUpdateMeDto" } } }, @@ -16209,7 +15705,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WorkflowResponseDto" + "$ref": "#/components/schemas/UserAdminResponseDto" } } }, @@ -16227,73 +15723,70 @@ "api_key": [] } ], - "summary": "Update a workflow", + "summary": "Update current user", "tags": [ - "Workflows", + "Users", "Deprecated" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + }, { "version": "v3", "state": "Deprecated", - "replacementId": "updateWorkflow" + "replacementId": "updateMyUser" } ], - "x-immich-permission": "workflow.update", + "x-immich-permission": "user.update", "x-immich-state": "Deprecated" } }, - "/workflows/{id}/logs": { + "/users/me/calendar-heatmap": { "get": { - "description": "Retrieve logs of a workflows runs by ID", - "operationId": "getWorkflowLogs", + "description": "Retrieve activity counts for a specified period, in a calendar heatmap format.", + "operationId": "getMyCalendarHeatmap", "parameters": [ { - "name": "before", + "name": "from", "required": false, "in": "query", - "description": "Filter by runs before a date/time", - "schema": { - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "example": "2024-01-01T00:00:00.000Z", - "type": "string" - } - }, - { - "name": "id", - "required": true, - "in": "path", + "description": "Start date in UTC", "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "format": "date", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", + "example": "2024-01-01", "type": "string" } }, { - "name": "limit", + "name": "to", "required": false, "in": "query", - "description": "Maximum number of logs", + "description": "End date in UTC", "schema": { - "maximum": 9007199254740991, - "exclusiveMinimum": true, - "default": 50, - "type": "integer", - "minimum": 0 + "format": "date", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$", + "example": "2024-01-01", + "type": "string" } }, { - "name": "result", + "name": "type", "required": false, "in": "query", - "description": "Filter by run result", "schema": { - "$ref": "#/components/schemas/WorkflowResult" + "default": "Upload", + "$ref": "#/components/schemas/CalendarHeatmapType" } } ], @@ -16302,10 +15795,7 @@ "content": { "application/json": { "schema": { - "items": { - "$ref": "#/components/schemas/WorkflowLogEntryDto" - }, - "type": "array" + "$ref": "#/components/schemas/CalendarHeatmapResponseDto" } } }, @@ -16323,44 +15813,31 @@ "api_key": [] } ], - "summary": "Retrieve workflow logs", + "summary": "Retrieve calendar heatmap activity", "tags": [ - "Workflows" + "Users" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v3", "state": "Added" + }, + { + "version": "v3", + "state": "Stable" } ], - "x-immich-permission": "workflow.logs" + "x-immich-permission": "user.read", + "x-immich-state": "Stable" } }, - "/workflows/{id}/share": { - "get": { - "description": "Retrieve a workflow details without ids, default values, etc.", - "operationId": "getWorkflowForShare", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - } - ], + "/users/me/license": { + "delete": { + "description": "Delete the registered product key for the current user.", + "operationId": "deleteUserLicense", + "parameters": [], "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorkflowShareResponseDto" - } - } - }, + "204": { "description": "" } }, @@ -16375,22 +15852,1238 @@ "api_key": [] } ], - "summary": "Retrieve a workflow", + "summary": "Delete user product key", "tags": [ - "Workflows" + "Users" ], "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" } ], - "x-immich-permission": "workflow.read" - } - } - }, - "info": { - "title": "Immich", + "x-immich-permission": "userLicense.delete", + "x-immich-state": "Stable" + }, + "get": { + "description": "Retrieve information about whether the current user has a registered product key.", + "operationId": "getUserLicense", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LicenseResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve user product key", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "userLicense.read", + "x-immich-state": "Stable" + }, + "put": { + "description": "Register a product key for the current user.", + "operationId": "setUserLicense", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LicenseKeyDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LicenseResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Set user product key", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "userLicense.update", + "x-immich-state": "Stable" + } + }, + "/users/me/onboarding": { + "delete": { + "description": "Delete the onboarding status of the current user.", + "operationId": "deleteUserOnboarding", + "parameters": [], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Delete user onboarding", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "userOnboarding.delete", + "x-immich-state": "Stable" + }, + "get": { + "description": "Retrieve the onboarding status of the current user.", + "operationId": "getUserOnboarding", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve user onboarding", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "userOnboarding.read", + "x-immich-state": "Stable" + }, + "put": { + "description": "Update the onboarding status of the current user.", + "operationId": "setUserOnboarding", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OnboardingResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Update user onboarding", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "userOnboarding.update", + "x-immich-state": "Stable" + } + }, + "/users/me/preferences": { + "get": { + "description": "Retrieve the preferences for the current user.", + "operationId": "getMyPreferences", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPreferencesResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Get my preferences", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "userPreference.read", + "x-immich-state": "Stable" + }, + "put": { + "deprecated": true, + "description": "Update the preferences of the current user.", + "operationId": "updateMyPreferences", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPreferencesUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPreferencesResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Update my preferences", + "tags": [ + "Users", + "Deprecated" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v3", + "state": "Deprecated", + "replacementId": "updateMyPreferences" + } + ], + "x-immich-permission": "userPreference.update", + "x-immich-state": "Deprecated" + } + }, + "/users/profile-image": { + "delete": { + "description": "Delete the profile image of the current user.", + "operationId": "deleteProfileImage", + "parameters": [], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Delete user profile image", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "userProfileImage.delete", + "x-immich-state": "Stable" + }, + "post": { + "description": "Upload and set a new profile image for the current user.", + "operationId": "createProfileImage", + "parameters": [], + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/CreateProfileImageDto" + } + } + }, + "description": "A new avatar for the user", + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProfileImageResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Create user profile image", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "userProfileImage.update", + "x-immich-state": "Stable" + } + }, + "/users/{id}": { + "get": { + "description": "Retrieve a specific user by their ID.", + "operationId": "getUser", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve a user", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "user.read", + "x-immich-state": "Stable" + } + }, + "/users/{id}/profile-image": { + "get": { + "description": "Retrieve the profile image file for a user.", + "operationId": "getProfileImage", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/octet-stream": { + "schema": { + "format": "binary", + "type": "string" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve user profile image", + "tags": [ + "Users" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "userProfileImage.read", + "x-immich-state": "Stable" + } + }, + "/view/folder": { + "get": { + "description": "Retrieve assets that are children of a specific folder.", + "operationId": "getAssetsByOriginalPath", + "parameters": [ + { + "name": "path", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve assets by original path", + "tags": [ + "Views" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "folder.read", + "x-immich-state": "Stable" + } + }, + "/view/folder/unique-paths": { + "get": { + "description": "Retrieve a list of unique folder paths from asset original paths.", + "operationId": "getUniqueOriginalPaths", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve unique paths", + "tags": [ + "Views" + ], + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Beta" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-permission": "folder.read", + "x-immich-state": "Stable" + } + }, + "/workflows": { + "get": { + "description": "Retrieve a list of workflows available to the authenticated user.", + "operationId": "searchWorkflows", + "parameters": [ + { + "name": "description", + "required": false, + "in": "query", + "description": "Workflow description", + "schema": { + "type": "string" + } + }, + { + "name": "enabled", + "required": false, + "in": "query", + "description": "Workflow enabled", + "schema": { + "type": "boolean" + } + }, + { + "name": "id", + "required": false, + "in": "query", + "description": "Workflow ID", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "logging", + "required": false, + "in": "query", + "description": "Workflow logs run results", + "schema": { + "type": "boolean" + } + }, + { + "name": "name", + "required": false, + "in": "query", + "description": "Workflow name", + "schema": { + "type": "string" + } + }, + { + "name": "trigger", + "required": false, + "in": "query", + "description": "Workflow trigger type", + "schema": { + "$ref": "#/components/schemas/WorkflowTrigger" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/WorkflowResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "List all workflows", + "tags": [ + "Workflows" + ], + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ], + "x-immich-permission": "workflow.read" + }, + "post": { + "description": "Create a new workflow, the workflow can also be created with empty filters and actions.", + "operationId": "createWorkflow", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowCreateDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Create a workflow", + "tags": [ + "Workflows" + ], + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ], + "x-immich-permission": "workflow.create" + } + }, + "/workflows/triggers": { + "get": { + "description": "Retrieve a list of all available workflow triggers.", + "operationId": "getWorkflowTriggers", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/WorkflowTriggerResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "List all workflow triggers", + "tags": [ + "Workflows" + ], + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ] + } + }, + "/workflows/{id}": { + "delete": { + "description": "Delete a workflow by its ID.", + "operationId": "deleteWorkflow", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Delete a workflow", + "tags": [ + "Workflows" + ], + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ], + "x-immich-permission": "workflow.delete" + }, + "get": { + "description": "Retrieve information about a specific workflow by its ID.", + "operationId": "getWorkflow", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve a workflow", + "tags": [ + "Workflows" + ], + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ], + "x-immich-permission": "workflow.read" + }, + "put": { + "deprecated": true, + "description": "Update the information of a specific workflow by its ID. This endpoint can be used to update the workflow name, description, trigger type, filters and actions order, etc.", + "operationId": "updateWorkflow", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Update a workflow", + "tags": [ + "Workflows", + "Deprecated" + ], + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + }, + { + "version": "v3", + "state": "Deprecated", + "replacementId": "updateWorkflow" + } + ], + "x-immich-permission": "workflow.update", + "x-immich-state": "Deprecated" + } + }, + "/workflows/{id}/logs": { + "get": { + "description": "Retrieve logs of a workflows runs by ID", + "operationId": "getWorkflowLogs", + "parameters": [ + { + "name": "before", + "required": false, + "in": "query", + "description": "Filter by runs before a date/time", + "schema": { + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "example": "2024-01-01T00:00:00.000Z", + "type": "string" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + { + "name": "limit", + "required": false, + "in": "query", + "description": "Maximum number of logs", + "schema": { + "maximum": 9007199254740991, + "exclusiveMinimum": true, + "default": 50, + "type": "integer", + "minimum": 0 + } + }, + { + "name": "result", + "required": false, + "in": "query", + "description": "Filter by run result", + "schema": { + "$ref": "#/components/schemas/WorkflowResult" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/WorkflowLogEntryDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve workflow logs", + "tags": [ + "Workflows" + ], + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ], + "x-immich-permission": "workflow.logs" + } + }, + "/workflows/{id}/share": { + "get": { + "description": "Retrieve a workflow details without ids, default values, etc.", + "operationId": "getWorkflowForShare", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorkflowShareResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "summary": "Retrieve a workflow", + "tags": [ + "Workflows" + ], + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ], + "x-immich-permission": "workflow.read" + } + } + }, + "info": { + "title": "Immich", "description": "Immich API", "version": "3.1.0", "contact": {} @@ -16420,6 +17113,22 @@ "name": "Authentication (admin)", "description": "Administrative endpoints related to authentication." }, + { + "name": "Cluster groups", + "description": "A cluster group is a set of users whose faces are clustered together, so that a person can be shared between them." + }, + { + "name": "Config (user)", + "description": "The system configuration properties that are visible to logged in users." + }, + { + "name": "Config (admin)", + "description": "Endpoints to view and modify the full system configuration." + }, + { + "name": "Config (public)", + "description": "The system configuration properties that are visible to everyone." + }, { "name": "Database Backups (admin)", "description": "Manage backups of the Immich database." @@ -16598,3002 +17307,2928 @@ } }, "required": [ - "albumId", - "type" + "albumId", + "type" + ], + "type": "object" + }, + "ActivityResponseDto": { + "properties": { + "assetId": { + "description": "Asset ID (if activity is for an asset)", + "format": "uuid", + "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "comment": { + "description": "Comment text (for comment activities)", + "nullable": true, + "type": "string" + }, + "createdAt": { + "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "id": { + "description": "Activity ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/ReactionType" + }, + "user": { + "$ref": "#/components/schemas/UserResponseDto" + } + }, + "required": [ + "assetId", + "createdAt", + "id", + "type", + "user" + ], + "type": "object" + }, + "ActivityStatisticsResponseDto": { + "properties": { + "comments": { + "description": "Number of comments", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "likes": { + "description": "Number of likes", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "comments", + "likes" + ], + "type": "object" + }, + "AddUsersDto": { + "properties": { + "albumUsers": { + "description": "Album users to add", + "items": { + "$ref": "#/components/schemas/AlbumUserAddDto" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "albumUsers" + ], + "type": "object" + }, + "AdminConfigBackupsDto": { + "properties": { + "database": { + "$ref": "#/components/schemas/AdminConfigDatabaseBackupDto" + } + }, + "required": [ + "database" + ], + "type": "object" + }, + "AdminConfigClipDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + }, + "modelName": { + "description": "Name of the model to use", + "type": "string" + } + }, + "required": [ + "enabled", + "modelName" + ], + "type": "object" + }, + "AdminConfigDatabaseBackupDto": { + "properties": { + "cronExpression": { + "description": "Cron expression", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "keepLastAmount": { + "description": "Keep last amount", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "cronExpression", + "enabled", + "keepLastAmount" + ], + "type": "object" + }, + "AdminConfigDto": { + "description": "Configuration properties that are visible to the admin", + "properties": { + "backup": { + "$ref": "#/components/schemas/AdminConfigBackupsDto" + }, + "ffmpeg": { + "$ref": "#/components/schemas/AdminConfigFFmpegDto" + }, + "image": { + "$ref": "#/components/schemas/AdminConfigImageDto" + }, + "integrityChecks": { + "$ref": "#/components/schemas/AdminConfigIntegrityChecksDto" + }, + "job": { + "$ref": "#/components/schemas/AdminConfigJobDto" + }, + "library": { + "$ref": "#/components/schemas/AdminConfigLibraryDto" + }, + "logging": { + "$ref": "#/components/schemas/AdminConfigLoggingDto" + }, + "machineLearning": { + "$ref": "#/components/schemas/AdminConfigMachineLearningDto" + }, + "map": { + "$ref": "#/components/schemas/AdminConfigMapDto" + }, + "metadata": { + "$ref": "#/components/schemas/AdminConfigMetadataDto" + }, + "newVersionCheck": { + "$ref": "#/components/schemas/AdminConfigNewVersionCheckDto" + }, + "nightlyTasks": { + "$ref": "#/components/schemas/AdminConfigNightlyTasksDto" + }, + "notifications": { + "$ref": "#/components/schemas/AdminConfigNotificationsDto" + }, + "oauth": { + "$ref": "#/components/schemas/AdminConfigOAuthDto" + }, + "passwordLogin": { + "$ref": "#/components/schemas/AdminConfigPasswordLoginDto" + }, + "reverseGeocoding": { + "$ref": "#/components/schemas/AdminConfigReverseGeocodingDto" + }, + "server": { + "$ref": "#/components/schemas/AdminConfigServerDto" + }, + "storageTemplate": { + "$ref": "#/components/schemas/AdminConfigStorageTemplateDto" + }, + "templates": { + "$ref": "#/components/schemas/AdminConfigTemplatesDto" + }, + "theme": { + "$ref": "#/components/schemas/AdminConfigThemeDto" + }, + "trash": { + "$ref": "#/components/schemas/AdminConfigTrashDto" + }, + "user": { + "$ref": "#/components/schemas/AdminConfigUserDto" + } + }, + "required": [ + "backup", + "ffmpeg", + "image", + "integrityChecks", + "job", + "library", + "logging", + "machineLearning", + "map", + "metadata", + "newVersionCheck", + "nightlyTasks", + "notifications", + "oauth", + "passwordLogin", + "reverseGeocoding", + "server", + "storageTemplate", + "templates", + "theme", + "trash", + "user" + ], + "type": "object" + }, + "AdminConfigDuplicateDetectionDto": { + "properties": { + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + }, + "maxDistance": { + "description": "Maximum distance threshold for duplicate detection", + "format": "double", + "maximum": 0.1, + "minimum": 0.001, + "type": "number" + } + }, + "required": [ + "enabled", + "maxDistance" ], "type": "object" }, - "ActivityResponseDto": { + "AdminConfigFFmpegDto": { "properties": { - "assetId": { - "description": "Asset ID (if activity is for an asset)", - "format": "uuid", - "nullable": true, - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "accel": { + "$ref": "#/components/schemas/TranscodeHWAccel" }, - "comment": { - "description": "Comment text (for comment activities)", - "nullable": true, + "accelDecode": { + "description": "Accelerated decode", + "type": "boolean" + }, + "acceptedAudioCodecs": { + "description": "Accepted audio codecs", + "items": { + "$ref": "#/components/schemas/AudioCodec" + }, + "type": "array" + }, + "acceptedContainers": { + "description": "Accepted containers", + "items": { + "$ref": "#/components/schemas/VideoContainer" + }, + "type": "array" + }, + "acceptedVideoCodecs": { + "description": "Accepted video codecs", + "items": { + "$ref": "#/components/schemas/VideoCodec" + }, + "type": "array" + }, + "bframes": { + "description": "B-frames", + "maximum": 16, + "minimum": -1, + "type": "integer" + }, + "cqMode": { + "$ref": "#/components/schemas/CQMode" + }, + "crf": { + "description": "CRF", + "maximum": 51, + "minimum": 0, + "type": "integer" + }, + "gopSize": { + "description": "GOP size", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "maxBitrate": { + "description": "Max bitrate", "type": "string" }, - "createdAt": { - "description": "Creation date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "preferredHwDevice": { + "description": "Preferred hardware device", "type": "string" }, - "id": { - "description": "Activity ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "preset": { + "description": "Preset", "type": "string" }, - "type": { - "$ref": "#/components/schemas/ReactionType" + "realtime": { + "$ref": "#/components/schemas/AdminConfigFFmpegRealtimeDto" }, - "user": { - "$ref": "#/components/schemas/UserResponseDto" - } - }, - "required": [ - "assetId", - "createdAt", - "id", - "type", - "user" - ], - "type": "object" - }, - "ActivityStatisticsResponseDto": { - "properties": { - "comments": { - "description": "Number of comments", - "maximum": 9007199254740991, + "refs": { + "description": "References", + "maximum": 6, "minimum": 0, "type": "integer" }, - "likes": { - "description": "Number of likes", + "targetAudioCodec": { + "$ref": "#/components/schemas/AudioCodec" + }, + "targetResolution": { + "description": "Target resolution", + "type": "string" + }, + "targetVideoCodec": { + "$ref": "#/components/schemas/VideoCodec" + }, + "temporalAQ": { + "description": "Temporal AQ", + "type": "boolean" + }, + "threads": { + "description": "Threads", "maximum": 9007199254740991, "minimum": 0, "type": "integer" + }, + "tonemap": { + "$ref": "#/components/schemas/ToneMapping" + }, + "transcode": { + "$ref": "#/components/schemas/TranscodePolicy" + }, + "twoPass": { + "description": "Two pass", + "type": "boolean" } }, "required": [ - "comments", - "likes" + "accel", + "accelDecode", + "acceptedAudioCodecs", + "acceptedContainers", + "acceptedVideoCodecs", + "bframes", + "cqMode", + "crf", + "gopSize", + "maxBitrate", + "preferredHwDevice", + "preset", + "realtime", + "refs", + "targetAudioCodec", + "targetResolution", + "targetVideoCodec", + "temporalAQ", + "threads", + "tonemap", + "transcode", + "twoPass" ], "type": "object" }, - "AddUsersDto": { + "AdminConfigFFmpegRealtimeDto": { "properties": { - "albumUsers": { - "description": "Album users to add", + "enabled": { + "description": "Enable real-time HLS transcoding (alpha)", + "type": "boolean" + }, + "resolutions": { + "description": "Resolutions to use for real-time HLS transcoding", "items": { - "$ref": "#/components/schemas/AlbumUserAddDto" + "$ref": "#/components/schemas/HlsVideoResolution" + }, + "type": "array" + }, + "videoCodecs": { + "description": "Video codecs to use for real-time HLS transcoding", + "items": { + "$ref": "#/components/schemas/VideoCodec" }, - "minItems": 1, "type": "array" } }, "required": [ - "albumUsers" + "enabled", + "resolutions", + "videoCodecs" ], "type": "object" }, - "AdminOnboardingUpdateDto": { + "AdminConfigFacesDto": { "properties": { - "isOnboarded": { - "description": "Is admin onboarded", + "import": { + "description": "Import", "type": "boolean" } }, "required": [ - "isOnboarded" + "import" ], "type": "object" }, - "AlbumResponseDto": { + "AdminConfigFacialRecognitionDto": { "properties": { - "albumName": { - "description": "Album name", - "type": "string" - }, - "albumThumbnailAssetId": { - "description": "Thumbnail asset ID", - "format": "uuid", - "nullable": true, - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" }, - "albumUsers": { - "description": "First entry is always the album owner. Second entry is the auth user, if it differs from the owner. The rest are ordered alphabetically.", - "items": { - "$ref": "#/components/schemas/AlbumUserResponseDto" - }, - "minItems": 1, - "type": "array" + "maxDistance": { + "description": "Maximum distance threshold for face recognition", + "format": "double", + "maximum": 2, + "minimum": 0.1, + "type": "number" }, - "assetCount": { - "description": "Number of assets", + "minFaces": { + "description": "Minimum number of faces required for recognition", "maximum": 9007199254740991, - "minimum": 0, + "minimum": 1, "type": "integer" }, - "contributorCounts": { - "items": { - "$ref": "#/components/schemas/ContributorCountResponseDto" - }, - "type": "array" - }, - "createdAt": { - "description": "Creation date", - "format": "date-time", - "type": "string" - }, - "description": { - "description": "Album description", - "type": "string", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v3", - "state": "Updated", - "description": "An empty string is returned instead of null for backwards compatibility; null will be returned in v4." - } - ] - }, - "endDate": { - "description": "End date (latest asset)", - "format": "date-time", - "type": "string" - }, - "hasSharedLink": { - "description": "Has shared link", - "type": "boolean" + "minScore": { + "description": "Minimum confidence score for face detection", + "format": "double", + "maximum": 1, + "minimum": 0.1, + "type": "number" }, - "id": { - "description": "Album ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "modelName": { + "description": "Name of the model to use", "type": "string" - }, - "isActivityEnabled": { - "description": "Activity feed enabled", + } + }, + "required": [ + "enabled", + "maxDistance", + "minFaces", + "minScore", + "modelName" + ], + "type": "object" + }, + "AdminConfigGeneratedFullsizeImageDto": { + "properties": { + "enabled": { + "description": "Enabled", "type": "boolean" }, - "lastModifiedAssetTimestamp": { - "description": "Last modified asset timestamp", - "format": "date-time", - "type": "string" - }, - "order": { - "$ref": "#/components/schemas/AssetOrder" + "format": { + "$ref": "#/components/schemas/ImageFormat" }, - "shared": { - "description": "Is shared album", + "progressive": { + "description": "Progressive", "type": "boolean" }, - "startDate": { - "description": "Start date (earliest asset)", - "format": "date-time", - "type": "string" - }, - "updatedAt": { - "description": "Last update date", - "format": "date-time", - "type": "string" + "quality": { + "description": "Quality", + "maximum": 100, + "minimum": 1, + "type": "integer" } }, "required": [ - "albumName", - "albumThumbnailAssetId", - "albumUsers", - "assetCount", - "createdAt", - "description", - "hasSharedLink", - "id", - "isActivityEnabled", - "shared", - "updatedAt" + "enabled", + "format", + "quality" ], "type": "object" }, - "AlbumStatisticsResponseDto": { + "AdminConfigGeneratedImageDto": { "properties": { - "notShared": { - "description": "Number of non-shared albums", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "format": { + "$ref": "#/components/schemas/ImageFormat" }, - "owned": { - "description": "Number of owned albums", - "maximum": 9007199254740991, - "minimum": 0, + "progressive": { + "description": "Progressive", + "type": "boolean" + }, + "quality": { + "description": "Quality", + "maximum": 100, + "minimum": 1, "type": "integer" }, - "shared": { - "description": "Number of shared albums", + "size": { + "description": "Size", "maximum": 9007199254740991, - "minimum": 0, + "minimum": 1, "type": "integer" } }, "required": [ - "notShared", - "owned", - "shared" + "format", + "quality", + "size" ], "type": "object" }, - "AlbumUserAddDto": { + "AdminConfigImageDto": { "properties": { - "role": { - "$ref": "#/components/schemas/AlbumUserRole", - "default": "editor", - "description": "Album user role" + "colorspace": { + "$ref": "#/components/schemas/Colorspace" }, - "userId": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "extractEmbedded": { + "description": "Extract embedded", + "type": "boolean" + }, + "fullsize": { + "$ref": "#/components/schemas/AdminConfigGeneratedFullsizeImageDto" + }, + "preview": { + "$ref": "#/components/schemas/AdminConfigGeneratedImageDto" + }, + "thumbnail": { + "$ref": "#/components/schemas/AdminConfigGeneratedImageDto" } }, "required": [ - "userId" + "colorspace", + "extractEmbedded", + "fullsize", + "preview", + "thumbnail" ], "type": "object" }, - "AlbumUserCreateDto": { + "AdminConfigIntegrityChecksDto": { + "description": "Integrity checks config", "properties": { - "role": { - "$ref": "#/components/schemas/AlbumUserRole" + "checksumFiles": { + "$ref": "#/components/schemas/AdminConfigIntegrityChecksumJobDto" }, - "userId": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "missingFiles": { + "$ref": "#/components/schemas/AdminConfigIntegrityJobDto" + }, + "untrackedFiles": { + "$ref": "#/components/schemas/AdminConfigIntegrityJobDto" } }, "required": [ - "role", - "userId" + "checksumFiles", + "missingFiles", + "untrackedFiles" ], "type": "object" }, - "AlbumUserResponseDto": { + "AdminConfigIntegrityChecksumJobDto": { + "description": "Integrity checksum job config", "properties": { - "role": { - "$ref": "#/components/schemas/AlbumUserRole" + "cronExpression": { + "description": "Cron expression for when the integrity check should run", + "type": "string" }, - "user": { - "$ref": "#/components/schemas/UserResponseDto" + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "percentageLimit": { + "description": "Percentage limit of the integrity checksum job", + "format": "double", + "maximum": 1, + "minimum": 0, + "type": "number" + }, + "timeLimit": { + "description": "How long the integrity checksum job may run for", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" } }, "required": [ - "role", - "user" + "cronExpression", + "enabled", + "percentageLimit", + "timeLimit" ], "type": "object" }, - "AlbumUserRole": { - "description": "Album user role", - "enum": [ - "editor", - "owner", - "viewer" + "AdminConfigIntegrityJobDto": { + "description": "Integrity job config", + "properties": { + "cronExpression": { + "description": "Cron expression for when the integrity check should run", + "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "cronExpression", + "enabled" ], - "type": "string" + "type": "object" }, - "AlbumsAddAssetsDto": { + "AdminConfigJobDto": { "properties": { - "albumIds": { - "description": "Album IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "backgroundTask": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" }, - "assetIds": { - "description": "Asset IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "editor": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "faceDetection": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "integrityCheck": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "library": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "metadataExtraction": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "migration": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "notifications": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "ocr": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "search": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "sidecar": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "smartSearch": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "thumbnailGeneration": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "videoConversion": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" + }, + "workflow": { + "$ref": "#/components/schemas/AdminConfigJobSettingsDto" } }, "required": [ - "albumIds", - "assetIds" + "backgroundTask", + "editor", + "faceDetection", + "integrityCheck", + "library", + "metadataExtraction", + "migration", + "notifications", + "ocr", + "search", + "sidecar", + "smartSearch", + "thumbnailGeneration", + "videoConversion", + "workflow" ], "type": "object" }, - "AlbumsAddAssetsResponseDto": { + "AdminConfigJobSettingsDto": { "properties": { - "error": { - "$ref": "#/components/schemas/BulkIdErrorReason" - }, - "success": { - "description": "Operation success", - "type": "boolean" + "concurrency": { + "description": "Concurrency", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" } }, "required": [ - "success" + "concurrency" ], "type": "object" }, - "AlbumsResponse": { + "AdminConfigLibraryDto": { "properties": { - "defaultAssetOrder": { - "$ref": "#/components/schemas/AssetOrder" + "scan": { + "$ref": "#/components/schemas/AdminConfigLibraryScanDto" + }, + "watch": { + "$ref": "#/components/schemas/AdminConfigLibraryWatchDto" } }, "required": [ - "defaultAssetOrder" + "scan", + "watch" ], "type": "object" }, - "AlbumsUpdate": { - "description": "Album preferences", - "properties": { - "defaultAssetOrder": { - "$ref": "#/components/schemas/AssetOrder" - } - }, - "type": "object" - }, - "ApiKeyCreateDto": { + "AdminConfigLibraryScanDto": { "properties": { - "name": { - "description": "API key name", + "cronExpression": { + "description": "Cron expression", "type": "string" }, - "permissions": { - "description": "List of permissions", - "items": { - "$ref": "#/components/schemas/Permission" - }, - "minItems": 1, - "type": "array" + "enabled": { + "description": "Enabled", + "type": "boolean" } }, "required": [ - "permissions" + "cronExpression", + "enabled" ], "type": "object" }, - "ApiKeyCreateResponseDto": { + "AdminConfigLibraryWatchDto": { "properties": { - "apiKey": { - "$ref": "#/components/schemas/ApiKeyResponseDto" - }, - "secret": { - "description": "API key secret (only shown once)", - "type": "string" + "enabled": { + "description": "Enabled", + "type": "boolean" } }, "required": [ - "apiKey", - "secret" + "enabled" ], "type": "object" }, - "ApiKeyResponseDto": { + "AdminConfigLoggingDto": { "properties": { - "createdAt": { - "description": "Creation date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "id": { - "description": "API key ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "name": { - "description": "API key name", - "type": "string" - }, - "permissions": { - "description": "List of permissions", - "items": { - "$ref": "#/components/schemas/Permission" - }, - "type": "array" + "enabled": { + "description": "Enabled", + "type": "boolean" }, - "updatedAt": { - "description": "Last update date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "level": { + "$ref": "#/components/schemas/LogLevel" } }, "required": [ - "createdAt", - "id", - "name", - "permissions", - "updatedAt" + "enabled", + "level" ], "type": "object" }, - "ApiKeyUpdateDto": { - "properties": { - "name": { - "description": "API key name", - "type": "string" - }, - "permissions": { - "description": "List of permissions", - "items": { - "$ref": "#/components/schemas/Permission" - }, - "minItems": 1, - "type": "array" - } - }, - "type": "object" - }, - "AssetBulkDeleteDto": { + "AdminConfigMachineLearningAvailabilityChecksDto": { "properties": { - "force": { - "description": "Force delete even if in use", + "enabled": { + "description": "Enabled", "type": "boolean" }, - "ids": { - "description": "IDs to process", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "interval": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "timeout": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" } }, "required": [ - "ids" + "enabled", + "interval", + "timeout" ], "type": "object" }, - "AssetBulkUpdateDto": { + "AdminConfigMachineLearningDto": { "properties": { - "dateTimeOriginal": { - "description": "Original date and time", - "type": "string" - }, - "dateTimeRelative": { - "description": "Relative time offset in minutes", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "description": { - "description": "Asset description", - "type": "string" + "availabilityChecks": { + "$ref": "#/components/schemas/AdminConfigMachineLearningAvailabilityChecksDto" }, - "duplicateId": { - "description": "Duplicate ID", - "nullable": true, - "type": "string" + "clip": { + "$ref": "#/components/schemas/AdminConfigClipDto" }, - "ids": { - "description": "Asset IDs to update", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "duplicateDetection": { + "$ref": "#/components/schemas/AdminConfigDuplicateDetectionDto" }, - "isFavorite": { - "description": "Mark as favorite", + "enabled": { + "description": "Enabled", "type": "boolean" }, - "latitude": { - "description": "Latitude coordinate", - "maximum": 90, - "minimum": -90, - "type": "number" - }, - "longitude": { - "description": "Longitude coordinate", - "maximum": 180, - "minimum": -180, - "type": "number" - }, - "rating": { - "description": "Rating in range [1-5] (starred), -1 (rejected), or null (unrated)", - "maximum": 5, - "minimum": -1, - "nullable": true, - "type": "integer", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - }, - { - "version": "v3", - "state": "Updated", - "description": "Using 0 as a rating is no longer valid." - } - ], - "x-immich-state": "Stable" + "facialRecognition": { + "$ref": "#/components/schemas/AdminConfigFacialRecognitionDto" }, - "timeZone": { - "description": "Time zone (IANA timezone)", - "type": "string" + "ocr": { + "$ref": "#/components/schemas/AdminConfigOcrDto" }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" - } - }, - "required": [ - "ids" - ], - "type": "object" - }, - "AssetBulkUploadCheckDto": { - "properties": { - "assets": { - "description": "Assets to check", + "urls": { + "description": "ML service URLs", "items": { - "$ref": "#/components/schemas/AssetBulkUploadCheckItem" + "type": "string" }, + "minItems": 1, "type": "array" } }, "required": [ - "assets" + "availabilityChecks", + "clip", + "duplicateDetection", + "enabled", + "facialRecognition", + "ocr", + "urls" ], "type": "object" }, - "AssetBulkUploadCheckItem": { + "AdminConfigMapDto": { "properties": { - "checksum": { - "description": "Base64 or hex encoded SHA1 hash", + "darkStyle": { + "description": "Dark map style URL", + "format": "uri", "type": "string" }, - "id": { - "description": "Client-side identifier echoed in the response to match results to inputs (e.g. filename)", + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "lightStyle": { + "description": "Light map style URL", + "format": "uri", "type": "string" } }, "required": [ - "checksum", - "id" + "darkStyle", + "enabled", + "lightStyle" ], "type": "object" }, - "AssetBulkUploadCheckResponseDto": { + "AdminConfigMetadataDto": { "properties": { - "results": { - "description": "Upload check results", - "items": { - "$ref": "#/components/schemas/AssetBulkUploadCheckResult" - }, - "type": "array" + "faces": { + "$ref": "#/components/schemas/AdminConfigFacesDto" } }, "required": [ - "results" + "faces" ], "type": "object" }, - "AssetBulkUploadCheckResult": { + "AdminConfigNewVersionCheckDto": { "properties": { - "action": { - "$ref": "#/components/schemas/AssetUploadAction" - }, - "assetId": { - "description": "Existing asset ID if duplicate", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "id": { - "description": "Client-side identifier echoed from the request to match results to inputs", - "type": "string" + "channel": { + "$ref": "#/components/schemas/ReleaseChannel" }, - "isTrashed": { - "description": "Whether existing asset is trashed", + "enabled": { + "description": "Enabled", "type": "boolean" - }, - "reason": { - "$ref": "#/components/schemas/AssetRejectReason" } }, "required": [ - "action", - "id" + "channel", + "enabled" ], "type": "object" }, - "AssetCopyDto": { + "AdminConfigNightlyTasksDto": { "properties": { - "albums": { - "default": true, - "description": "Copy album associations", + "clusterNewFaces": { + "description": "Cluster new faces", "type": "boolean" }, - "favorite": { - "default": true, - "description": "Copy favorite status", + "databaseCleanup": { + "description": "Database cleanup", "type": "boolean" }, - "sharedLinks": { - "default": true, - "description": "Copy shared links", + "generateMemories": { + "description": "Generate memories", "type": "boolean" }, - "sidecar": { - "default": true, - "description": "Copy sidecar file", + "missingThumbnails": { + "description": "Missing thumbnails", "type": "boolean" }, - "sourceId": { - "description": "Source asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "startTime": { + "description": "Start time (HH:MM)", + "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$", "type": "string" }, - "stack": { - "default": true, - "description": "Copy stack association", + "syncQuotaUsage": { + "description": "Sync quota usage", "type": "boolean" - }, - "targetId": { - "description": "Target asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" } }, "required": [ - "sourceId", - "targetId" + "clusterNewFaces", + "databaseCleanup", + "generateMemories", + "missingThumbnails", + "startTime", + "syncQuotaUsage" ], "type": "object" }, - "AssetEditAction": { - "description": "Type of edit action to perform", - "enum": [ - "crop", - "rotate", - "mirror" + "AdminConfigNotificationsDto": { + "properties": { + "smtp": { + "$ref": "#/components/schemas/AdminConfigSmtpDto" + } + }, + "required": [ + "smtp" ], - "type": "string" + "type": "object" }, - "AssetEditActionItemDto": { + "AdminConfigOAuthDto": { "properties": { - "action": { - "$ref": "#/components/schemas/AssetEditAction" + "allowInsecureRequests": { + "description": "Allow insecure requests", + "type": "boolean" }, - "parameters": { - "anyOf": [ - { - "$ref": "#/components/schemas/CropParameters" - }, - { - "$ref": "#/components/schemas/RotateParameters" - }, - { - "$ref": "#/components/schemas/MirrorParameters" - } - ], - "description": "List of edit actions to apply (crop, rotate, or mirror)" + "autoLaunch": { + "description": "Auto launch", + "type": "boolean" + }, + "autoRegister": { + "description": "Auto register", + "type": "boolean" + }, + "buttonText": { + "description": "Button text", + "type": "string" + }, + "clientId": { + "description": "Client ID", + "type": "string" + }, + "clientSecret": { + "description": "Client secret", + "type": "string" + }, + "defaultStorageQuota": { + "description": "Default storage quota", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "endSessionEndpoint": { + "description": "End session endpoint", + "type": "string" + }, + "issuerUrl": { + "description": "Issuer URL", + "type": "string" + }, + "mobileOverrideEnabled": { + "description": "Mobile override enabled", + "type": "boolean" + }, + "mobileRedirectUri": { + "description": "Mobile redirect URI (set to empty string to disable)", + "type": "string" + }, + "profileSigningAlgorithm": { + "description": "Profile signing algorithm", + "type": "string" + }, + "prompt": { + "description": "OAuth prompt parameter (e.g. select_account, login, consent)", + "type": "string" + }, + "roleClaim": { + "description": "Role claim", + "type": "string" + }, + "scope": { + "description": "Scope", + "type": "string" + }, + "signingAlgorithm": { + "description": "Signing algorithm", + "type": "string" + }, + "storageLabelClaim": { + "description": "Storage label claim", + "type": "string" + }, + "storageQuotaClaim": { + "description": "Storage quota claim", + "type": "string" + }, + "timeout": { + "description": "Timeout", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "tokenEndpointAuthMethod": { + "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" } }, "required": [ - "action", - "parameters" + "allowInsecureRequests", + "autoLaunch", + "autoRegister", + "buttonText", + "clientId", + "clientSecret", + "defaultStorageQuota", + "enabled", + "endSessionEndpoint", + "issuerUrl", + "mobileOverrideEnabled", + "mobileRedirectUri", + "profileSigningAlgorithm", + "prompt", + "roleClaim", + "scope", + "signingAlgorithm", + "storageLabelClaim", + "storageQuotaClaim", + "timeout", + "tokenEndpointAuthMethod" ], "type": "object" }, - "AssetEditActionItemResponseDto": { + "AdminConfigOcrDto": { "properties": { - "action": { - "$ref": "#/components/schemas/AssetEditAction" + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" + }, + "maxResolution": { + "description": "Maximum resolution for OCR processing", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "minDetectionScore": { + "description": "Minimum confidence score for text detection", + "format": "double", + "maximum": 1, + "minimum": 0.1, + "type": "number" + }, + "minRecognitionScore": { + "description": "Minimum confidence score for text recognition", + "format": "double", + "maximum": 1, + "minimum": 0.1, + "type": "number" }, - "id": { - "description": "Asset edit ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "modelName": { + "description": "Name of the model to use", "type": "string" - }, - "parameters": { - "anyOf": [ - { - "$ref": "#/components/schemas/CropParameters" - }, - { - "$ref": "#/components/schemas/RotateParameters" - }, - { - "$ref": "#/components/schemas/MirrorParameters" - } - ], - "description": "List of edit actions to apply (crop, rotate, or mirror)" } }, "required": [ - "action", - "id", - "parameters" + "enabled", + "maxResolution", + "minDetectionScore", + "minRecognitionScore", + "modelName" ], "type": "object" }, - "AssetEditsCreateDto": { + "AdminConfigPasswordLoginDto": { "properties": { - "edits": { - "description": "List of edit actions to apply (crop, rotate, or mirror)", - "items": { - "$ref": "#/components/schemas/AssetEditActionItemDto" - }, - "minItems": 1, - "type": "array" + "enabled": { + "description": "Enabled", + "type": "boolean" } }, "required": [ - "edits" + "enabled" ], "type": "object" }, - "AssetEditsResponseDto": { + "AdminConfigReverseGeocodingDto": { "properties": { - "assetId": { - "description": "Asset ID these edits belong to", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "edits": { - "description": "List of edit actions applied to the asset", - "items": { - "$ref": "#/components/schemas/AssetEditActionItemResponseDto" - }, - "type": "array" + "enabled": { + "description": "Enabled", + "type": "boolean" } }, "required": [ - "assetId", - "edits" + "enabled" ], "type": "object" }, - "AssetFaceCreateDto": { + "AdminConfigServerDto": { "properties": { - "assetId": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "externalDomain": { + "description": "External domain", "type": "string" }, - "height": { - "description": "Face bounding box height", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "imageHeight": { - "description": "Image height in pixels", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "imageWidth": { - "description": "Image width in pixels", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "personId": { - "description": "Person ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "loginPageMessage": { + "description": "Login page message", "type": "string" }, - "width": { - "description": "Face bounding box width", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "x": { - "description": "Face bounding box X coordinate", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "y": { - "description": "Face bounding box Y coordinate", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "publicUsers": { + "description": "Public users", + "type": "boolean" } }, "required": [ - "assetId", - "height", - "imageHeight", - "imageWidth", - "personId", - "width", - "x", - "y" + "externalDomain", + "loginPageMessage", + "publicUsers" ], "type": "object" }, - "AssetFaceDeleteDto": { + "AdminConfigSmtpDto": { "properties": { - "force": { - "description": "Force delete even if person has other faces", + "enabled": { + "description": "Whether SMTP email notifications are enabled", "type": "boolean" + }, + "from": { + "description": "Email address to send from", + "type": "string" + }, + "replyTo": { + "description": "Email address for replies", + "type": "string" + }, + "transport": { + "$ref": "#/components/schemas/AdminConfigSmtpTransportDto" } }, "required": [ - "force" + "enabled", + "from", + "replyTo", + "transport" ], "type": "object" }, - "AssetFaceResponseDto": { - "description": "Asset face with person", + "AdminConfigSmtpTransportDto": { "properties": { - "boundingBoxX1": { - "description": "Bounding box X1 coordinate", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "boundingBoxX2": { - "description": "Bounding box X2 coordinate", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "boundingBoxY1": { - "description": "Bounding box Y1 coordinate", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "host": { + "description": "SMTP server hostname", + "type": "string" }, - "boundingBoxY2": { - "description": "Bounding box Y2 coordinate", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "ignoreCert": { + "description": "Whether to ignore SSL certificate errors", + "type": "boolean" }, - "id": { - "description": "Face ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "password": { + "description": "SMTP password", "type": "string" }, - "imageHeight": { - "description": "Image height in pixels", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "imageWidth": { - "description": "Image width in pixels", - "maximum": 9007199254740991, + "port": { + "description": "SMTP server port", + "maximum": 65535, "minimum": 0, "type": "integer" }, - "person": { - "allOf": [ - { - "$ref": "#/components/schemas/PersonResponseDto" - } - ], - "nullable": true - }, - "sourceType": { - "$ref": "#/components/schemas/SourceType" - } - }, - "required": [ - "boundingBoxX1", - "boundingBoxX2", - "boundingBoxY1", - "boundingBoxY2", - "id", - "imageHeight", - "imageWidth", - "person" - ], - "type": "object" - }, - "AssetFaceUpdateDto": { - "properties": { - "data": { - "description": "Face update items", - "items": { - "$ref": "#/components/schemas/AssetFaceUpdateItem" - }, - "type": "array" - } - }, - "required": [ - "data" - ], - "type": "object" - }, - "AssetFaceUpdateItem": { - "properties": { - "assetId": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "secure": { + "description": "Whether to use secure connection (TLS/SSL)", + "type": "boolean" }, - "personId": { - "description": "Person ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "username": { + "description": "SMTP username", "type": "string" } }, "required": [ - "assetId", - "personId" - ], - "type": "object" - }, - "AssetIdErrorReason": { - "description": "Error reason if failed", - "enum": [ - "duplicate", - "no_permission", - "not_found" + "host", + "ignoreCert", + "password", + "port", + "secure", + "username" ], - "type": "string" + "type": "object" }, - "AssetIdsDto": { + "AdminConfigStorageTemplateDto": { "properties": { - "assetIds": { - "description": "Asset IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "enabled": { + "description": "Enabled", + "type": "boolean" + }, + "hashVerificationEnabled": { + "description": "Hash verification enabled", + "type": "boolean" + }, + "template": { + "description": "Template", + "type": "string" } }, "required": [ - "assetIds" + "enabled", + "hashVerificationEnabled", + "template" ], "type": "object" }, - "AssetIdsResponseDto": { + "AdminConfigTemplateEmailsDto": { "properties": { - "assetId": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "albumInviteTemplate": { + "description": "Album invite template", "type": "string" }, - "error": { - "$ref": "#/components/schemas/AssetIdErrorReason" + "albumUpdateTemplate": { + "description": "Album update template", + "type": "string" }, - "success": { - "description": "Whether operation succeeded", - "type": "boolean" + "welcomeTemplate": { + "description": "Welcome template", + "type": "string" } }, "required": [ - "assetId", - "success" + "albumInviteTemplate", + "albumUpdateTemplate", + "welcomeTemplate" ], "type": "object" }, - "AssetJobName": { - "description": "Job name", - "enum": [ - "refresh-faces", - "refresh-metadata", - "regenerate-thumbnail", - "transcode-video" + "AdminConfigTemplatesDto": { + "properties": { + "email": { + "$ref": "#/components/schemas/AdminConfigTemplateEmailsDto" + } + }, + "required": [ + "email" ], - "type": "string" + "type": "object" }, - "AssetJobsDto": { + "AdminConfigThemeDto": { "properties": { - "assetIds": { - "description": "Asset IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" - }, - "name": { - "$ref": "#/components/schemas/AssetJobName" + "customCss": { + "description": "Custom CSS for theming", + "type": "string" } }, "required": [ - "assetIds", - "name" + "customCss" ], "type": "object" }, - "AssetMediaCreateDto": { + "AdminConfigTrashDto": { "properties": { - "assetData": { - "description": "Asset file data", - "format": "binary", - "type": "string" - }, - "duration": { - "description": "Duration in milliseconds (for videos)", + "days": { + "description": "Days", "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, - "fileCreatedAt": { - "description": "File creation date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "fileModifiedAt": { - "description": "File modification date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "filename": { - "description": "Filename", - "type": "string" - }, - "isFavorite": { - "description": "Mark as favorite", + "enabled": { + "description": "Enabled", "type": "boolean" - }, - "livePhotoVideoId": { - "description": "Live photo video ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "metadata": { - "description": "Asset metadata items", - "items": { - "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" - }, - "type": "array" - }, - "sidecarData": { - "description": "Sidecar file data", - "format": "binary", - "type": "string" - }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" } }, "required": [ - "assetData", - "fileCreatedAt", - "fileModifiedAt" + "days", + "enabled" ], "type": "object" }, - "AssetMediaResponseDto": { + "AdminConfigUserDto": { "properties": { - "id": { - "description": "Asset media ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/AssetMediaStatus" + "deleteDelay": { + "description": "Delete delay", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" } }, "required": [ - "id", - "status" + "deleteDelay" ], "type": "object" }, - "AssetMediaSize": { - "description": "Asset media size", - "enum": [ - "original", - "fullsize", - "preview", - "thumbnail" - ], - "type": "string" - }, - "AssetMediaStatus": { - "description": "Upload status", - "enum": [ - "created", - "duplicate" - ], - "type": "string" - }, - "AssetMetadataBulkDeleteDto": { + "AdminOnboardingUpdateDto": { "properties": { - "items": { - "description": "Metadata items to delete", - "items": { - "$ref": "#/components/schemas/AssetMetadataBulkDeleteItemDto" - }, - "type": "array" + "isOnboarded": { + "description": "Is admin onboarded", + "type": "boolean" } }, "required": [ - "items" + "isOnboarded" ], "type": "object" }, - "AssetMetadataBulkDeleteItemDto": { + "AlbumResponseDto": { "properties": { - "assetId": { - "description": "Asset ID", + "albumName": { + "description": "Album name", + "type": "string" + }, + "albumThumbnailAssetId": { + "description": "Thumbnail asset ID", + "format": "uuid", + "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "albumUsers": { + "description": "First entry is always the album owner. Second entry is the auth user, if it differs from the owner. The rest are ordered alphabetically.", + "items": { + "$ref": "#/components/schemas/AlbumUserResponseDto" + }, + "minItems": 1, + "type": "array" + }, + "assetCount": { + "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "contributorCounts": { + "items": { + "$ref": "#/components/schemas/ContributorCountResponseDto" + }, + "type": "array" + }, + "createdAt": { + "description": "Creation date", + "format": "date-time", + "type": "string" + }, + "description": { + "description": "Album description", + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v3", + "state": "Updated", + "description": "An empty string is returned instead of null for backwards compatibility; null will be returned in v4." + } + ] + }, + "endDate": { + "description": "End date (latest asset)", + "format": "date-time", + "type": "string" + }, + "hasSharedLink": { + "description": "Has shared link", + "type": "boolean" + }, + "id": { + "description": "Album ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "key": { - "description": "Metadata key", - "type": "string" - } - }, - "required": [ - "assetId", - "key" - ], - "type": "object" - }, - "AssetMetadataBulkResponseDto": { - "properties": { - "assetId": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "isActivityEnabled": { + "description": "Activity feed enabled", + "type": "boolean" + }, + "lastModifiedAssetTimestamp": { + "description": "Last modified asset timestamp", + "format": "date-time", "type": "string" }, - "key": { - "description": "Metadata key", + "order": { + "$ref": "#/components/schemas/AssetOrder" + }, + "shared": { + "description": "Is shared album", + "type": "boolean" + }, + "startDate": { + "description": "Start date (earliest asset)", + "format": "date-time", "type": "string" }, "updatedAt": { "description": "Last update date", - "example": "2024-01-01T00:00:00.000Z", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - }, - "value": { - "additionalProperties": {}, - "description": "Metadata value (object)", - "type": "object" } }, "required": [ - "assetId", - "key", - "updatedAt", - "value" + "albumName", + "albumThumbnailAssetId", + "albumUsers", + "assetCount", + "createdAt", + "description", + "hasSharedLink", + "id", + "isActivityEnabled", + "shared", + "updatedAt" ], "type": "object" }, - "AssetMetadataBulkUpsertDto": { + "AlbumStatisticsResponseDto": { "properties": { - "items": { - "description": "Metadata items to upsert", - "items": { - "$ref": "#/components/schemas/AssetMetadataBulkUpsertItemDto" - }, - "type": "array" + "notShared": { + "description": "Number of non-shared albums", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "owned": { + "description": "Number of owned albums", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "shared": { + "description": "Number of shared albums", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" } }, "required": [ - "items" + "notShared", + "owned", + "shared" ], "type": "object" }, - "AssetMetadataBulkUpsertItemDto": { + "AlbumUserAddDto": { "properties": { - "assetId": { - "description": "Asset ID", + "role": { + "$ref": "#/components/schemas/AlbumUserRole", + "default": "editor", + "description": "Album user role" + }, + "userId": { + "description": "User ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "key": { - "description": "Metadata key", - "type": "string" - }, - "value": { - "additionalProperties": {}, - "description": "Metadata value (object)", - "type": "object" } }, "required": [ - "assetId", - "key", - "value" + "userId" ], "type": "object" }, - "AssetMetadataResponseDto": { + "AlbumUserCreateDto": { "properties": { - "key": { - "description": "Metadata key", - "type": "string" + "role": { + "$ref": "#/components/schemas/AlbumUserRole" }, - "updatedAt": { - "description": "Last update date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "value": { - "additionalProperties": {}, - "description": "Metadata value (object)", - "type": "object" } }, "required": [ - "key", - "updatedAt", - "value" + "role", + "userId" ], "type": "object" }, - "AssetMetadataUpsertDto": { + "AlbumUserResponseDto": { "properties": { - "items": { - "description": "Metadata items to upsert", - "items": { - "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" - }, - "type": "array" + "role": { + "$ref": "#/components/schemas/AlbumUserRole" + }, + "user": { + "$ref": "#/components/schemas/UserResponseDto" } }, "required": [ - "items" + "role", + "user" ], "type": "object" }, - "AssetMetadataUpsertItemDto": { - "properties": { - "key": { - "description": "Metadata key", - "type": "string" - }, - "value": { - "additionalProperties": {}, - "description": "Metadata value (object)", - "type": "object" - } - }, - "required": [ - "key", - "value" + "AlbumUserRole": { + "description": "Album user role", + "enum": [ + "editor", + "owner", + "viewer" ], - "type": "object" + "type": "string" }, - "AssetOcrResponseDto": { + "AlbumsAddAssetsDto": { "properties": { - "assetId": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "boxScore": { - "description": "Confidence score for text detection box", - "format": "double", - "type": "number" - }, - "id": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "text": { - "description": "Recognized text", - "type": "string" - }, - "textScore": { - "description": "Confidence score for text recognition", - "format": "double", - "type": "number" - }, - "x1": { - "description": "Normalized x coordinate of box corner 1 (0-1)", - "format": "double", - "type": "number" - }, - "x2": { - "description": "Normalized x coordinate of box corner 2 (0-1)", - "format": "double", - "type": "number" - }, - "x3": { - "description": "Normalized x coordinate of box corner 3 (0-1)", - "format": "double", - "type": "number" - }, - "x4": { - "description": "Normalized x coordinate of box corner 4 (0-1)", - "format": "double", - "type": "number" - }, - "y1": { - "description": "Normalized y coordinate of box corner 1 (0-1)", - "format": "double", - "type": "number" - }, - "y2": { - "description": "Normalized y coordinate of box corner 2 (0-1)", - "format": "double", - "type": "number" - }, - "y3": { - "description": "Normalized y coordinate of box corner 3 (0-1)", - "format": "double", - "type": "number" + "albumIds": { + "description": "Album IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" }, - "y4": { - "description": "Normalized y coordinate of box corner 4 (0-1)", - "format": "double", - "type": "number" + "assetIds": { + "description": "Asset IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" } }, "required": [ - "assetId", - "boxScore", - "id", - "text", - "textScore", - "x1", - "x2", - "x3", - "x4", - "y1", - "y2", - "y3", - "y4" + "albumIds", + "assetIds" ], "type": "object" }, - "AssetOrder": { - "description": "Asset sort order", - "enum": [ - "asc", - "desc" + "AlbumsAddAssetsResponseDto": { + "properties": { + "error": { + "$ref": "#/components/schemas/BulkIdErrorReason" + }, + "success": { + "description": "Operation success", + "type": "boolean" + } + }, + "required": [ + "success" ], - "type": "string" + "type": "object" }, - "AssetOrderBy": { - "description": "Asset sorting property", - "enum": [ - "takenAt", - "createdAt" + "AlbumsResponse": { + "properties": { + "defaultAssetOrder": { + "$ref": "#/components/schemas/AssetOrder" + } + }, + "required": [ + "defaultAssetOrder" ], - "type": "string" + "type": "object" }, - "AssetRejectReason": { - "description": "Rejection reason if rejected", - "enum": [ - "duplicate", - "unsupported-format" - ], - "type": "string" + "AlbumsUpdate": { + "description": "Album preferences", + "properties": { + "defaultAssetOrder": { + "$ref": "#/components/schemas/AssetOrder" + } + }, + "type": "object" }, - "AssetResponseDto": { + "ApiKeyCreateDto": { "properties": { - "checksum": { - "description": "Base64 encoded SHA1 hash", - "type": "string" - }, - "createdAt": { - "description": "The UTC timestamp when the asset was originally uploaded to Immich.", - "format": "date-time", - "type": "string" - }, - "duplicateId": { - "description": "Duplicate group ID", - "format": "uuid", - "nullable": true, - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "duration": { - "description": "Video/gif duration in milliseconds (null for static images)", - "maximum": 2147483647, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "exifInfo": { - "$ref": "#/components/schemas/ExifResponseDto" - }, - "fileCreatedAt": { - "description": "The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.", - "format": "date-time", - "type": "string" - }, - "fileModifiedAt": { - "description": "The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.", - "format": "date-time", - "type": "string" - }, - "hasMetadata": { - "description": "Whether asset has metadata", - "type": "boolean" - }, - "height": { - "description": "Asset height", - "maximum": 9007199254740991, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "id": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "name": { + "description": "API key name", "type": "string" }, - "isArchived": { - "description": "Is archived", - "type": "boolean" - }, - "isEdited": { - "description": "Is edited", - "type": "boolean", - "x-immich-history": [ - { - "version": "v2.5.0", - "state": "Added" - }, - { - "version": "v2.5.0", - "state": "Beta" - } - ], - "x-immich-state": "Beta" - }, - "isFavorite": { - "description": "Is favorite", - "type": "boolean" - }, - "isOffline": { - "description": "Is offline", - "type": "boolean" - }, - "isTrashed": { - "description": "Is trashed", - "type": "boolean" - }, - "libraryId": { - "description": "Library ID", - "format": "uuid", - "nullable": true, - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string", + "permissions": { + "description": "List of permissions", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "permissions" + ], + "type": "object" + }, + "ApiKeyCreateResponseDto": { + "properties": { + "apiKey": { + "$ref": "#/components/schemas/ApiKeyResponseDto", "x-immich-history": [ { "version": "v1", "state": "Added" }, { - "version": "v1", + "version": "v3.2.0", "state": "Deprecated" } ], "x-immich-state": "Deprecated" }, - "livePhotoVideoId": { - "description": "Live photo video ID", - "nullable": true, - "type": "string" - }, - "localDateTime": { - "description": "The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by \"local\" days and months.", + "createdAt": { + "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "originalFileName": { - "description": "Original file name", - "type": "string" - }, - "originalMimeType": { - "description": "Original MIME type", - "type": "string" - }, - "originalPath": { - "description": "Original file path", - "type": "string" - }, - "owner": { - "$ref": "#/components/schemas/UserResponseDto" - }, - "ownerId": { - "description": "Owner user ID", + "id": { + "description": "API key ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "people": { - "items": { - "$ref": "#/components/schemas/PersonResponseDto" - }, - "type": "array" - }, - "resized": { - "description": "Is resized", - "type": "boolean", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1.113.0", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" - }, - "stack": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetStackResponseDto" - } - ], - "nullable": true + "name": { + "description": "API key name", + "type": "string" }, - "tags": { + "permissions": { + "description": "List of permissions", "items": { - "$ref": "#/components/schemas/TagResponseDto" + "$ref": "#/components/schemas/Permission" }, "type": "array" }, - "thumbhash": { - "description": "Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.", - "nullable": true, + "secret": { + "description": "API key secret (only shown once)", "type": "string" }, - "type": { - "$ref": "#/components/schemas/AssetTypeEnum" - }, "updatedAt": { - "description": "The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.", + "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" - }, - "width": { - "description": "Asset width", - "maximum": 9007199254740991, - "minimum": 0, - "nullable": true, - "type": "integer" } }, "required": [ - "checksum", + "apiKey", "createdAt", - "duration", - "fileCreatedAt", - "fileModifiedAt", - "hasMetadata", - "height", "id", - "isArchived", - "isEdited", - "isFavorite", - "isOffline", - "isTrashed", - "localDateTime", - "originalFileName", - "originalPath", - "ownerId", - "thumbhash", - "type", - "updatedAt", - "visibility", - "width" + "name", + "permissions", + "secret", + "updatedAt" ], "type": "object" }, - "AssetStackResponseDto": { + "ApiKeyResponseDto": { "properties": { - "assetCount": { - "description": "Number of assets in stack", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "createdAt": { + "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, "id": { - "description": "Stack ID", + "description": "API key ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "primaryAssetId": { - "description": "Primary asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "name": { + "description": "API key name", + "type": "string" + }, + "permissions": { + "description": "List of permissions", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "type": "array" + }, + "updatedAt": { + "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" } }, "required": [ - "assetCount", + "createdAt", "id", - "primaryAssetId" + "name", + "permissions", + "updatedAt" ], "type": "object" }, - "AssetStatsResponseDto": { + "ApiKeyUpdateDto": { "properties": { - "images": { - "description": "Number of images", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "total": { - "description": "Total number of assets", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "name": { + "description": "API key name", + "type": "string" }, - "videos": { - "description": "Number of videos", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "permissions": { + "description": "List of permissions", + "items": { + "$ref": "#/components/schemas/Permission" + }, + "minItems": 1, + "type": "array" } }, - "required": [ - "images", - "total", - "videos" - ], "type": "object" }, - "AssetTypeEnum": { - "description": "Asset type", - "enum": [ - "IMAGE", - "VIDEO", - "AUDIO", - "OTHER" - ], - "type": "string" - }, - "AssetUploadAction": { - "description": "Upload action", - "enum": [ - "accept", - "reject" - ], - "type": "string" - }, - "AssetVisibility": { - "description": "Asset visibility", - "enum": [ - "archive", - "timeline", - "hidden", - "locked" - ], - "type": "string" - }, - "AudioCodec": { - "description": "Target audio codec", - "enum": [ - "mp3", - "aac", - "opus", - "pcm_s16le" - ], - "type": "string" - }, - "AuthStatusResponseDto": { + "AssetBulkDeleteDto": { "properties": { - "expiresAt": { - "description": "Session expiration date", - "type": "string" - }, - "isElevated": { - "description": "Is elevated session", - "type": "boolean" - }, - "password": { - "description": "Has password set", - "type": "boolean" - }, - "pinCode": { - "description": "Has PIN code set", + "force": { + "description": "Force delete even if in use", "type": "boolean" }, - "pinExpiresAt": { - "description": "PIN expiration date", - "type": "string" + "ids": { + "description": "IDs to process", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" } }, "required": [ - "isElevated", - "password", - "pinCode" + "ids" ], "type": "object" }, - "AvatarUpdate": { - "properties": { - "color": { - "$ref": "#/components/schemas/UserAvatarColor" - } - }, - "type": "object" - }, - "BulkIdErrorReason": { - "description": "Error reason", - "enum": [ - "duplicate", - "no_permission", - "not_found", - "unknown", - "validation" - ], - "type": "string" - }, - "BulkIdResponseDto": { + "AssetBulkUpdateDto": { "properties": { - "error": { - "$ref": "#/components/schemas/BulkIdErrorReason" + "dateTimeOriginal": { + "description": "Original date and time", + "type": "string" }, - "errorMessage": { + "dateTimeRelative": { + "description": "Relative time offset in minutes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "description": { + "description": "Asset description", + "type": "string" + }, + "duplicateId": { + "description": "Duplicate ID", + "nullable": true, "type": "string" }, - "id": { - "description": "ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "ids": { + "description": "Asset IDs to update", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "isFavorite": { + "description": "Mark as favorite", + "type": "boolean" + }, + "latitude": { + "description": "Latitude coordinate", + "maximum": 90, + "minimum": -90, + "type": "number" + }, + "longitude": { + "description": "Longitude coordinate", + "maximum": 180, + "minimum": -180, + "type": "number" + }, + "rating": { + "description": "Rating in range [1-5] (starred), -1 (rejected), or null (unrated)", + "maximum": 5, + "minimum": -1, + "nullable": true, + "type": "integer", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v3", + "state": "Updated", + "description": "Using 0 as a rating is no longer valid." + } + ], + "x-immich-state": "Stable" + }, + "timeZone": { + "description": "Time zone (IANA timezone)", "type": "string" }, - "success": { - "description": "Whether operation succeeded", - "type": "boolean" + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" } }, "required": [ - "id", - "success" + "ids" ], "type": "object" }, - "BulkIdsDto": { + "AssetBulkUploadCheckDto": { "properties": { - "ids": { - "description": "IDs to process", + "assets": { + "description": "Assets to check", "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "$ref": "#/components/schemas/AssetBulkUploadCheckItem" }, "type": "array" } }, "required": [ - "ids" + "assets" ], "type": "object" }, - "CLIPConfig": { + "AssetBulkUploadCheckItem": { "properties": { - "enabled": { - "description": "Whether the task is enabled", - "type": "boolean" + "checksum": { + "description": "Base64 or hex encoded SHA1 hash", + "type": "string" }, - "modelName": { - "description": "Name of the model to use", + "id": { + "description": "Client-side identifier echoed in the response to match results to inputs (e.g. filename)", "type": "string" } }, "required": [ - "enabled", - "modelName" + "checksum", + "id" ], "type": "object" }, - "CQMode": { - "description": "CQ mode", - "enum": [ - "auto", - "cqp", - "icq" - ], - "type": "string" - }, - "CalendarHeatmapResponseDto": { + "AssetBulkUploadCheckResponseDto": { "properties": { - "from": { - "description": "Start date in UTC", - "example": "2024-01-01", - "type": "string" - }, - "series": { + "results": { + "description": "Upload check results", "items": { - "properties": { - "count": { - "description": "Activity count", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "date": { - "description": "Date in UTC", - "example": "2024-01-01", - "type": "string" - } - }, - "required": [ - "date", - "count" - ], - "type": "object" + "$ref": "#/components/schemas/AssetBulkUploadCheckResult" }, "type": "array" - }, - "to": { - "description": "End date in UTC", - "example": "2024-12-31", - "type": "string" - }, - "totalCount": { - "description": "Total activity count over the period", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" } }, "required": [ - "from", - "series", - "to", - "totalCount" + "results" ], "type": "object" }, - "CalendarHeatmapType": { - "description": "Type of calendar heatmap", - "enum": [ - "Upload", - "Taken" - ], - "type": "string" - }, - "CastResponse": { + "AssetBulkUploadCheckResult": { "properties": { - "gCastEnabled": { - "description": "Whether Google Cast is enabled", + "action": { + "$ref": "#/components/schemas/AssetUploadAction" + }, + "assetId": { + "description": "Existing asset ID if duplicate", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "id": { + "description": "Client-side identifier echoed from the request to match results to inputs", + "type": "string" + }, + "isTrashed": { + "description": "Whether existing asset is trashed", "type": "boolean" + }, + "reason": { + "$ref": "#/components/schemas/AssetRejectReason" } }, "required": [ - "gCastEnabled" + "action", + "id" ], "type": "object" }, - "CastUpdate": { + "AssetCopyDto": { "properties": { - "gCastEnabled": { - "description": "Whether Google Cast is enabled", + "albums": { + "default": true, + "description": "Copy album associations", "type": "boolean" - } - }, - "type": "object" - }, - "ChangePasswordDto": { - "properties": { - "invalidateSessions": { - "default": false, - "description": "Invalidate all other sessions", + }, + "favorite": { + "default": true, + "description": "Copy favorite status", "type": "boolean" }, - "newPassword": { - "description": "New password (min 8 characters)", - "example": "password", - "minLength": 8, + "sharedLinks": { + "default": true, + "description": "Copy shared links", + "type": "boolean" + }, + "sidecar": { + "default": true, + "description": "Copy sidecar file", + "type": "boolean" + }, + "sourceId": { + "description": "Source asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "password": { - "description": "Current password", - "example": "password", + "stack": { + "default": true, + "description": "Copy stack association", + "type": "boolean" + }, + "targetId": { + "description": "Target asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, "required": [ - "newPassword", - "password" + "sourceId", + "targetId" ], "type": "object" }, - "Colorspace": { - "description": "Colorspace", + "AssetEditAction": { + "description": "Type of edit action to perform", "enum": [ - "srgb", - "p3" + "crop", + "rotate", + "mirror" ], "type": "string" }, - "ContributorCountResponseDto": { - "properties": { - "assetCount": { - "description": "Number of assets contributed", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "userId": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - "required": [ - "assetCount", - "userId" - ], - "type": "object" - }, - "CreateAlbumDto": { + "AssetEditActionItemDto": { "properties": { - "albumName": { - "description": "Album name", - "type": "string" - }, - "albumUsers": { - "description": "Album users", - "items": { - "$ref": "#/components/schemas/AlbumUserCreateDto" - }, - "type": "array" - }, - "assetIds": { - "description": "Initial asset IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "action": { + "$ref": "#/components/schemas/AssetEditAction" }, - "description": { - "description": "Album description", - "nullable": true, - "type": "string", - "x-immich-history": [ + "parameters": { + "anyOf": [ + { + "$ref": "#/components/schemas/CropParameters" + }, { - "version": "v1", - "state": "Added" + "$ref": "#/components/schemas/RotateParameters" }, { - "version": "v3", - "state": "Updated", - "description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4." + "$ref": "#/components/schemas/MirrorParameters" } - ] + ], + "description": "List of edit actions to apply (crop, rotate, or mirror)" } }, "required": [ - "albumName" + "action", + "parameters" ], "type": "object" }, - "CreateLibraryDto": { + "AssetEditActionItemResponseDto": { "properties": { - "exclusionPatterns": { - "description": "Exclusion patterns (max 128)", - "items": { - "type": "string" - }, - "maxItems": 128, - "type": "array" - }, - "importPaths": { - "description": "Import paths (max 128)", - "items": { - "type": "string" - }, - "maxItems": 128, - "type": "array" - }, - "name": { - "description": "Library name", - "minLength": 1, - "type": "string" + "action": { + "$ref": "#/components/schemas/AssetEditAction" }, - "ownerId": { - "description": "Owner user ID", + "id": { + "description": "Asset edit ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" + }, + "parameters": { + "anyOf": [ + { + "$ref": "#/components/schemas/CropParameters" + }, + { + "$ref": "#/components/schemas/RotateParameters" + }, + { + "$ref": "#/components/schemas/MirrorParameters" + } + ], + "description": "List of edit actions to apply (crop, rotate, or mirror)" } }, "required": [ - "ownerId" + "action", + "id", + "parameters" ], "type": "object" }, - "CreateProfileImageDto": { + "AssetEditsCreateDto": { "properties": { - "file": { - "description": "Profile image file", - "format": "binary", - "type": "string" + "edits": { + "description": "List of edit actions to apply (crop, rotate, or mirror)", + "items": { + "$ref": "#/components/schemas/AssetEditActionItemDto" + }, + "minItems": 1, + "type": "array" } }, "required": [ - "file" + "edits" ], "type": "object" }, - "CreateProfileImageResponseDto": { + "AssetEditsResponseDto": { "properties": { - "profileChangedAt": { - "description": "Profile image change date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "profileImagePath": { - "description": "Profile image file path", - "type": "string" - }, - "userId": { - "description": "User ID", + "assetId": { + "description": "Asset ID these edits belong to", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" + }, + "edits": { + "description": "List of edit actions applied to the asset", + "items": { + "$ref": "#/components/schemas/AssetEditActionItemResponseDto" + }, + "type": "array" } }, "required": [ - "profileChangedAt", - "profileImagePath", - "userId" + "assetId", + "edits" ], "type": "object" }, - "CropParameters": { + "AssetFaceCreateDto": { "properties": { + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, "height": { - "description": "Height of the crop", + "description": "Face bounding box height", "maximum": 9007199254740991, - "minimum": 1, + "minimum": -9007199254740991, + "type": "integer" + }, + "imageHeight": { + "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "imageWidth": { + "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, + "personId": { + "description": "Person ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, "width": { - "description": "Width of the crop", + "description": "Face bounding box width", "maximum": 9007199254740991, - "minimum": 1, + "minimum": -9007199254740991, "type": "integer" }, "x": { - "description": "Top-Left X coordinate of crop", + "description": "Face bounding box X coordinate", "maximum": 9007199254740991, - "minimum": 0, + "minimum": -9007199254740991, "type": "integer" }, "y": { - "description": "Top-Left Y coordinate of crop", + "description": "Face bounding box Y coordinate", "maximum": 9007199254740991, - "minimum": 0, + "minimum": -9007199254740991, "type": "integer" } }, "required": [ + "assetId", "height", + "imageHeight", + "imageWidth", + "personId", "width", "x", "y" ], "type": "object" }, - "DatabaseBackupConfig": { + "AssetFaceDeleteDto": { "properties": { - "cronExpression": { - "description": "Cron expression", - "type": "string" - }, - "enabled": { - "description": "Enabled", + "force": { + "description": "Force delete even if person has other faces", "type": "boolean" - }, - "keepLastAmount": { - "description": "Keep last amount", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "cronExpression", - "enabled", - "keepLastAmount" - ], - "type": "object" - }, - "DatabaseBackupDeleteDto": { - "properties": { - "backups": { - "description": "Backup filenames to delete", - "items": { - "type": "string" - }, - "type": "array" } }, "required": [ - "backups" + "force" ], "type": "object" }, - "DatabaseBackupDto": { + "AssetFaceResponseDto": { + "description": "Asset face with person", "properties": { - "filename": { - "description": "Backup filename", - "type": "string" + "boundingBoxX1": { + "description": "Bounding box X1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "filesize": { - "description": "Backup file size", + "boundingBoxX2": { + "description": "Bounding box X2 coordinate", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" }, - "timezone": { - "description": "Backup timezone", + "boundingBoxY1": { + "description": "Bounding box Y1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "boundingBoxY2": { + "description": "Bounding box Y2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "id": { + "description": "Face ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" + }, + "imageHeight": { + "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "imageWidth": { + "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "person": { + "allOf": [ + { + "$ref": "#/components/schemas/PersonResponseDto" + } + ], + "nullable": true + }, + "sourceType": { + "$ref": "#/components/schemas/SourceType" } }, "required": [ - "filename", - "filesize", - "timezone" + "boundingBoxX1", + "boundingBoxX2", + "boundingBoxY1", + "boundingBoxY2", + "id", + "imageHeight", + "imageWidth", + "person" ], "type": "object" }, - "DatabaseBackupListResponseDto": { + "AssetFaceUpdateDto": { "properties": { - "backups": { - "description": "List of backups", + "data": { + "description": "Face update items", "items": { - "$ref": "#/components/schemas/DatabaseBackupDto" + "$ref": "#/components/schemas/AssetFaceUpdateItem" }, "type": "array" } }, "required": [ - "backups" + "data" ], "type": "object" }, - "DatabaseBackupUploadDto": { + "AssetFaceUpdateItem": { "properties": { - "file": { - "description": "Database backup file", - "format": "binary", + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - } - }, - "type": "object" - }, - "DownloadArchiveDto": { - "properties": { - "assetIds": { - "description": "Asset IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" }, - "edited": { - "description": "Download edited asset if available", - "type": "boolean" + "personId": { + "description": "Person ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "assetIds" + "assetId", + "personId" ], "type": "object" }, - "DownloadArchiveInfo": { + "AssetIdErrorReason": { + "description": "Error reason if failed", + "enum": [ + "duplicate", + "no_permission", + "not_found" + ], + "type": "string" + }, + "AssetIdsDto": { "properties": { "assetIds": { - "description": "Asset IDs in this archive", + "description": "Asset IDs", "items": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" - }, - "size": { - "description": "Archive size in bytes", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" } }, "required": [ - "assetIds", - "size" + "assetIds" ], "type": "object" }, - "DownloadInfoDto": { + "AssetIdsResponseDto": { "properties": { - "albumId": { - "description": "Album ID to download", + "assetId": { + "description": "Asset ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "archiveSize": { - "description": "Archive size limit in bytes", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - }, - "assetIds": { - "description": "Asset IDs to download", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" - }, - "userId": { - "description": "User ID to download assets from", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - "type": "object" - }, - "DownloadResponse": { - "properties": { - "archiveSize": { - "description": "Maximum archive size in bytes", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "error": { + "$ref": "#/components/schemas/AssetIdErrorReason" }, - "includeEmbeddedVideos": { - "description": "Whether to include embedded videos in downloads", + "success": { + "description": "Whether operation succeeded", "type": "boolean" } }, "required": [ - "archiveSize", - "includeEmbeddedVideos" + "assetId", + "success" ], "type": "object" }, - "DownloadResponseDto": { + "AssetJobName": { + "description": "Job name", + "enum": [ + "refresh-faces", + "refresh-metadata", + "regenerate-thumbnail", + "transcode-video" + ], + "type": "string" + }, + "AssetJobsDto": { "properties": { - "archives": { - "description": "Archive information", + "assetIds": { + "description": "Asset IDs", "items": { - "$ref": "#/components/schemas/DownloadArchiveInfo" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, "type": "array" }, - "totalSize": { - "description": "Total size in bytes", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "name": { + "$ref": "#/components/schemas/AssetJobName" } }, "required": [ - "archives", - "totalSize" + "assetIds", + "name" ], "type": "object" }, - "DownloadUpdate": { + "AssetMediaCreateDto": { "properties": { - "archiveSize": { - "description": "Maximum archive size in bytes", + "assetData": { + "description": "Asset file data", + "format": "binary", + "type": "string" + }, + "duration": { + "description": "Duration in milliseconds (for videos)", "maximum": 9007199254740991, - "minimum": 1, + "minimum": 0, "type": "integer" }, - "includeEmbeddedVideos": { - "description": "Whether to include embedded videos in downloads", - "type": "boolean" - } - }, - "type": "object" - }, - "DuplicateDetectionConfig": { - "properties": { - "enabled": { - "description": "Whether the task is enabled", + "fileCreatedAt": { + "description": "File creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "fileModifiedAt": { + "description": "File modification date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "filename": { + "description": "Filename", + "type": "string" + }, + "isFavorite": { + "description": "Mark as favorite", "type": "boolean" }, - "maxDistance": { - "description": "Maximum distance threshold for duplicate detection", - "format": "double", - "maximum": 0.1, - "minimum": 0.001, - "type": "number" - } - }, - "required": [ - "enabled", - "maxDistance" - ], - "type": "object" - }, - "DuplicateResolveDto": { - "properties": { - "groups": { - "description": "List of duplicate groups to resolve", - "items": { - "$ref": "#/components/schemas/DuplicateResolveGroupDto" - }, - "minItems": 1, - "type": "array" - } - }, - "required": [ - "groups" - ], - "type": "object" - }, - "DuplicateResolveGroupDto": { - "properties": { - "duplicateId": { + "livePhotoVideoId": { + "description": "Live photo video ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "keepAssetIds": { - "description": "Asset IDs to keep", + "metadata": { + "description": "Asset metadata items", "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" }, "type": "array" }, - "trashAssetIds": { - "description": "Asset IDs to trash or delete", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "sidecarData": { + "description": "Sidecar file data", + "format": "binary", + "type": "string" + }, + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" } }, "required": [ - "duplicateId", - "keepAssetIds", - "trashAssetIds" - ], - "type": "object" - }, - "DuplicateResponseDto": { - "properties": { - "assets": { - "description": "Duplicate assets", - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - }, - "duplicateId": { - "description": "Duplicate group ID", + "assetData", + "fileCreatedAt", + "fileModifiedAt" + ], + "type": "object" + }, + "AssetMediaResponseDto": { + "properties": { + "id": { + "description": "Asset media ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "suggestedKeepAssetIds": { - "description": "Suggested asset IDs to keep based on file size and EXIF data", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "status": { + "$ref": "#/components/schemas/AssetMediaStatus" } }, "required": [ - "assets", - "duplicateId", - "suggestedKeepAssetIds" + "id", + "status" ], "type": "object" }, - "EmailNotificationsResponse": { + "AssetMediaSize": { + "description": "Asset media size", + "enum": [ + "original", + "fullsize", + "preview", + "thumbnail" + ], + "type": "string" + }, + "AssetMediaStatus": { + "description": "Upload status", + "enum": [ + "created", + "duplicate" + ], + "type": "string" + }, + "AssetMetadataBulkDeleteDto": { "properties": { - "albumInvite": { - "description": "Whether to receive email notifications for album invites", - "type": "boolean" - }, - "albumUpdate": { - "description": "Whether to receive email notifications for album updates", - "type": "boolean" - }, - "enabled": { - "description": "Whether email notifications are enabled", - "type": "boolean" + "items": { + "description": "Metadata items to delete", + "items": { + "$ref": "#/components/schemas/AssetMetadataBulkDeleteItemDto" + }, + "type": "array" } }, "required": [ - "albumInvite", - "albumUpdate", - "enabled" + "items" ], "type": "object" }, - "EmailNotificationsUpdate": { + "AssetMetadataBulkDeleteItemDto": { "properties": { - "albumInvite": { - "description": "Whether to receive email notifications for album invites", - "type": "boolean" - }, - "albumUpdate": { - "description": "Whether to receive email notifications for album updates", - "type": "boolean" + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "enabled": { - "description": "Whether email notifications are enabled", - "type": "boolean" + "key": { + "description": "Metadata key", + "type": "string" } }, + "required": [ + "assetId", + "key" + ], "type": "object" }, - "ExifResponseDto": { - "description": "EXIF response", + "AssetMetadataBulkResponseDto": { "properties": { - "city": { - "default": null, - "description": "City name", - "nullable": true, - "type": "string" - }, - "country": { - "default": null, - "description": "Country name", - "nullable": true, - "type": "string" - }, - "dateTimeOriginal": { - "default": null, - "description": "Original date/time", - "format": "date-time", - "nullable": true, - "type": "string" - }, - "description": { - "default": null, - "description": "Image description", - "nullable": true, - "type": "string" - }, - "exifImageHeight": { - "default": null, - "description": "Image height in pixels", - "maximum": 9007199254740991, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "exifImageWidth": { - "default": null, - "description": "Image width in pixels", - "maximum": 9007199254740991, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "exposureTime": { - "default": null, - "description": "Exposure time", - "nullable": true, - "type": "string" - }, - "fNumber": { - "default": null, - "description": "F-number (aperture)", - "nullable": true, - "type": "number" - }, - "fileSizeInByte": { - "default": null, - "description": "File size in bytes", - "maximum": 9007199254740991, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "focalLength": { - "default": null, - "description": "Focal length in mm", - "nullable": true, - "type": "number" - }, - "iso": { - "default": null, - "description": "ISO sensitivity", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "nullable": true, - "type": "integer" - }, - "latitude": { - "default": null, - "description": "GPS latitude", - "nullable": true, - "type": "number" - }, - "lensModel": { - "default": null, - "description": "Lens model", - "nullable": true, - "type": "string" - }, - "longitude": { - "default": null, - "description": "GPS longitude", - "nullable": true, - "type": "number" - }, - "make": { - "default": null, - "description": "Camera make", - "nullable": true, + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "model": { - "default": null, - "description": "Camera model", - "nullable": true, + "key": { + "description": "Metadata key", "type": "string" }, - "modifyDate": { - "default": null, - "description": "Modification date/time", + "updatedAt": { + "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", - "nullable": true, - "type": "string" - }, - "orientation": { - "default": null, - "description": "Image orientation", - "nullable": true, - "type": "string" - }, - "projectionType": { - "default": null, - "description": "Projection type", - "nullable": true, - "type": "string" - }, - "rating": { - "default": null, - "description": "Rating", - "maximum": 5, - "minimum": 1, - "nullable": true, - "type": "integer" - }, - "state": { - "default": null, - "description": "State/province name", - "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "timeZone": { - "default": null, - "description": "Time zone", - "nullable": true, - "type": "string" + "value": { + "additionalProperties": {}, + "description": "Metadata value (object)", + "type": "object" } }, + "required": [ + "assetId", + "key", + "updatedAt", + "value" + ], "type": "object" }, - "FaceDto": { + "AssetMetadataBulkUpsertDto": { "properties": { - "id": { - "description": "Face ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "items": { + "description": "Metadata items to upsert", + "items": { + "$ref": "#/components/schemas/AssetMetadataBulkUpsertItemDto" + }, + "type": "array" } }, "required": [ - "id" + "items" ], "type": "object" }, - "FacialRecognitionConfig": { + "AssetMetadataBulkUpsertItemDto": { "properties": { - "enabled": { - "description": "Whether the task is enabled", - "type": "boolean" - }, - "maxDistance": { - "description": "Maximum distance threshold for face recognition", - "format": "double", - "maximum": 2, - "minimum": 0.1, - "type": "number" - }, - "minFaces": { - "description": "Minimum number of faces required for recognition", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - }, - "minScore": { - "description": "Minimum confidence score for face detection", - "format": "double", - "maximum": 1, - "minimum": 0.1, - "type": "number" + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "modelName": { - "description": "Name of the model to use", + "key": { + "description": "Metadata key", "type": "string" + }, + "value": { + "additionalProperties": {}, + "description": "Metadata value (object)", + "type": "object" } }, "required": [ - "enabled", - "maxDistance", - "minFaces", - "minScore", - "modelName" + "assetId", + "key", + "value" ], "type": "object" }, - "FoldersResponse": { + "AssetMetadataResponseDto": { "properties": { - "enabled": { - "description": "Whether folders are enabled", - "type": "boolean" + "key": { + "description": "Metadata key", + "type": "string" }, - "sidebarWeb": { - "description": "Whether folders appear in web sidebar", - "type": "boolean" + "updatedAt": { + "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "value": { + "additionalProperties": {}, + "description": "Metadata value (object)", + "type": "object" } }, "required": [ - "enabled", - "sidebarWeb" + "key", + "updatedAt", + "value" ], "type": "object" }, - "FoldersUpdate": { + "AssetMetadataUpsertDto": { "properties": { - "enabled": { - "description": "Whether folders are enabled", - "type": "boolean" - }, - "sidebarWeb": { - "description": "Whether folders appear in web sidebar", - "type": "boolean" + "items": { + "description": "Metadata items to upsert", + "items": { + "$ref": "#/components/schemas/AssetMetadataUpsertItemDto" + }, + "type": "array" } }, - "type": "object" - }, - "HlsVideoResolution": { - "description": "HLS video resolution", - "enum": [ - 480, - 720, - 1080, - 1440, - 2160 - ], - "type": "integer" - }, - "ImageFormat": { - "description": "Image format", - "enum": [ - "jpeg", - "webp" - ], - "type": "string" - }, - "IntegrityReport": { - "description": "Integrity report type", - "enum": [ - "untracked_file", - "missing_file", - "checksum_mismatch" + "required": [ + "items" ], - "type": "string" + "type": "object" }, - "IntegrityReportResponseDto": { + "AssetMetadataUpsertItemDto": { "properties": { - "items": { - "items": { - "properties": { - "id": { - "description": "Integrity report item id", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "path": { - "description": "Integrity report item path", - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/IntegrityReport" - } - }, - "required": [ - "id", - "type", - "path" - ], - "type": "object" - }, - "type": "array" - }, - "nextCursor": { + "key": { + "description": "Metadata key", "type": "string" + }, + "value": { + "additionalProperties": {}, + "description": "Metadata value (object)", + "type": "object" } }, "required": [ - "items" + "key", + "value" ], "type": "object" }, - "IntegrityReportSummaryResponseDto": { + "AssetOcrResponseDto": { "properties": { - "checksum_mismatch": { - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "assetId": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "missing_file": { - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "boxScore": { + "description": "Confidence score for text detection box", + "format": "double", + "type": "number" }, - "untracked_file": { - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "id": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "text": { + "description": "Recognized text", + "type": "string" + }, + "textScore": { + "description": "Confidence score for text recognition", + "format": "double", + "type": "number" + }, + "x1": { + "description": "Normalized x coordinate of box corner 1 (0-1)", + "format": "double", + "type": "number" + }, + "x2": { + "description": "Normalized x coordinate of box corner 2 (0-1)", + "format": "double", + "type": "number" + }, + "x3": { + "description": "Normalized x coordinate of box corner 3 (0-1)", + "format": "double", + "type": "number" + }, + "x4": { + "description": "Normalized x coordinate of box corner 4 (0-1)", + "format": "double", + "type": "number" + }, + "y1": { + "description": "Normalized y coordinate of box corner 1 (0-1)", + "format": "double", + "type": "number" + }, + "y2": { + "description": "Normalized y coordinate of box corner 2 (0-1)", + "format": "double", + "type": "number" + }, + "y3": { + "description": "Normalized y coordinate of box corner 3 (0-1)", + "format": "double", + "type": "number" + }, + "y4": { + "description": "Normalized y coordinate of box corner 4 (0-1)", + "format": "double", + "type": "number" } }, "required": [ - "checksum_mismatch", - "missing_file", - "untracked_file" + "assetId", + "boxScore", + "id", + "text", + "textScore", + "x1", + "x2", + "x3", + "x4", + "y1", + "y2", + "y3", + "y4" ], "type": "object" }, - "JobCreateDto": { - "properties": { - "name": { - "$ref": "#/components/schemas/ManualJobName" - } - }, - "required": [ - "name" + "AssetOrder": { + "description": "Asset sort order", + "enum": [ + "asc", + "desc" ], - "type": "object" + "type": "string" }, - "JobName": { - "description": "Job name", + "AssetOrderBy": { + "description": "Asset sorting property", "enum": [ - "AssetDelete", - "AssetDeleteCheck", - "AssetDetectFacesQueueAll", - "AssetDetectFaces", - "AssetDetectDuplicatesQueueAll", - "AssetDetectDuplicates", - "AssetEditThumbnailGeneration", - "AssetEncodeVideoQueueAll", - "AssetEncodeVideo", - "AssetEmptyTrash", - "AssetExtractMetadataQueueAll", - "AssetExtractMetadata", - "AssetFileMigration", - "AssetGenerateThumbnailsQueueAll", - "AssetGenerateThumbnails", - "AuditTableCleanup", - "DatabaseBackup", - "FacialRecognitionQueueAll", - "FacialRecognition", - "FileDelete", - "FileMigrationQueueAll", - "LibraryDeleteCheck", - "LibraryDelete", - "LibraryRemoveAsset", - "LibraryScanAssetsQueueAll", - "LibrarySyncAssets", - "LibrarySyncFilesQueueAll", - "LibrarySyncFiles", - "LibraryScanQueueAll", - "HlsSessionCleanup", - "MemoryCleanup", - "MemoryGenerate", - "NotificationsCleanup", - "NotifyUserSignup", - "NotifyAlbumInvite", - "NotifyAlbumUpdate", - "UserDelete", - "UserDeleteCheck", - "UserSyncUsage", - "PersonCleanup", - "PersonFileMigration", - "PersonGenerateThumbnail", - "SessionCleanup", - "SendMail", - "SidecarQueueAll", - "SidecarCheck", - "SidecarWrite", - "SmartSearchQueueAll", - "SmartSearch", - "StorageTemplateMigration", - "StorageTemplateMigrationSingle", - "TagCleanup", - "VersionCheck", - "OcrQueueAll", - "Ocr", - "WorkflowAssetTrigger", - "IntegrityUntrackedFilesQueueAll", - "IntegrityUntrackedFiles", - "IntegrityUntrackedRefresh", - "IntegrityMissingFilesQueueAll", - "IntegrityMissingFiles", - "IntegrityMissingFilesRefresh", - "IntegrityChecksumFiles", - "IntegrityChecksumFilesRefresh", - "IntegrityDeleteReportType", - "IntegrityDeleteReports" + "takenAt", + "createdAt" + ], + "type": "string" + }, + "AssetRejectReason": { + "description": "Rejection reason if rejected", + "enum": [ + "duplicate", + "unsupported-format" ], "type": "string" }, - "JobSettingsDto": { + "AssetResponseDto": { "properties": { - "concurrency": { - "description": "Concurrency", + "checksum": { + "description": "Base64 encoded SHA1 hash", + "type": "string" + }, + "createdAt": { + "description": "The UTC timestamp when the asset was originally uploaded to Immich.", + "format": "date-time", + "type": "string" + }, + "duplicateId": { + "description": "Duplicate group ID", + "format": "uuid", + "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "duration": { + "description": "Video/gif duration in milliseconds (null for static images)", + "maximum": 2147483647, + "minimum": 0, + "nullable": true, + "type": "integer" + }, + "exifInfo": { + "$ref": "#/components/schemas/ExifResponseDto" + }, + "fileCreatedAt": { + "description": "The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.", + "format": "date-time", + "type": "string" + }, + "fileModifiedAt": { + "description": "The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.", + "format": "date-time", + "type": "string" + }, + "hasMetadata": { + "description": "Whether asset has metadata", + "type": "boolean" + }, + "height": { + "description": "Asset height", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer" + }, + "id": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "isArchived": { + "description": "Is archived", + "type": "boolean" + }, + "isEdited": { + "description": "Is edited", + "type": "boolean", + "x-immich-history": [ + { + "version": "v2.5.0", + "state": "Added" + }, + { + "version": "v2.5.0", + "state": "Beta" + } + ], + "x-immich-state": "Beta" + }, + "isFavorite": { + "description": "Is favorite", + "type": "boolean" + }, + "isOffline": { + "description": "Is offline", + "type": "boolean" + }, + "isTrashed": { + "description": "Is trashed", + "type": "boolean" + }, + "libraryId": { + "description": "Library ID", + "format": "uuid", + "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1", + "state": "Deprecated" + } + ], + "x-immich-state": "Deprecated" + }, + "livePhotoVideoId": { + "description": "Live photo video ID", + "nullable": true, + "type": "string" + }, + "localDateTime": { + "description": "The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by \"local\" days and months.", + "format": "date-time", + "type": "string" + }, + "originalFileName": { + "description": "Original file name", + "type": "string" + }, + "originalMimeType": { + "description": "Original MIME type", + "type": "string" + }, + "originalPath": { + "description": "Original file path", + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/UserResponseDto" + }, + "ownerId": { + "description": "Owner user ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "people": { + "items": { + "$ref": "#/components/schemas/PersonResponseDto" + }, + "type": "array" + }, + "resized": { + "description": "Is resized", + "type": "boolean", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v1.113.0", + "state": "Deprecated" + } + ], + "x-immich-state": "Deprecated" + }, + "stack": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetStackResponseDto" + } + ], + "nullable": true + }, + "tags": { + "items": { + "$ref": "#/components/schemas/TagResponseDto" + }, + "type": "array" + }, + "thumbhash": { + "description": "Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.", + "nullable": true, + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/AssetTypeEnum" + }, + "updatedAt": { + "description": "The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.", + "format": "date-time", + "type": "string" + }, + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" + }, + "width": { + "description": "Asset width", "maximum": 9007199254740991, - "minimum": 1, + "minimum": 0, + "nullable": true, "type": "integer" } }, "required": [ - "concurrency" + "checksum", + "createdAt", + "duration", + "fileCreatedAt", + "fileModifiedAt", + "hasMetadata", + "height", + "id", + "isArchived", + "isEdited", + "isFavorite", + "isOffline", + "isTrashed", + "localDateTime", + "originalFileName", + "originalPath", + "ownerId", + "thumbhash", + "type", + "updatedAt", + "visibility", + "width" ], "type": "object" }, - "LibraryResponseDto": { + "AssetStackResponseDto": { "properties": { "assetCount": { - "description": "Number of assets", + "description": "Number of assets in stack", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" }, - "createdAt": { - "description": "Creation date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "exclusionPatterns": { - "description": "Exclusion patterns", - "items": { - "type": "string" - }, - "type": "array" - }, "id": { - "description": "Library ID", + "description": "Stack ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "importPaths": { - "description": "Import paths", - "items": { - "type": "string" - }, - "type": "array" - }, - "name": { - "description": "Library name", - "type": "string" - }, - "ownerId": { - "description": "Owner user ID", + "primaryAssetId": { + "description": "Primary asset ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "refreshedAt": { - "description": "Last refresh date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "updatedAt": { - "description": "Last update date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" } }, "required": [ "assetCount", - "createdAt", - "exclusionPatterns", "id", - "importPaths", - "name", - "ownerId", - "refreshedAt", - "updatedAt" + "primaryAssetId" ], "type": "object" }, - "LibraryStatsResponseDto": { + "AssetStatsResponseDto": { "properties": { - "photos": { - "description": "Number of photos", + "images": { + "description": "Number of images", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" @@ -19604,12 +20239,6 @@ "minimum": -9007199254740991, "type": "integer" }, - "usage": { - "description": "Storage usage in bytes", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, "videos": { "description": "Number of videos", "maximum": 9007199254740991, @@ -19618,386 +20247,555 @@ } }, "required": [ - "photos", - "total", - "usage", - "videos" + "images", + "total", + "videos" + ], + "type": "object" + }, + "AssetTypeEnum": { + "description": "Asset type", + "enum": [ + "IMAGE", + "VIDEO", + "AUDIO", + "OTHER" + ], + "type": "string" + }, + "AssetUploadAction": { + "description": "Upload action", + "enum": [ + "accept", + "reject" + ], + "type": "string" + }, + "AssetVisibility": { + "description": "Asset visibility", + "enum": [ + "archive", + "timeline", + "hidden", + "locked" + ], + "type": "string" + }, + "AudioCodec": { + "description": "Target audio codec", + "enum": [ + "mp3", + "aac", + "opus", + "pcm_s16le" + ], + "type": "string" + }, + "AuthStatusResponseDto": { + "properties": { + "expiresAt": { + "description": "Session expiration date", + "type": "string" + }, + "isElevated": { + "description": "Is elevated session", + "type": "boolean" + }, + "password": { + "description": "Has password set", + "type": "boolean" + }, + "pinCode": { + "description": "Has PIN code set", + "type": "boolean" + }, + "pinExpiresAt": { + "description": "PIN expiration date", + "type": "string" + } + }, + "required": [ + "isElevated", + "password", + "pinCode" + ], + "type": "object" + }, + "AvatarUpdate": { + "properties": { + "color": { + "$ref": "#/components/schemas/UserAvatarColor" + } + }, + "type": "object" + }, + "BulkIdErrorReason": { + "description": "Error reason", + "enum": [ + "duplicate", + "no_permission", + "not_found", + "unknown", + "validation" + ], + "type": "string" + }, + "BulkIdResponseDto": { + "properties": { + "error": { + "$ref": "#/components/schemas/BulkIdErrorReason" + }, + "errorMessage": { + "type": "string" + }, + "id": { + "description": "ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "success": { + "description": "Whether operation succeeded", + "type": "boolean" + } + }, + "required": [ + "id", + "success" + ], + "type": "object" + }, + "BulkIdsDto": { + "properties": { + "ids": { + "description": "IDs to process", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "ids" ], "type": "object" }, - "LicenseKeyDto": { + "CQMode": { + "description": "CQ mode", + "enum": [ + "auto", + "cqp", + "icq" + ], + "type": "string" + }, + "CalendarHeatmapResponseDto": { "properties": { - "activationKey": { - "description": "Activation key", + "from": { + "description": "Start date in UTC", + "example": "2024-01-01", "type": "string" }, - "licenseKey": { - "description": "License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/)", - "pattern": "^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$", + "series": { + "items": { + "properties": { + "count": { + "description": "Activity count", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "date": { + "description": "Date in UTC", + "example": "2024-01-01", + "type": "string" + } + }, + "required": [ + "date", + "count" + ], + "type": "object" + }, + "type": "array" + }, + "to": { + "description": "End date in UTC", + "example": "2024-12-31", "type": "string" + }, + "totalCount": { + "description": "Total activity count over the period", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" } }, "required": [ - "activationKey", - "licenseKey" + "from", + "series", + "to", + "totalCount" ], "type": "object" }, - "LicenseResponseDto": { - "$ref": "#/components/schemas/UserLicense" - }, - "LogLevel": { - "description": "Log level", + "CalendarHeatmapType": { + "description": "Type of calendar heatmap", "enum": [ - "verbose", - "debug", - "log", - "warn", - "error", - "fatal" + "Upload", + "Taken" ], "type": "string" }, - "LoginCredentialDto": { + "CastResponse": { "properties": { - "email": { - "description": "User email", - "example": "testuser@email.com", - "format": "email", - "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", - "type": "string" - }, - "password": { - "description": "User password", - "example": "password", - "type": "string" + "gCastEnabled": { + "description": "Whether Google Cast is enabled", + "type": "boolean" } }, "required": [ - "email", - "password" + "gCastEnabled" ], "type": "object" }, - "LoginResponseDto": { + "CastUpdate": { "properties": { - "accessToken": { - "description": "Access token", - "type": "string" - }, - "isAdmin": { - "description": "Is admin user", + "gCastEnabled": { + "description": "Whether Google Cast is enabled", "type": "boolean" - }, - "isOnboarded": { - "description": "Is onboarded", + } + }, + "type": "object" + }, + "ChangePasswordDto": { + "properties": { + "invalidateSessions": { + "default": false, + "description": "Invalidate all other sessions", "type": "boolean" }, - "name": { - "description": "User name", - "type": "string" - }, - "profileImagePath": { - "description": "Profile image path", + "newPassword": { + "description": "New password (min 8 characters)", + "example": "password", + "minLength": 8, "type": "string" }, - "shouldChangePassword": { - "description": "Should change password", - "type": "boolean" - }, - "userEmail": { - "description": "User email", - "format": "email", - "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", + "password": { + "description": "Current password", + "example": "password", "type": "string" - }, + } + }, + "required": [ + "newPassword", + "password" + ], + "type": "object" + }, + "ClusterGroupRequestCreateDto": { + "properties": { "userId": { - "description": "User ID", + "description": "User to invite into the cluster group", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, "required": [ - "accessToken", - "isAdmin", - "isOnboarded", - "name", - "profileImagePath", - "shouldChangePassword", - "userEmail", "userId" ], "type": "object" }, - "LogoutResponseDto": { + "ClusterGroupRequestResponseDto": { "properties": { - "redirectUri": { - "description": "Redirect URI", + "clusterGroupId": { + "description": "Cluster group the user is invited to join", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "successful": { - "description": "Logout successful", - "type": "boolean" + "createdAt": { + "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "id": { + "description": "Request ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "userId": { + "description": "User the request was created for", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "redirectUri", - "successful" + "clusterGroupId", + "createdAt", + "id", + "userId" ], "type": "object" }, - "MachineLearningAvailabilityChecksDto": { + "Colorspace": { + "description": "Colorspace", + "enum": [ + "srgb", + "p3" + ], + "type": "string" + }, + "ContributorCountResponseDto": { "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "interval": { + "assetCount": { + "description": "Number of assets contributed", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" }, - "timeout": { - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "enabled", - "interval", - "timeout" + "assetCount", + "userId" ], "type": "object" }, - "MaintenanceAction": { - "description": "Maintenance action", - "enum": [ - "start", - "end", - "select_database_restore", - "restore_database" - ], - "type": "string" - }, - "MaintenanceAuthDto": { + "CreateAlbumDto": { "properties": { - "username": { - "description": "Maintenance username", + "albumName": { + "description": "Album name", "type": "string" + }, + "albumUsers": { + "description": "Album users", + "items": { + "$ref": "#/components/schemas/AlbumUserCreateDto" + }, + "type": "array" + }, + "assetIds": { + "description": "Initial asset IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "Album description", + "nullable": true, + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v3", + "state": "Updated", + "description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4." + } + ] } }, "required": [ - "username" + "albumName" ], "type": "object" }, - "MaintenanceDetectInstallResponseDto": { + "CreateLibraryDto": { "properties": { - "storage": { + "exclusionPatterns": { + "description": "Exclusion patterns (max 128)", "items": { - "$ref": "#/components/schemas/MaintenanceDetectInstallStorageFolderDto" + "type": "string" + }, + "maxItems": 128, + "type": "array" + }, + "importPaths": { + "description": "Import paths (max 128)", + "items": { + "type": "string" }, + "maxItems": 128, "type": "array" + }, + "name": { + "description": "Library name", + "minLength": 1, + "type": "string" + }, + "ownerId": { + "description": "Owner user ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "storage" + "ownerId" ], "type": "object" }, - "MaintenanceDetectInstallStorageFolderDto": { + "CreateProfileImageDto": { "properties": { - "files": { - "description": "Number of files in the folder", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "folder": { - "$ref": "#/components/schemas/StorageFolder" - }, - "readable": { - "description": "Whether the folder is readable", - "type": "boolean" - }, - "writable": { - "description": "Whether the folder is writable", - "type": "boolean" + "file": { + "description": "Profile image file", + "format": "binary", + "type": "string" } }, "required": [ - "files", - "folder", - "readable", - "writable" + "file" ], "type": "object" }, - "MaintenanceLoginDto": { + "CreateProfileImageResponseDto": { "properties": { - "token": { - "description": "Maintenance token", + "profileChangedAt": { + "description": "Profile image change date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "profileImagePath": { + "description": "Profile image file path", + "type": "string" + }, + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, + "required": [ + "profileChangedAt", + "profileImagePath", + "userId" + ], "type": "object" }, - "MaintenanceStatusResponseDto": { + "CropParameters": { "properties": { - "action": { - "$ref": "#/components/schemas/MaintenanceAction" - }, - "active": { - "type": "boolean" + "height": { + "description": "Height of the crop", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" }, - "error": { - "type": "string" + "width": { + "description": "Width of the crop", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" }, - "progress": { + "x": { + "description": "Top-Left X coordinate of crop", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" }, - "task": { - "type": "string" + "y": { + "description": "Top-Left Y coordinate of crop", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" } }, "required": [ - "action", - "active" + "height", + "width", + "x", + "y" ], "type": "object" }, - "ManualJobName": { - "description": "Manual job name", - "enum": [ - "person-cleanup", - "tag-cleanup", - "user-cleanup", - "memory-cleanup", - "memory-create", - "backup-database", - "integrity-missing-files", - "integrity-untracked-files", - "integrity-checksum-mismatch", - "integrity-missing-files-refresh", - "integrity-untracked-files-refresh", - "integrity-checksum-mismatch-refresh", - "integrity-missing-files-delete-all", - "integrity-untracked-files-delete-all", - "integrity-checksum-mismatch-delete-all" - ], - "type": "string" - }, - "MapMarkerResponseDto": { + "DatabaseBackupDeleteDto": { "properties": { - "city": { - "description": "City name", - "nullable": true, - "type": "string" - }, - "country": { - "description": "Country name", - "nullable": true, - "type": "string" - }, - "id": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "lat": { - "description": "Latitude", - "format": "double", - "type": "number" - }, - "lon": { - "description": "Longitude", - "format": "double", - "type": "number" - }, - "state": { - "description": "State/Province name", - "nullable": true, - "type": "string" + "backups": { + "description": "Backup filenames to delete", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ - "city", - "country", - "id", - "lat", - "lon", - "state" + "backups" ], "type": "object" }, - "MapReverseGeocodeResponseDto": { + "DatabaseBackupDto": { "properties": { - "city": { - "description": "City name", - "nullable": true, + "filename": { + "description": "Backup filename", "type": "string" }, - "country": { - "description": "Country name", - "nullable": true, - "type": "string" + "filesize": { + "description": "Backup file size", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "state": { - "description": "State/Province name", - "nullable": true, + "timezone": { + "description": "Backup timezone", "type": "string" } }, "required": [ - "city", - "country", - "state" + "filename", + "filesize", + "timezone" ], "type": "object" }, - "MemoriesResponse": { + "DatabaseBackupListResponseDto": { "properties": { - "duration": { - "description": "Memory duration in seconds", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "enabled": { - "description": "Whether memories are enabled", - "type": "boolean" + "backups": { + "description": "List of backups", + "items": { + "$ref": "#/components/schemas/DatabaseBackupDto" + }, + "type": "array" } }, "required": [ - "duration", - "enabled" + "backups" ], "type": "object" }, - "MemoriesUpdate": { - "properties": { - "duration": { - "description": "Memory duration in seconds", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - }, - "enabled": { - "description": "Whether memories are enabled", - "type": "boolean" + "DatabaseBackupUploadDto": { + "properties": { + "file": { + "description": "Database backup file", + "format": "binary", + "type": "string" } }, "type": "object" }, - "MemoryCreateDto": { + "DownloadArchiveDto": { "properties": { "assetIds": { - "description": "Asset IDs to associate with memory", + "description": "Asset IDs", "items": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", @@ -20005,241 +20803,153 @@ }, "type": "array" }, - "data": { - "$ref": "#/components/schemas/OnThisDayDto" - }, - "hideAt": { - "description": "Date when memory should be hidden", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string", - "x-immich-history": [ - { - "version": "v2.6.0", - "state": "Added" - }, - { - "version": "v2.6.0", - "state": "Stable" - } - ], - "x-immich-state": "Stable" - }, - "isSaved": { - "description": "Is memory saved", + "edited": { + "description": "Download edited asset if available", "type": "boolean" - }, - "memoryAt": { - "description": "Memory date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "seenAt": { - "description": "Date when memory was seen", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "showAt": { - "description": "Date when memory should be shown", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string", - "x-immich-history": [ - { - "version": "v2.6.0", - "state": "Added" - }, - { - "version": "v2.6.0", - "state": "Stable" - } - ], - "x-immich-state": "Stable" - }, - "type": { - "$ref": "#/components/schemas/MemoryType" } }, "required": [ - "data", - "memoryAt", - "type" + "assetIds" ], "type": "object" }, - "MemoryResponseDto": { + "DownloadArchiveInfo": { "properties": { - "assets": { + "assetIds": { + "description": "Asset IDs in this archive", "items": { - "$ref": "#/components/schemas/AssetResponseDto" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, "type": "array" }, - "createdAt": { - "description": "Creation date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "data": { - "$ref": "#/components/schemas/OnThisDayDto" - }, - "deletedAt": { - "description": "Deletion date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "hideAt": { - "description": "Date when memory should be hidden", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "id": { - "description": "Memory ID", + "size": { + "description": "Archive size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + } + }, + "required": [ + "assetIds", + "size" + ], + "type": "object" + }, + "DownloadInfoDto": { + "properties": { + "albumId": { + "description": "Album ID to download", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "isSaved": { - "description": "Is memory saved", - "type": "boolean" + "archiveSize": { + "description": "Archive size limit in bytes", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" }, - "memoryAt": { - "description": "Memory date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "assetIds": { + "description": "Asset IDs to download", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" }, - "ownerId": { - "description": "Owner user ID", + "userId": { + "description": "User ID to download assets from", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" + } + }, + "type": "object" + }, + "DownloadResponse": { + "properties": { + "archiveSize": { + "description": "Maximum archive size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "seenAt": { - "description": "Date when memory was seen", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "showAt": { - "description": "Date when memory should be shown", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/MemoryType" - }, - "updatedAt": { - "description": "Last update date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "includeEmbeddedVideos": { + "description": "Whether to include embedded videos in downloads", + "type": "boolean" } }, "required": [ - "assets", - "createdAt", - "data", - "id", - "isSaved", - "memoryAt", - "ownerId", - "type", - "updatedAt" + "archiveSize", + "includeEmbeddedVideos" ], "type": "object" }, - "MemorySearchOrder": { - "description": "Sort order", - "enum": [ - "asc", - "desc", - "random" - ], - "type": "string" - }, - "MemoryStatisticsResponseDto": { + "DownloadResponseDto": { "properties": { - "total": { - "description": "Total number of memories", + "archives": { + "description": "Archive information", + "items": { + "$ref": "#/components/schemas/DownloadArchiveInfo" + }, + "type": "array" + }, + "totalSize": { + "description": "Total size in bytes", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" } }, "required": [ - "total" + "archives", + "totalSize" ], "type": "object" }, - "MemoryType": { - "description": "Memory type", - "enum": [ - "on_this_day" - ], - "type": "string" - }, - "MemoryUpdateDto": { + "DownloadUpdate": { "properties": { - "isSaved": { - "description": "Is memory saved", - "type": "boolean" - }, - "memoryAt": { - "description": "Memory date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "archiveSize": { + "description": "Maximum archive size in bytes", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" }, - "seenAt": { - "description": "Date when memory was seen", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "includeEmbeddedVideos": { + "description": "Whether to include embedded videos in downloads", + "type": "boolean" } }, "type": "object" }, - "MergePersonDto": { + "DuplicateResolveDto": { "properties": { - "ids": { - "description": "Person IDs to merge", + "groups": { + "description": "List of duplicate groups to resolve", "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "$ref": "#/components/schemas/DuplicateResolveGroupDto" }, + "minItems": 1, "type": "array" } }, "required": [ - "ids" + "groups" ], "type": "object" }, - "MetadataSearchDto": { + "DuplicateResolveGroupDto": { "properties": { - "albumIds": { - "description": "Filter by album IDs", + "duplicateId": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "keepAssetIds": { + "description": "Asset IDs to keep", "items": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", @@ -20247,328 +20957,469 @@ }, "type": "array" }, - "checksum": { - "description": "Filter by file checksum", - "type": "string" - }, - "city": { - "description": "Filter by city name", - "nullable": true, - "type": "string" - }, - "country": { - "description": "Filter by country name", - "nullable": true, - "type": "string" - }, - "createdAfter": { - "description": "Filter by creation date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "createdBefore": { - "description": "Filter by creation date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "description": { - "description": "Filter by description text", - "type": "string" - }, - "encodedVideoPath": { - "description": "Filter by encoded video file path", - "type": "string" + "trashAssetIds": { + "description": "Asset IDs to trash or delete", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "duplicateId", + "keepAssetIds", + "trashAssetIds" + ], + "type": "object" + }, + "DuplicateResponseDto": { + "properties": { + "assets": { + "description": "Duplicate assets", + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" }, - "id": { - "description": "Filter by asset ID", + "duplicateId": { + "description": "Duplicate group ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "isEncoded": { - "description": "Filter by encoded status", + "suggestedKeepAssetIds": { + "description": "Suggested asset IDs to keep based on file size and EXIF data", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "assets", + "duplicateId", + "suggestedKeepAssetIds" + ], + "type": "object" + }, + "EmailNotificationsResponse": { + "properties": { + "albumInvite": { + "description": "Whether to receive email notifications for album invites", "type": "boolean" }, - "isFavorite": { - "description": "Filter by favorite status", + "albumUpdate": { + "description": "Whether to receive email notifications for album updates", "type": "boolean" }, - "isMotion": { - "description": "Filter by motion photo status", + "enabled": { + "description": "Whether email notifications are enabled", "type": "boolean" - }, - "isNotInAlbum": { - "description": "Filter assets not in any album", + } + }, + "required": [ + "albumInvite", + "albumUpdate", + "enabled" + ], + "type": "object" + }, + "EmailNotificationsUpdate": { + "properties": { + "albumInvite": { + "description": "Whether to receive email notifications for album invites", "type": "boolean" }, - "isOffline": { - "description": "Filter by offline status", + "albumUpdate": { + "description": "Whether to receive email notifications for album updates", "type": "boolean" }, - "lensModel": { - "description": "Filter by lens model", + "enabled": { + "description": "Whether email notifications are enabled", + "type": "boolean" + } + }, + "type": "object" + }, + "ExifResponseDto": { + "description": "EXIF response", + "properties": { + "city": { + "default": null, + "description": "City name", "nullable": true, "type": "string" }, - "libraryId": { - "description": "Library ID to filter by", - "format": "uuid", + "country": { + "default": null, + "description": "Country name", "nullable": true, - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "make": { - "description": "Filter by camera make", + "dateTimeOriginal": { + "default": null, + "description": "Original date/time", + "format": "date-time", "nullable": true, "type": "string" }, - "model": { - "description": "Filter by camera model", + "description": { + "default": null, + "description": "Image description", "nullable": true, "type": "string" }, - "ocr": { - "description": "Filter by OCR text content", - "type": "string" - }, - "order": { - "$ref": "#/components/schemas/AssetOrder", - "default": "desc", - "description": "Sort order" - }, - "originalFileName": { - "description": "Filter by original file name", - "type": "string" - }, - "originalPath": { - "description": "Filter by original file path", - "type": "string" - }, - "page": { - "description": "Page number", + "exifImageHeight": { + "default": null, + "description": "Image height in pixels", "maximum": 9007199254740991, - "minimum": 1, + "minimum": 0, + "nullable": true, "type": "integer" }, - "personIds": { - "description": "Filter by person IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "exifImageWidth": { + "default": null, + "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer" }, - "previewPath": { - "description": "Filter by preview file path", + "exposureTime": { + "default": null, + "description": "Exposure time", + "nullable": true, "type": "string" }, - "rating": { - "description": "Filter by rating [1-5], or null for unrated", - "maximum": 5, - "minimum": 1, + "fNumber": { + "default": null, + "description": "F-number (aperture)", "nullable": true, - "type": "integer", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - }, - { - "version": "v2.6.0", - "state": "Updated", - "description": "Using -1 as a rating is deprecated and will be removed in the next major version." - }, - { - "version": "v3", - "state": "Updated", - "description": "Using -1 as a rating is no longer valid." - } - ], - "x-immich-state": "Stable" + "type": "number" + }, + "fileSizeInByte": { + "default": null, + "description": "File size in bytes", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer" }, - "size": { - "description": "Number of results to return", - "maximum": 1000, - "minimum": 1, + "focalLength": { + "default": null, + "description": "Focal length in mm", + "nullable": true, + "type": "number" + }, + "iso": { + "default": null, + "description": "ISO sensitivity", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "nullable": true, "type": "integer" }, - "state": { - "description": "Filter by state/province name", + "latitude": { + "default": null, + "description": "GPS latitude", + "nullable": true, + "type": "number" + }, + "lensModel": { + "default": null, + "description": "Lens model", "nullable": true, "type": "string" }, - "tagIds": { - "description": "Filter by tag IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, + "longitude": { + "default": null, + "description": "GPS longitude", "nullable": true, - "type": "array" + "type": "number" }, - "takenAfter": { - "description": "Filter by taken date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "make": { + "default": null, + "description": "Camera make", + "nullable": true, "type": "string" }, - "takenBefore": { - "description": "Filter by taken date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "model": { + "default": null, + "description": "Camera model", + "nullable": true, "type": "string" }, - "thumbnailPath": { - "description": "Filter by thumbnail file path", + "modifyDate": { + "default": null, + "description": "Modification date/time", + "format": "date-time", + "nullable": true, "type": "string" }, - "trashedAfter": { - "description": "Filter by trash date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "orientation": { + "default": null, + "description": "Image orientation", + "nullable": true, "type": "string" }, - "trashedBefore": { - "description": "Filter by trash date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "projectionType": { + "default": null, + "description": "Projection type", + "nullable": true, "type": "string" }, - "type": { - "$ref": "#/components/schemas/AssetTypeEnum" + "rating": { + "default": null, + "description": "Rating", + "maximum": 5, + "minimum": 1, + "nullable": true, + "type": "integer" }, - "updatedAfter": { - "description": "Filter by update date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "state": { + "default": null, + "description": "State/province name", + "nullable": true, "type": "string" }, - "updatedBefore": { - "description": "Filter by update date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "timeZone": { + "default": null, + "description": "Time zone", + "nullable": true, "type": "string" - }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" - }, - "withDeleted": { - "description": "Include deleted assets", + } + }, + "type": "object" + }, + "FaceDto": { + "properties": { + "id": { + "description": "Face ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "FoldersResponse": { + "properties": { + "enabled": { + "description": "Whether folders are enabled", "type": "boolean" }, - "withExif": { - "description": "Include EXIF data in response", + "sidebarWeb": { + "description": "Whether folders appear in web sidebar", "type": "boolean" - }, - "withPeople": { - "description": "Include people data in response", + } + }, + "required": [ + "enabled", + "sidebarWeb" + ], + "type": "object" + }, + "FoldersUpdate": { + "properties": { + "enabled": { + "description": "Whether folders are enabled", "type": "boolean" }, - "withStacked": { - "description": "Include stacked assets", + "sidebarWeb": { + "description": "Whether folders appear in web sidebar", "type": "boolean" } }, "type": "object" }, - "MirrorAxis": { - "description": "Axis to mirror along", + "HlsVideoResolution": { + "description": "HLS video resolution", "enum": [ - "horizontal", - "vertical" + 480, + 720, + 1080, + 1440, + 2160 + ], + "type": "integer" + }, + "ImageFormat": { + "description": "Image format", + "enum": [ + "jpeg", + "webp" ], "type": "string" }, - "MirrorParameters": { + "IntegrityReport": { + "description": "Integrity report type", + "enum": [ + "untracked_file", + "missing_file", + "checksum_mismatch" + ], + "type": "string" + }, + "IntegrityReportResponseDto": { "properties": { - "axis": { - "$ref": "#/components/schemas/MirrorAxis" + "items": { + "items": { + "properties": { + "id": { + "description": "Integrity report item id", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "path": { + "description": "Integrity report item path", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/IntegrityReport" + } + }, + "required": [ + "id", + "type", + "path" + ], + "type": "object" + }, + "type": "array" + }, + "nextCursor": { + "type": "string" } }, "required": [ - "axis" + "items" ], "type": "object" }, - "NotificationCreateDto": { + "IntegrityReportSummaryResponseDto": { "properties": { - "data": { - "additionalProperties": {}, - "description": "Additional notification data", - "type": "object" - }, - "description": { - "description": "Notification description", - "nullable": true, - "type": "string" - }, - "level": { - "$ref": "#/components/schemas/NotificationLevel" - }, - "readAt": { - "description": "Date when notification was read", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "title": { - "description": "Notification title", - "type": "string" + "checksum_mismatch": { + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" }, - "type": { - "$ref": "#/components/schemas/NotificationType" + "missing_file": { + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" }, - "userId": { - "description": "User ID to send notification to", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "untracked_file": { + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" } }, "required": [ - "title", - "userId" + "checksum_mismatch", + "missing_file", + "untracked_file" ], "type": "object" }, - "NotificationDeleteAllDto": { + "JobCreateDto": { "properties": { - "ids": { - "description": "Notification IDs to delete", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "minItems": 1, - "type": "array" + "name": { + "$ref": "#/components/schemas/ManualJobName" } }, "required": [ - "ids" + "name" ], "type": "object" }, - "NotificationDto": { + "JobName": { + "description": "Job name", + "enum": [ + "AssetDelete", + "AssetDeleteCheck", + "AssetDetectFacesQueueAll", + "AssetDetectFaces", + "AssetDetectDuplicatesQueueAll", + "AssetDetectDuplicates", + "AssetEditThumbnailGeneration", + "AssetEncodeVideoQueueAll", + "AssetEncodeVideo", + "AssetEmptyTrash", + "AssetExtractMetadataQueueAll", + "AssetExtractMetadata", + "AssetFileMigration", + "AssetGenerateThumbnailsQueueAll", + "AssetGenerateThumbnails", + "AuditTableCleanup", + "DatabaseBackup", + "FacialRecognitionQueueAll", + "FacialRecognition", + "FileDelete", + "FileMigrationQueueAll", + "LibraryDeleteCheck", + "LibraryDelete", + "LibraryRemoveAsset", + "LibraryScanAssetsQueueAll", + "LibrarySyncAssets", + "LibrarySyncFilesQueueAll", + "LibrarySyncFiles", + "LibraryScanQueueAll", + "HlsSessionCleanup", + "MemoryCleanup", + "MemoryGenerate", + "NotificationsCleanup", + "NotifyUserSignup", + "NotifyAlbumInvite", + "NotifyAlbumUpdate", + "UserDelete", + "UserDeleteCheck", + "UserSyncUsage", + "PersonCleanup", + "PersonFileMigration", + "PersonGenerateThumbnail", + "SessionCleanup", + "SendMail", + "SidecarQueueAll", + "SidecarCheck", + "SidecarWrite", + "SmartSearchQueueAll", + "SmartSearch", + "StorageTemplateMigration", + "StorageTemplateMigrationSingle", + "TagCleanup", + "VersionCheck", + "OcrQueueAll", + "Ocr", + "WorkflowAssetTrigger", + "IntegrityUntrackedFilesQueueAll", + "IntegrityUntrackedFiles", + "IntegrityUntrackedRefresh", + "IntegrityMissingFilesQueueAll", + "IntegrityMissingFiles", + "IntegrityMissingFilesRefresh", + "IntegrityChecksumFiles", + "IntegrityChecksumFilesRefresh", + "IntegrityDeleteReportType", + "IntegrityDeleteReports" + ], + "type": "string" + }, + "LibraryResponseDto": { "properties": { + "assetCount": { + "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, "createdAt": { "description": "Creation date", "example": "2024-01-01T00:00:00.000Z", @@ -20576,4656 +21427,5195 @@ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "data": { - "additionalProperties": {}, - "description": "Additional notification data", - "type": "object" - }, - "description": { - "description": "Notification description", - "type": "string" + "exclusionPatterns": { + "description": "Exclusion patterns", + "items": { + "type": "string" + }, + "type": "array" }, "id": { - "description": "Notification ID", + "description": "Library ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "level": { - "$ref": "#/components/schemas/NotificationLevel" + "importPaths": { + "description": "Import paths", + "items": { + "type": "string" + }, + "type": "array" }, - "readAt": { - "description": "Date when notification was read", + "name": { + "description": "Library name", + "type": "string" + }, + "ownerId": { + "description": "Owner user ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "refreshedAt": { + "description": "Last refresh date", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "title": { - "description": "Notification title", + "updatedAt": { + "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - }, - "type": { - "$ref": "#/components/schemas/NotificationType" } }, "required": [ + "assetCount", "createdAt", + "exclusionPatterns", "id", - "level", - "title", - "type" + "importPaths", + "name", + "ownerId", + "refreshedAt", + "updatedAt" ], "type": "object" }, - "NotificationLevel": { - "description": "Notification level", - "enum": [ - "success", - "error", - "warning", - "info" - ], - "type": "string" - }, - "NotificationType": { - "description": "Notification type", - "enum": [ - "JobFailed", - "BackupFailed", - "SystemMessage", - "AlbumInvite", - "AlbumUpdate", - "Custom" - ], - "type": "string" - }, - "NotificationUpdateAllDto": { + "LibraryStatsResponseDto": { "properties": { - "ids": { - "description": "Notification IDs to update", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "minItems": 1, - "type": "array" + "photos": { + "description": "Number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "readAt": { - "description": "Date when notifications were read", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "total": { + "description": "Total number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "usage": { + "description": "Storage usage in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "videos": { + "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" } }, "required": [ - "ids" + "photos", + "total", + "usage", + "videos" ], "type": "object" }, - "NotificationUpdateDto": { - "properties": { - "readAt": { - "description": "Date when notification was read", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - } - }, - "type": "object" - }, - "OAuthAuthorizeResponseDto": { - "properties": { - "url": { - "description": "OAuth authorization URL", + "LicenseKeyDto": { + "properties": { + "activationKey": { + "description": "Activation key", + "type": "string" + }, + "licenseKey": { + "description": "License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/)", + "pattern": "^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$", "type": "string" } }, "required": [ - "url" + "activationKey", + "licenseKey" ], "type": "object" }, - "OAuthBackchannelLogoutDto": { + "LicenseResponseDto": { + "$ref": "#/components/schemas/UserLicense" + }, + "LogLevel": { + "description": "Log level", + "enum": [ + "verbose", + "debug", + "log", + "warn", + "error", + "fatal" + ], + "type": "string" + }, + "LoginCredentialDto": { "properties": { - "logout_token": { - "description": "OAuth logout token", + "email": { + "description": "User email", + "example": "testuser@email.com", + "format": "email", + "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", + "type": "string" + }, + "password": { + "description": "User password", + "example": "password", "type": "string" } }, "required": [ - "logout_token" + "email", + "password" ], "type": "object" }, - "OAuthCallbackDto": { + "LoginResponseDto": { "properties": { - "codeVerifier": { - "description": "OAuth code verifier (PKCE)", + "accessToken": { + "description": "Access token", "type": "string" }, - "state": { - "description": "OAuth state parameter", + "isAdmin": { + "description": "Is admin user", + "type": "boolean" + }, + "isOnboarded": { + "description": "Is onboarded", + "type": "boolean" + }, + "name": { + "description": "User name", "type": "string" }, - "url": { - "description": "OAuth callback URL", - "minLength": 1, + "profileImagePath": { + "description": "Profile image path", + "type": "string" + }, + "shouldChangePassword": { + "description": "Should change password", + "type": "boolean" + }, + "userEmail": { + "description": "User email", + "format": "email", + "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", + "type": "string" + }, + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, "required": [ - "url" + "accessToken", + "isAdmin", + "isOnboarded", + "name", + "profileImagePath", + "shouldChangePassword", + "userEmail", + "userId" ], "type": "object" }, - "OAuthConfigDto": { + "LogoutResponseDto": { "properties": { - "codeChallenge": { - "description": "OAuth code challenge (PKCE)", - "type": "string" - }, "redirectUri": { - "description": "OAuth redirect URI", + "description": "Redirect URI", "type": "string" }, - "state": { - "description": "OAuth state parameter", - "type": "string" + "successful": { + "description": "Logout successful", + "type": "boolean" } }, "required": [ - "redirectUri" + "redirectUri", + "successful" ], "type": "object" }, - "OAuthTokenEndpointAuthMethod": { - "description": "OAuth token endpoint auth method", + "MaintenanceAction": { + "description": "Maintenance action", "enum": [ - "client_secret_post", - "client_secret_basic" + "start", + "end", + "select_database_restore", + "restore_database" ], "type": "string" }, - "OcrConfig": { + "MaintenanceAuthDto": { "properties": { - "enabled": { - "description": "Whether the task is enabled", - "type": "boolean" - }, - "maxResolution": { - "description": "Maximum resolution for OCR processing", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - }, - "minDetectionScore": { - "description": "Minimum confidence score for text detection", - "format": "double", - "maximum": 1, - "minimum": 0.1, - "type": "number" - }, - "minRecognitionScore": { - "description": "Minimum confidence score for text recognition", - "format": "double", - "maximum": 1, - "minimum": 0.1, - "type": "number" - }, - "modelName": { - "description": "Name of the model to use", + "username": { + "description": "Maintenance username", "type": "string" } }, "required": [ - "enabled", - "maxResolution", - "minDetectionScore", - "minRecognitionScore", - "modelName" + "username" ], "type": "object" }, - "OnThisDayDto": { + "MaintenanceDetectInstallResponseDto": { "properties": { - "year": { - "description": "Year for on this day memory", - "maximum": 9999, - "minimum": 1000, - "type": "integer" + "storage": { + "items": { + "$ref": "#/components/schemas/MaintenanceDetectInstallStorageFolderDto" + }, + "type": "array" } }, "required": [ - "year" + "storage" ], "type": "object" }, - "OnboardingDto": { + "MaintenanceDetectInstallStorageFolderDto": { "properties": { - "isOnboarded": { - "description": "Is user onboarded", + "files": { + "description": "Number of files in the folder", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "folder": { + "$ref": "#/components/schemas/StorageFolder" + }, + "readable": { + "description": "Whether the folder is readable", + "type": "boolean" + }, + "writable": { + "description": "Whether the folder is writable", "type": "boolean" } }, "required": [ - "isOnboarded" + "files", + "folder", + "readable", + "writable" ], "type": "object" }, - "OnboardingResponseDto": { + "MaintenanceLoginDto": { "properties": { - "isOnboarded": { - "description": "Is user onboarded", - "type": "boolean" + "token": { + "description": "Maintenance token", + "type": "string" } }, - "required": [ - "isOnboarded" - ], "type": "object" }, - "PartnerCreateDto": { + "MaintenanceStatusResponseDto": { "properties": { - "sharedWithId": { - "description": "User ID to share with", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "action": { + "$ref": "#/components/schemas/MaintenanceAction" + }, + "active": { + "type": "boolean" + }, + "error": { + "type": "string" + }, + "progress": { + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "task": { "type": "string" } }, "required": [ - "sharedWithId" + "action", + "active" ], "type": "object" }, - "PartnerDirection": { - "description": "Partner direction", + "ManualJobName": { + "description": "Manual job name", "enum": [ - "shared-by", - "shared-with" + "person-cleanup", + "tag-cleanup", + "user-cleanup", + "memory-cleanup", + "memory-create", + "backup-database", + "integrity-missing-files", + "integrity-untracked-files", + "integrity-checksum-mismatch", + "integrity-missing-files-refresh", + "integrity-untracked-files-refresh", + "integrity-checksum-mismatch-refresh", + "integrity-missing-files-delete-all", + "integrity-untracked-files-delete-all", + "integrity-checksum-mismatch-delete-all" ], "type": "string" }, - "PartnerResponseDto": { - "description": "Partner response", + "MapMarkerResponseDto": { "properties": { - "avatarColor": { - "$ref": "#/components/schemas/UserAvatarColor" + "city": { + "description": "City name", + "nullable": true, + "type": "string" }, - "email": { - "description": "User email", - "format": "email", - "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", + "country": { + "description": "Country name", + "nullable": true, "type": "string" }, "id": { - "description": "User ID", + "description": "Asset ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "inTimeline": { - "description": "Show in timeline", - "type": "boolean" + "lat": { + "description": "Latitude", + "format": "double", + "type": "number" }, - "name": { - "description": "User name", + "lon": { + "description": "Longitude", + "format": "double", + "type": "number" + }, + "state": { + "description": "State/Province name", + "nullable": true, + "type": "string" + } + }, + "required": [ + "city", + "country", + "id", + "lat", + "lon", + "state" + ], + "type": "object" + }, + "MapReverseGeocodeResponseDto": { + "properties": { + "city": { + "description": "City name", + "nullable": true, "type": "string" }, - "profileChangedAt": { - "description": "Profile change date", - "format": "date-time", + "country": { + "description": "Country name", + "nullable": true, "type": "string" }, - "profileImagePath": { - "description": "Profile image path", + "state": { + "description": "State/Province name", + "nullable": true, "type": "string" } }, "required": [ - "avatarColor", - "email", - "id", - "name", - "profileChangedAt", - "profileImagePath" + "city", + "country", + "state" ], "type": "object" }, - "PartnerUpdateDto": { + "MemoriesResponse": { "properties": { - "inTimeline": { - "description": "Show partner assets in timeline", + "duration": { + "description": "Memory duration in seconds", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "enabled": { + "description": "Whether memories are enabled", "type": "boolean" } }, "required": [ - "inTimeline" + "duration", + "enabled" ], "type": "object" }, - "PeopleResponse": { + "MemoriesUpdate": { "properties": { - "enabled": { - "description": "Whether people are enabled", - "type": "boolean" - }, - "minimumFaces": { - "description": "People face threshold", + "duration": { + "description": "Memory duration in seconds", "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, - "sidebarWeb": { - "description": "Whether people appear in web sidebar", + "enabled": { + "description": "Whether memories are enabled", "type": "boolean" } }, - "required": [ - "enabled", - "sidebarWeb" - ], "type": "object" }, - "PeopleResponseDto": { - "description": "People response", + "MemoryCreateDto": { "properties": { - "hasNextPage": { - "description": "Whether there are more pages", - "type": "boolean", + "assetIds": { + "description": "Asset IDs to associate with memory", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "data": { + "$ref": "#/components/schemas/OnThisDayDto" + }, + "hideAt": { + "description": "Date when memory should be hidden", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string", "x-immich-history": [ { - "version": "v1.110.0", + "version": "v2.6.0", "state": "Added" }, { - "version": "v2", + "version": "v2.6.0", "state": "Stable" } ], "x-immich-state": "Stable" }, - "hidden": { - "description": "Number of hidden people", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "isSaved": { + "description": "Is memory saved", + "type": "boolean" }, - "people": { - "items": { - "$ref": "#/components/schemas/PersonResponseDto" - }, - "type": "array" + "memoryAt": { + "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "total": { - "description": "Total number of people", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "hidden", - "people", - "total" - ], - "type": "object" - }, - "PeopleUpdate": { - "properties": { - "enabled": { - "description": "Whether people are enabled", - "type": "boolean" + "seenAt": { + "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "minimumFaces": { - "description": "People face threshold", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" + "showAt": { + "description": "Date when memory should be shown", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string", + "x-immich-history": [ + { + "version": "v2.6.0", + "state": "Added" + }, + { + "version": "v2.6.0", + "state": "Stable" + } + ], + "x-immich-state": "Stable" }, - "sidebarWeb": { - "description": "Whether people appear in web sidebar", - "type": "boolean" + "type": { + "$ref": "#/components/schemas/MemoryType" } }, + "required": [ + "data", + "memoryAt", + "type" + ], "type": "object" }, - "PeopleUpdateDto": { + "MemoryResponseDto": { "properties": { - "people": { - "description": "People to update", + "assets": { "items": { - "$ref": "#/components/schemas/PeopleUpdateItem" + "$ref": "#/components/schemas/AssetResponseDto" }, "type": "array" - } - }, - "required": [ - "people" - ], - "type": "object" - }, - "PeopleUpdateItem": { - "properties": { - "birthDate": { - "description": "Person date of birth", - "format": "date", - "nullable": true, + }, + "createdAt": { + "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "color": { - "description": "Person color (hex)", - "nullable": true, - "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", + "data": { + "$ref": "#/components/schemas/OnThisDayDto" + }, + "deletedAt": { + "description": "Deletion date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "hideAt": { + "description": "Date when memory should be hidden", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "id": { + "description": "Memory ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "isSaved": { + "description": "Is memory saved", + "type": "boolean" + }, + "memoryAt": { + "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "featureFaceAssetId": { - "description": "Asset ID used for feature face thumbnail", + "ownerId": { + "description": "Owner user ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "id": { - "description": "Person ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "seenAt": { + "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "isFavorite": { - "description": "Mark as favorite", - "type": "boolean" + "showAt": { + "description": "Date when memory should be shown", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "isHidden": { - "description": "Person visibility (hidden)", - "type": "boolean" + "type": { + "$ref": "#/components/schemas/MemoryType" }, - "name": { - "description": "Person name", + "updatedAt": { + "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" } }, "required": [ - "id" + "assets", + "createdAt", + "data", + "id", + "isSaved", + "memoryAt", + "ownerId", + "type", + "updatedAt" ], "type": "object" }, - "Permission": { - "description": "List of permissions", + "MemorySearchOrder": { + "description": "Sort order", "enum": [ - "all", - "activity.create", - "activity.read", - "activity.update", - "activity.delete", - "activity.statistics", - "apiKey.create", - "apiKey.read", - "apiKey.update", - "apiKey.delete", - "apiKey.rotate", - "asset.read", - "asset.update", - "asset.delete", - "asset.statistics", - "asset.share", - "asset.view", - "asset.download", - "asset.upload", - "asset.copy", - "asset.derive", - "asset.edit.get", - "asset.edit.create", - "asset.edit.delete", - "album.create", - "album.read", - "album.update", - "album.delete", - "album.statistics", - "album.share", - "album.download", - "albumAsset.create", - "albumAsset.delete", - "albumUser.create", - "albumUser.update", - "albumUser.delete", - "auth.changePassword", - "authDevice.delete", - "archive.read", - "backup.list", - "backup.download", - "backup.upload", - "backup.delete", - "duplicate.read", - "duplicate.delete", - "face.create", - "face.read", - "face.update", - "face.delete", - "folder.read", - "job.create", - "job.read", - "library.create", - "library.read", - "library.update", - "library.delete", - "library.statistics", - "timeline.read", - "timeline.download", - "maintenance", - "map.read", - "map.search", - "memory.create", - "memory.read", - "memory.update", - "memory.delete", - "memory.statistics", - "memoryAsset.create", - "memoryAsset.delete", - "notification.create", - "notification.read", - "notification.update", - "notification.delete", - "partner.create", - "partner.read", - "partner.update", - "partner.delete", - "person.create", - "person.read", - "person.update", - "person.delete", - "person.statistics", - "person.merge", - "person.reassign", - "pinCode.create", - "pinCode.update", - "pinCode.delete", - "plugin.create", - "plugin.read", - "plugin.update", - "plugin.delete", - "server.about", - "server.apkLinks", - "server.storage", - "server.statistics", - "server.versionCheck", - "serverLicense.read", - "serverLicense.update", - "serverLicense.delete", - "session.create", - "session.read", - "session.update", - "session.delete", - "session.lock", - "sharedLink.create", - "sharedLink.read", - "sharedLink.update", - "sharedLink.delete", - "stack.create", - "stack.read", - "stack.update", - "stack.delete", - "sync.stream", - "syncCheckpoint.read", - "syncCheckpoint.update", - "syncCheckpoint.delete", - "systemConfig.read", - "systemConfig.update", - "systemMetadata.read", - "systemMetadata.update", - "tag.create", - "tag.read", - "tag.update", - "tag.delete", - "tag.asset", - "user.read", - "user.update", - "userLicense.create", - "userLicense.read", - "userLicense.update", - "userLicense.delete", - "userOnboarding.read", - "userOnboarding.update", - "userOnboarding.delete", - "userPreference.read", - "userPreference.update", - "userProfileImage.create", - "userProfileImage.read", - "userProfileImage.update", - "userProfileImage.delete", - "queue.read", - "queue.update", - "queueJob.create", - "queueJob.read", - "queueJob.update", - "queueJob.delete", - "workflow.create", - "workflow.read", - "workflow.update", - "workflow.delete", - "workflow.logs", - "adminUser.create", - "adminUser.read", - "adminUser.update", - "adminUser.delete", - "adminSession.read", - "adminAuth.unlinkAll" + "asc", + "desc", + "random" + ], + "type": "string" + }, + "MemoryStatisticsResponseDto": { + "properties": { + "total": { + "description": "Total number of memories", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + } + }, + "required": [ + "total" + ], + "type": "object" + }, + "MemoryType": { + "description": "Memory type", + "enum": [ + "on_this_day" + ], + "type": "string" + }, + "MemoryUpdateDto": { + "properties": { + "isSaved": { + "description": "Is memory saved", + "type": "boolean" + }, + "memoryAt": { + "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "seenAt": { + "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + } + }, + "type": "object" + }, + "MergePersonDto": { + "properties": { + "ids": { + "description": "Person IDs to merge", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "ids" ], - "type": "string" + "type": "object" }, - "PersonCreateDto": { + "MetadataSearchDto": { "properties": { - "birthDate": { - "description": "Person date of birth", - "format": "date", - "nullable": true, + "albumIds": { + "description": "Filter by album IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "checksum": { + "description": "Filter by file checksum", "type": "string" }, - "color": { - "description": "Person color (hex)", + "city": { + "description": "Filter by city name", "nullable": true, - "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, - "isFavorite": { - "description": "Mark as favorite", - "type": "boolean" + "country": { + "description": "Filter by country name", + "nullable": true, + "type": "string" }, - "isHidden": { - "description": "Person visibility (hidden)", - "type": "boolean" + "createdAfter": { + "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "name": { - "description": "Person name", + "createdBefore": { + "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - } - }, - "type": "object" - }, - "PersonResponseDto": { - "properties": { - "birthDate": { - "description": "Person date of birth", - "format": "date", - "nullable": true, + }, + "description": { + "description": "Filter by description text", "type": "string" }, - "color": { - "description": "Person color (hex)", - "type": "string", - "x-immich-history": [ - { - "version": "v1.126.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" + "encodedVideoPath": { + "description": "Filter by encoded video file path", + "type": "string" }, "id": { - "description": "Person ID", + "description": "Filter by asset ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, + "isEncoded": { + "description": "Filter by encoded status", + "type": "boolean" + }, "isFavorite": { - "description": "Is favorite", - "type": "boolean", - "x-immich-history": [ - { - "version": "v1.126.0", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - } - ], - "x-immich-state": "Stable" + "description": "Filter by favorite status", + "type": "boolean" }, - "isHidden": { - "description": "Is hidden", + "isMotion": { + "description": "Filter by motion photo status", "type": "boolean" }, - "name": { - "description": "Person name", + "isNotInAlbum": { + "description": "Filter assets not in any album", + "type": "boolean" + }, + "isOffline": { + "description": "Filter by offline status", + "type": "boolean" + }, + "lensModel": { + "description": "Filter by lens model", + "nullable": true, "type": "string" }, - "thumbnailPath": { - "description": "Thumbnail path", + "libraryId": { + "description": "Library ID to filter by", + "format": "uuid", + "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "updatedAt": { - "description": "Last update date", - "format": "date-time", - "type": "string", + "make": { + "description": "Filter by camera make", + "nullable": true, + "type": "string" + }, + "model": { + "description": "Filter by camera model", + "nullable": true, + "type": "string" + }, + "ocr": { + "description": "Filter by OCR text content", + "type": "string" + }, + "order": { + "$ref": "#/components/schemas/AssetOrder", + "default": "desc", + "description": "Sort order" + }, + "originalFileName": { + "description": "Filter by original file name", + "type": "string" + }, + "originalPath": { + "description": "Filter by original file path", + "type": "string" + }, + "page": { + "description": "Page number", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "personIds": { + "description": "Filter by person IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "previewPath": { + "description": "Filter by preview file path", + "type": "string" + }, + "rating": { + "description": "Filter by rating [1-5], or null for unrated", + "maximum": 5, + "minimum": 1, + "nullable": true, + "type": "integer", "x-immich-history": [ { - "version": "v1.107.0", + "version": "v1", "state": "Added" }, { "version": "v2", "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + }, + { + "version": "v3", + "state": "Updated", + "description": "Using -1 as a rating is no longer valid." } ], "x-immich-state": "Stable" - } - }, - "required": [ - "birthDate", - "id", - "isHidden", - "name", - "thumbnailPath" - ], - "type": "object" - }, - "PersonStatisticsResponseDto": { - "properties": { - "assets": { - "description": "Number of assets", - "maximum": 9007199254740991, - "minimum": -9007199254740991, + }, + "size": { + "description": "Number of results to return", + "maximum": 1000, + "minimum": 1, "type": "integer" - } - }, - "required": [ - "assets" - ], - "type": "object" - }, - "PersonUpdateDto": { - "properties": { - "birthDate": { - "description": "Person date of birth", - "format": "date", + }, + "state": { + "description": "Filter by state/province name", "nullable": true, "type": "string" }, - "color": { - "description": "Person color (hex)", + "tagIds": { + "description": "Filter by tag IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, "nullable": true, - "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", - "type": "string" + "type": "array" }, - "featureFaceAssetId": { - "description": "Asset ID used for feature face thumbnail", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "takenAfter": { + "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "isFavorite": { - "description": "Mark as favorite", - "type": "boolean" - }, - "isHidden": { - "description": "Person visibility (hidden)", - "type": "boolean" - }, - "name": { - "description": "Person name", + "takenBefore": { + "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - } - }, - "type": "object" - }, - "PinCodeChangeDto": { - "properties": { - "newPinCode": { - "description": "New PIN code (4-6 digits)", - "pattern": "^\\d{6}$", + }, + "thumbnailPath": { + "description": "Filter by thumbnail file path", "type": "string" }, - "password": { - "description": "User password (required if PIN code is not provided)", - "example": "password", + "trashedAfter": { + "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "pinCode": { - "description": "New PIN code (4-6 digits)", - "example": "123456", - "pattern": "^\\d{6}$", + "trashedBefore": { + "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - } - }, - "required": [ - "newPinCode" - ], - "type": "object" - }, - "PinCodeResetDto": { - "properties": { - "password": { - "description": "User password (required if PIN code is not provided)", - "example": "password", + }, + "type": { + "$ref": "#/components/schemas/AssetTypeEnum" + }, + "updatedAfter": { + "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "pinCode": { - "description": "New PIN code (4-6 digits)", - "example": "123456", - "pattern": "^\\d{6}$", + "updatedBefore": { + "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" + }, + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" + }, + "withDeleted": { + "description": "Include deleted assets", + "type": "boolean" + }, + "withExif": { + "description": "Include EXIF data in response", + "type": "boolean" + }, + "withPeople": { + "description": "Include people data in response", + "type": "boolean" + }, + "withStacked": { + "description": "Include stacked assets", + "type": "boolean" } }, "type": "object" }, - "PinCodeSetupDto": { + "MirrorAxis": { + "description": "Axis to mirror along", + "enum": [ + "horizontal", + "vertical" + ], + "type": "string" + }, + "MirrorParameters": { "properties": { - "pinCode": { - "description": "PIN code (4-6 digits)", - "example": "123456", - "pattern": "^\\d{6}$", - "type": "string" + "axis": { + "$ref": "#/components/schemas/MirrorAxis" } }, "required": [ - "pinCode" + "axis" ], "type": "object" }, - "PlacesResponseDto": { + "NotificationCreateDto": { "properties": { - "admin1name": { - "description": "Administrative level 1 name (state/province)", + "data": { + "additionalProperties": {}, + "description": "Additional notification data", + "type": "object" + }, + "description": { + "description": "Notification description", + "nullable": true, "type": "string" }, - "admin2name": { - "description": "Administrative level 2 name (county/district)", + "level": { + "$ref": "#/components/schemas/NotificationLevel" + }, + "readAt": { + "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "latitude": { - "description": "Latitude coordinate", - "type": "number" + "title": { + "description": "Notification title", + "type": "string" }, - "longitude": { - "description": "Longitude coordinate", - "type": "number" + "type": { + "$ref": "#/components/schemas/NotificationType" }, - "name": { - "description": "Place name", + "userId": { + "description": "User ID to send notification to", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, "required": [ - "latitude", - "longitude", - "name" + "title", + "userId" ], "type": "object" }, - "PluginMethodResponseDto": { + "NotificationDeleteAllDto": { "properties": { - "description": { - "description": "Description", - "type": "string" - }, - "hostFunctions": { - "type": "boolean" - }, - "key": { - "description": "Key", - "type": "string" - }, - "name": { - "description": "Name", - "type": "string" - }, - "schema": { - "properties": {}, - "type": "object" - }, - "title": { - "description": "Title", - "type": "string" - }, - "types": { - "description": "Workflow types", - "items": { - "$ref": "#/components/schemas/WorkflowType" - }, - "type": "array" - }, - "uiHints": { - "description": "Ui hints", + "ids": { + "description": "Notification IDs to delete", "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, + "minItems": 1, "type": "array" } }, "required": [ - "description", - "hostFunctions", - "key", - "name", - "title", - "types", - "uiHints" + "ids" ], "type": "object" }, - "PluginResponseDto": { + "NotificationDto": { "properties": { - "author": { - "description": "Plugin author", - "type": "string" - }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, + "data": { + "additionalProperties": {}, + "description": "Additional notification data", + "type": "object" + }, "description": { - "description": "Plugin description", + "description": "Notification description", "type": "string" }, "id": { - "description": "Plugin ID", + "description": "Notification ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "methods": { - "description": "Plugin methods", - "items": { - "$ref": "#/components/schemas/PluginMethodResponseDto" - }, - "type": "array" - }, - "name": { - "description": "Plugin name", - "type": "string" - }, - "title": { - "description": "Plugin title", - "type": "string" + "level": { + "$ref": "#/components/schemas/NotificationLevel" }, - "updatedAt": { - "description": "Last update date", + "readAt": { + "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "version": { - "description": "Plugin version", + "title": { + "description": "Notification title", "type": "string" + }, + "type": { + "$ref": "#/components/schemas/NotificationType" } }, "required": [ - "author", "createdAt", - "description", "id", - "methods", - "name", + "level", "title", - "updatedAt", - "version" + "type" ], "type": "object" }, - "PluginTemplateResponseDto": { + "NotificationLevel": { + "description": "Notification level", + "enum": [ + "success", + "error", + "warning", + "info" + ], + "type": "string" + }, + "NotificationType": { + "description": "Notification type", + "enum": [ + "JobFailed", + "BackupFailed", + "SystemMessage", + "AlbumInvite", + "AlbumUpdate", + "ClusterGroupRequest", + "Custom" + ], + "type": "string" + }, + "NotificationUpdateAllDto": { "properties": { - "description": { - "description": "Template description", - "type": "string" - }, - "key": { - "description": "Template key (unique across all templates)", - "type": "string" - }, - "steps": { - "description": "Workflow steps", + "ids": { + "description": "Notification IDs to update", "items": { - "$ref": "#/components/schemas/PluginTemplateStepResponseDto" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, + "minItems": 1, "type": "array" }, - "title": { - "description": "Template title", + "readAt": { + "description": "Date when notifications were read", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - }, - "trigger": { - "$ref": "#/components/schemas/WorkflowTrigger", - "description": "Workflow trigger" - }, - "uiHints": { - "description": "Ui hints, for example \"smart-album\"", - "items": { - "type": "string" - }, - "type": "array" } }, "required": [ - "description", - "key", - "steps", - "title", - "trigger", - "uiHints" + "ids" ], "type": "object" }, - "PluginTemplateStepResponseDto": { + "NotificationUpdateDto": { "properties": { - "config": { - "additionalProperties": {}, - "description": "Step configuration", + "readAt": { + "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", "nullable": true, - "type": "object" - }, - "enabled": { - "description": "Whether the step is enabled", - "type": "boolean" - }, - "method": { - "description": "Step plugin method", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + } + }, + "type": "object" + }, + "OAuthAuthorizeResponseDto": { + "properties": { + "url": { + "description": "OAuth authorization URL", "type": "string" } }, "required": [ - "config", - "method" + "url" ], "type": "object" }, - "PurchaseResponse": { + "OAuthBackchannelLogoutDto": { "properties": { - "hideBuyButtonUntil": { - "description": "Date until which to hide buy button", + "logout_token": { + "description": "OAuth logout token", + "type": "string" + } + }, + "required": [ + "logout_token" + ], + "type": "object" + }, + "OAuthCallbackDto": { + "properties": { + "codeVerifier": { + "description": "OAuth code verifier (PKCE)", "type": "string" }, - "showSupportBadge": { - "description": "Whether to show support badge", - "type": "boolean" + "state": { + "description": "OAuth state parameter", + "type": "string" + }, + "url": { + "description": "OAuth callback URL", + "minLength": 1, + "type": "string" } }, "required": [ - "hideBuyButtonUntil", - "showSupportBadge" + "url" ], "type": "object" }, - "PurchaseUpdate": { + "OAuthConfigDto": { "properties": { - "hideBuyButtonUntil": { - "description": "Date until which to hide buy button", + "codeChallenge": { + "description": "OAuth code challenge (PKCE)", "type": "string" }, - "showSupportBadge": { - "description": "Whether to show support badge", - "type": "boolean" + "redirectUri": { + "description": "OAuth redirect URI", + "type": "string" + }, + "state": { + "description": "OAuth state parameter", + "type": "string" } }, + "required": [ + "redirectUri" + ], "type": "object" }, - "QueueCommand": { - "description": "Queue command to execute", + "OAuthTokenEndpointAuthMethod": { + "description": "OAuth token endpoint auth method", "enum": [ - "start", - "pause", - "resume", - "empty", - "clear-failed" + "client_secret_post", + "client_secret_basic" ], "type": "string" }, - "QueueCommandDto": { + "OnThisDayDto": { "properties": { - "command": { - "$ref": "#/components/schemas/QueueCommand" - }, - "force": { - "description": "Force the command execution (if applicable)", - "type": "boolean" + "year": { + "description": "Year for on this day memory", + "maximum": 9999, + "minimum": 1000, + "type": "integer" } }, "required": [ - "command" + "year" ], "type": "object" }, - "QueueDeleteDto": { + "OnboardingDto": { "properties": { - "failed": { - "description": "If true, will also remove failed jobs from the queue.", - "type": "boolean", - "x-immich-history": [ - { - "version": "v2.4.0", - "state": "Added" - }, - { - "version": "v2.4.0", - "state": "Alpha" - } - ], - "x-immich-state": "Alpha" + "isOnboarded": { + "description": "Is user onboarded", + "type": "boolean" } }, + "required": [ + "isOnboarded" + ], "type": "object" }, - "QueueJobResponseDto": { + "OnboardingResponseDto": { "properties": { - "data": { - "additionalProperties": {}, - "description": "Job data payload", - "type": "object" - }, - "id": { - "description": "Job ID", - "type": "string" - }, - "name": { - "$ref": "#/components/schemas/JobName" - }, - "timestamp": { - "description": "Job creation timestamp", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "isOnboarded": { + "description": "Is user onboarded", + "type": "boolean" } }, "required": [ - "data", - "name", - "timestamp" + "isOnboarded" ], "type": "object" }, - "QueueJobStatus": { - "description": "Queue job status", - "enum": [ - "active", - "failed", - "completed", - "delayed", - "waiting", - "paused" + "PartnerCreateDto": { + "properties": { + "sharedWithId": { + "description": "User ID to share with", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + "required": [ + "sharedWithId" ], - "type": "string" + "type": "object" }, - "QueueName": { - "description": "Queue name", + "PartnerDirection": { + "description": "Partner direction", "enum": [ - "thumbnailGeneration", - "metadataExtraction", - "videoConversion", - "faceDetection", - "facialRecognition", - "smartSearch", - "duplicateDetection", - "backgroundTask", - "storageTemplateMigration", - "migration", - "search", - "sidecar", - "library", - "notifications", - "backupDatabase", - "ocr", - "workflow", - "integrityCheck", - "editor" + "shared-by", + "shared-with" ], "type": "string" }, - "QueueResponseDto": { + "PartnerResponseDto": { + "description": "Partner response", "properties": { - "isPaused": { - "description": "Whether the queue is paused", + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" + }, + "email": { + "description": "User email", + "format": "email", + "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", + "type": "string" + }, + "id": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "inTimeline": { + "description": "Show in timeline", "type": "boolean" }, "name": { - "$ref": "#/components/schemas/QueueName" + "description": "User name", + "type": "string" }, - "statistics": { - "$ref": "#/components/schemas/QueueStatisticsDto" + "profileChangedAt": { + "description": "Profile change date", + "format": "date-time", + "type": "string" + }, + "profileImagePath": { + "description": "Profile image path", + "type": "string" } }, "required": [ - "isPaused", + "avatarColor", + "email", + "id", "name", - "statistics" + "profileChangedAt", + "profileImagePath" ], "type": "object" }, - "QueueResponseLegacyDto": { + "PartnerUpdateDto": { "properties": { - "jobCounts": { - "$ref": "#/components/schemas/QueueStatisticsDto" - }, - "queueStatus": { - "$ref": "#/components/schemas/QueueStatusLegacyDto" + "inTimeline": { + "description": "Show partner assets in timeline", + "type": "boolean" } }, "required": [ - "jobCounts", - "queueStatus" + "inTimeline" ], "type": "object" }, - "QueueStatisticsDto": { + "PeopleResponse": { "properties": { - "active": { - "description": "Number of active jobs", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "enabled": { + "description": "Whether people are enabled", + "type": "boolean" }, - "completed": { - "description": "Number of completed jobs", + "minimumFaces": { + "description": "People face threshold", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 1, "type": "integer" }, - "delayed": { - "description": "Number of delayed jobs", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "sidebarWeb": { + "description": "Whether people appear in web sidebar", + "type": "boolean" + } + }, + "required": [ + "enabled", + "sidebarWeb" + ], + "type": "object" + }, + "PeopleResponseDto": { + "description": "People response", + "properties": { + "hasNextPage": { + "description": "Whether there are more pages", + "type": "boolean", + "x-immich-history": [ + { + "version": "v1.110.0", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-state": "Stable" }, - "failed": { - "description": "Number of failed jobs", + "hidden": { + "description": "Number of hidden people", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" }, - "paused": { - "description": "Number of paused jobs", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "people": { + "items": { + "$ref": "#/components/schemas/PersonResponseDto" + }, + "type": "array" }, - "waiting": { - "description": "Number of waiting jobs", + "total": { + "description": "Total number of people", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, "type": "integer" } }, "required": [ - "active", - "completed", - "delayed", - "failed", - "paused", - "waiting" + "hidden", + "people", + "total" ], "type": "object" }, - "QueueStatusLegacyDto": { + "PeopleUpdate": { "properties": { - "isActive": { - "description": "Whether the queue is currently active (has running jobs)", + "enabled": { + "description": "Whether people are enabled", "type": "boolean" }, - "isPaused": { - "description": "Whether the queue is paused", - "type": "boolean" - } - }, - "required": [ - "isActive", - "isPaused" - ], - "type": "object" - }, - "QueueUpdateDto": { - "properties": { - "isPaused": { - "description": "Whether to pause the queue", + "minimumFaces": { + "description": "People face threshold", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "sidebarWeb": { + "description": "Whether people appear in web sidebar", "type": "boolean" } }, "type": "object" }, - "QueuesResponseLegacyDto": { + "PeopleUpdateDto": { "properties": { - "backgroundTask": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "backupDatabase": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "duplicateDetection": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "editor": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "faceDetection": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "facialRecognition": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "integrityCheck": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "library": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "metadataExtraction": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "migration": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "notifications": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "ocr": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "search": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "sidecar": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "smartSearch": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "storageTemplateMigration": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "thumbnailGeneration": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "videoConversion": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" - }, - "workflow": { - "$ref": "#/components/schemas/QueueResponseLegacyDto" + "people": { + "description": "People to update", + "items": { + "$ref": "#/components/schemas/PeopleUpdateItem" + }, + "type": "array" } }, "required": [ - "backgroundTask", - "backupDatabase", - "duplicateDetection", - "editor", - "faceDetection", - "facialRecognition", - "integrityCheck", - "library", - "metadataExtraction", - "migration", - "notifications", - "ocr", - "search", - "sidecar", - "smartSearch", - "storageTemplateMigration", - "thumbnailGeneration", - "videoConversion", - "workflow" + "people" ], "type": "object" }, - "RandomSearchDto": { + "PeopleUpdateItem": { "properties": { - "albumIds": { - "description": "Filter by album IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" - }, - "city": { - "description": "Filter by city name", + "birthDate": { + "description": "Person date of birth", + "format": "date", "nullable": true, "type": "string" }, - "country": { - "description": "Filter by country name", + "color": { + "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, - "createdAfter": { - "description": "Filter by creation date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "featureFaceAssetId": { + "description": "Asset ID used for feature face thumbnail", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "createdBefore": { - "description": "Filter by creation date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "id": { + "description": "Person ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "isEncoded": { - "description": "Filter by encoded status", - "type": "boolean" - }, "isFavorite": { - "description": "Filter by favorite status", - "type": "boolean" - }, - "isMotion": { - "description": "Filter by motion photo status", - "type": "boolean" - }, - "isNotInAlbum": { - "description": "Filter assets not in any album", + "description": "Mark as favorite", "type": "boolean" }, - "isOffline": { - "description": "Filter by offline status", + "isHidden": { + "description": "Person visibility (hidden)", "type": "boolean" }, - "lensModel": { - "description": "Filter by lens model", + "name": { + "description": "Person name", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "Permission": { + "description": "List of permissions", + "enum": [ + "all", + "activity.create", + "activity.read", + "activity.update", + "activity.delete", + "activity.statistics", + "apiKey.create", + "apiKey.read", + "apiKey.update", + "apiKey.delete", + "apiKey.rotate", + "asset.read", + "asset.update", + "asset.delete", + "asset.statistics", + "asset.share", + "asset.view", + "asset.download", + "asset.upload", + "asset.copy", + "asset.derive", + "asset.edit.get", + "asset.edit.create", + "asset.edit.delete", + "album.create", + "album.read", + "album.update", + "album.delete", + "album.statistics", + "album.share", + "album.download", + "albumAsset.create", + "albumAsset.delete", + "albumUser.create", + "albumUser.update", + "albumUser.delete", + "auth.changePassword", + "authDevice.delete", + "archive.read", + "backup.list", + "backup.download", + "backup.upload", + "backup.delete", + "clusterGroup.read", + "clusterGroup.leave", + "clusterGroupRequest.create", + "clusterGroupRequest.read", + "clusterGroupRequest.delete", + "adminConfig.read", + "adminConfig.update", + "userConfig.read", + "duplicate.read", + "duplicate.delete", + "face.create", + "face.read", + "face.update", + "face.delete", + "folder.read", + "job.create", + "job.read", + "library.create", + "library.read", + "library.update", + "library.delete", + "library.statistics", + "timeline.read", + "timeline.download", + "maintenance", + "map.read", + "map.search", + "memory.create", + "memory.read", + "memory.update", + "memory.delete", + "memory.statistics", + "memoryAsset.create", + "memoryAsset.delete", + "notification.create", + "notification.read", + "notification.update", + "notification.delete", + "partner.create", + "partner.read", + "partner.update", + "partner.delete", + "person.create", + "person.read", + "person.update", + "person.delete", + "person.statistics", + "person.merge", + "person.reassign", + "pinCode.create", + "pinCode.update", + "pinCode.delete", + "plugin.create", + "plugin.read", + "plugin.update", + "plugin.delete", + "server.about", + "server.apkLinks", + "server.storage", + "server.statistics", + "server.versionCheck", + "serverLicense.read", + "serverLicense.update", + "serverLicense.delete", + "session.create", + "session.read", + "session.update", + "session.delete", + "session.lock", + "sharedLink.create", + "sharedLink.read", + "sharedLink.update", + "sharedLink.delete", + "stack.create", + "stack.read", + "stack.update", + "stack.delete", + "sync.stream", + "syncCheckpoint.read", + "syncCheckpoint.update", + "syncCheckpoint.delete", + "systemConfig.read", + "systemConfig.update", + "systemMetadata.read", + "systemMetadata.update", + "tag.create", + "tag.read", + "tag.update", + "tag.delete", + "tag.asset", + "user.read", + "user.update", + "userLicense.create", + "userLicense.read", + "userLicense.update", + "userLicense.delete", + "userOnboarding.read", + "userOnboarding.update", + "userOnboarding.delete", + "userPreference.read", + "userPreference.update", + "userProfileImage.create", + "userProfileImage.read", + "userProfileImage.update", + "userProfileImage.delete", + "queue.read", + "queue.update", + "queueJob.create", + "queueJob.read", + "queueJob.update", + "queueJob.delete", + "workflow.create", + "workflow.read", + "workflow.update", + "workflow.delete", + "workflow.logs", + "adminUser.create", + "adminUser.read", + "adminUser.update", + "adminUser.delete", + "adminSession.read", + "adminAuth.unlinkAll" + ], + "type": "string" + }, + "PersonCreateDto": { + "properties": { + "birthDate": { + "description": "Person date of birth", + "format": "date", "nullable": true, "type": "string" }, - "libraryId": { - "description": "Library ID to filter by", - "format": "uuid", + "color": { + "description": "Person color (hex)", "nullable": true, - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, - "make": { - "description": "Filter by camera make", - "nullable": true, - "type": "string" + "isFavorite": { + "description": "Mark as favorite", + "type": "boolean" }, - "model": { - "description": "Filter by camera model", - "nullable": true, - "type": "string" + "isHidden": { + "description": "Person visibility (hidden)", + "type": "boolean" }, - "ocr": { - "description": "Filter by OCR text content", + "name": { + "description": "Person name", "type": "string" - }, - "personIds": { - "description": "Filter by person IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" - }, - "rating": { - "description": "Filter by rating [1-5], or null for unrated", - "maximum": 5, - "minimum": 1, + } + }, + "type": "object" + }, + "PersonResponseDto": { + "properties": { + "birthDate": { + "description": "Person date of birth", + "format": "date", "nullable": true, - "type": "integer", + "type": "string" + }, + "color": { + "description": "Person color (hex)", + "type": "string", "x-immich-history": [ { - "version": "v1", + "version": "v1.126.0", "state": "Added" }, { "version": "v2", "state": "Stable" - }, - { - "version": "v2.6.0", - "state": "Updated", - "description": "Using -1 as a rating is deprecated and will be removed in the next major version." - }, - { - "version": "v3", - "state": "Updated", - "description": "Using -1 as a rating is no longer valid." } ], "x-immich-state": "Stable" }, - "size": { - "description": "Number of results to return", - "maximum": 1000, - "minimum": 1, - "type": "integer" - }, - "state": { - "description": "Filter by state/province name", - "nullable": true, - "type": "string" - }, - "tagIds": { - "description": "Filter by tag IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "nullable": true, - "type": "array" - }, - "takenAfter": { - "description": "Filter by taken date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "id": { + "description": "Person ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "takenBefore": { - "description": "Filter by taken date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "isFavorite": { + "description": "Is favorite", + "type": "boolean", + "x-immich-history": [ + { + "version": "v1.126.0", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-state": "Stable" }, - "trashedAfter": { - "description": "Filter by trash date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "isHidden": { + "description": "Is hidden", + "type": "boolean" }, - "trashedBefore": { - "description": "Filter by trash date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "name": { + "description": "Person name", "type": "string" }, - "type": { - "$ref": "#/components/schemas/AssetTypeEnum" - }, - "updatedAfter": { - "description": "Filter by update date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "thumbnailPath": { + "description": "Thumbnail path", "type": "string" }, - "updatedBefore": { - "description": "Filter by update date (before)", - "example": "2024-01-01T00:00:00.000Z", + "updatedAt": { + "description": "Last update date", "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" - }, - "withDeleted": { - "description": "Include deleted assets", - "type": "boolean" - }, - "withExif": { - "description": "Include EXIF data in response", - "type": "boolean" - }, - "withPeople": { - "description": "Include people data in response", - "type": "boolean" - }, - "withStacked": { - "description": "Include stacked assets", - "type": "boolean" + "type": "string", + "x-immich-history": [ + { + "version": "v1.107.0", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + } + ], + "x-immich-state": "Stable" } }, + "required": [ + "birthDate", + "id", + "isHidden", + "name", + "thumbnailPath" + ], "type": "object" }, - "RatingsResponse": { + "PersonStatisticsResponseDto": { "properties": { - "enabled": { - "description": "Whether ratings are enabled", - "type": "boolean" + "assets": { + "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" } }, "required": [ - "enabled" + "assets" ], "type": "object" }, - "RatingsUpdate": { + "PersonUpdateDto": { "properties": { - "enabled": { - "description": "Whether ratings are enabled", + "birthDate": { + "description": "Person date of birth", + "format": "date", + "nullable": true, + "type": "string" + }, + "color": { + "description": "Person color (hex)", + "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", + "type": "string" + }, + "featureFaceAssetId": { + "description": "Asset ID used for feature face thumbnail", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "isFavorite": { + "description": "Mark as favorite", + "type": "boolean" + }, + "isHidden": { + "description": "Person visibility (hidden)", "type": "boolean" + }, + "name": { + "description": "Person name", + "type": "string" } }, "type": "object" }, - "ReactionLevel": { - "description": "Reaction level", - "enum": [ - "album", - "asset" - ], - "type": "string" - }, - "ReactionType": { - "description": "Reaction type", - "enum": [ - "comment", - "like" - ], - "type": "string" - }, - "RecentlyAddedResponse": { + "PinCodeChangeDto": { "properties": { - "sidebarWeb": { - "description": "Whether the recently added page appears in the web sidebar", - "type": "boolean" + "newPinCode": { + "description": "New PIN code (4-6 digits)", + "pattern": "^\\d{6}$", + "type": "string" + }, + "password": { + "description": "User password (required if PIN code is not provided)", + "example": "password", + "type": "string" + }, + "pinCode": { + "description": "New PIN code (4-6 digits)", + "example": "123456", + "pattern": "^\\d{6}$", + "type": "string" } }, "required": [ - "sidebarWeb" + "newPinCode" ], "type": "object" }, - "RecentlyAddedUpdate": { + "PinCodeResetDto": { "properties": { - "sidebarWeb": { - "description": "Whether the recently added page appears in the web sidebar", - "type": "boolean" + "password": { + "description": "User password (required if PIN code is not provided)", + "example": "password", + "type": "string" + }, + "pinCode": { + "description": "New PIN code (4-6 digits)", + "example": "123456", + "pattern": "^\\d{6}$", + "type": "string" } }, "type": "object" }, - "ReleaseChannel": { - "description": "Release channel", - "enum": [ - "stable", - "releaseCandidate" + "PinCodeSetupDto": { + "properties": { + "pinCode": { + "description": "PIN code (4-6 digits)", + "example": "123456", + "pattern": "^\\d{6}$", + "type": "string" + } + }, + "required": [ + "pinCode" ], - "type": "string" + "type": "object" }, - "ReleaseEventV1": { + "PlacesResponseDto": { "properties": { - "checkedAt": { - "description": "When the server last checked for a latest version. As an ISO timestamp", + "admin1name": { + "description": "Administrative level 1 name (state/province)", "type": "string" }, - "isAvailable": { - "description": "Whether a new version is available", - "type": "boolean" + "admin2name": { + "description": "Administrative level 2 name (county/district)", + "type": "string" }, - "releaseVersion": { - "$ref": "#/components/schemas/ServerVersionResponseDto" + "latitude": { + "description": "Latitude coordinate", + "type": "number" }, - "serverVersion": { - "$ref": "#/components/schemas/ServerVersionResponseDto" + "longitude": { + "description": "Longitude coordinate", + "type": "number" }, - "type": { - "$ref": "#/components/schemas/ReleaseType", - "description": "Release type", - "nullable": true + "name": { + "description": "Place name", + "type": "string" } }, "required": [ - "checkedAt", - "isAvailable", - "releaseVersion", - "serverVersion", - "type" + "latitude", + "longitude", + "name" ], "type": "object" }, - "ReleaseType": { - "enum": [ - "major", - "premajor", - "minor", - "preminor", - "patch", - "prepatch", - "prerelease" - ], - "type": "string" - }, - "ReverseGeocodingStateResponseDto": { + "PluginMethodResponseDto": { "properties": { - "lastImportFileName": { - "description": "Last import file name", - "nullable": true, + "description": { + "description": "Description", "type": "string" }, - "lastUpdate": { - "description": "Last update timestamp", - "nullable": true, + "hostFunctions": { + "type": "boolean" + }, + "key": { + "description": "Key", "type": "string" + }, + "name": { + "description": "Name", + "type": "string" + }, + "schema": { + "properties": {}, + "type": "object" + }, + "title": { + "description": "Title", + "type": "string" + }, + "types": { + "description": "Workflow types", + "items": { + "$ref": "#/components/schemas/WorkflowType" + }, + "type": "array" + }, + "uiHints": { + "description": "Ui hints", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ - "lastImportFileName", - "lastUpdate" + "description", + "hostFunctions", + "key", + "name", + "title", + "types", + "uiHints" ], "type": "object" }, - "RotateParameters": { + "PluginResponseDto": { "properties": { - "angle": { - "description": "Rotation angle in degrees", - "type": "number" + "author": { + "description": "Plugin author", + "type": "string" + }, + "createdAt": { + "description": "Creation date", + "type": "string" + }, + "description": { + "description": "Plugin description", + "type": "string" + }, + "id": { + "description": "Plugin ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "methods": { + "description": "Plugin methods", + "items": { + "$ref": "#/components/schemas/PluginMethodResponseDto" + }, + "type": "array" + }, + "name": { + "description": "Plugin name", + "type": "string" + }, + "title": { + "description": "Plugin title", + "type": "string" + }, + "updatedAt": { + "description": "Last update date", + "type": "string" + }, + "version": { + "description": "Plugin version", + "type": "string" } }, "required": [ - "angle" + "author", + "createdAt", + "description", + "id", + "methods", + "name", + "title", + "updatedAt", + "version" ], "type": "object" }, - "SearchAlbumResponseDto": { + "PluginTemplateResponseDto": { "properties": { - "count": { - "description": "Number of albums in this page", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "description": { + "description": "Template description", + "type": "string" }, - "facets": { + "key": { + "description": "Template key (unique across all templates)", + "type": "string" + }, + "steps": { + "description": "Workflow steps", "items": { - "$ref": "#/components/schemas/SearchFacetResponseDto" + "$ref": "#/components/schemas/PluginTemplateStepResponseDto" }, "type": "array" }, - "items": { + "title": { + "description": "Template title", + "type": "string" + }, + "trigger": { + "$ref": "#/components/schemas/WorkflowTrigger", + "description": "Workflow trigger" + }, + "uiHints": { + "description": "Ui hints, for example \"smart-album\"", "items": { - "$ref": "#/components/schemas/AlbumResponseDto" + "type": "string" }, "type": "array" + } + }, + "required": [ + "description", + "key", + "steps", + "title", + "trigger", + "uiHints" + ], + "type": "object" + }, + "PluginTemplateStepResponseDto": { + "properties": { + "config": { + "additionalProperties": {}, + "description": "Step configuration", + "nullable": true, + "type": "object" + }, + "enabled": { + "description": "Whether the step is enabled", + "type": "boolean" }, - "total": { - "description": "Total number of matching albums", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "method": { + "description": "Step plugin method", + "type": "string" } }, "required": [ - "count", - "facets", - "items", - "total" + "config", + "method" ], "type": "object" }, - "SearchAssetResponseDto": { + "PublicConfigDto": { + "description": "Configuration properties that are visible to everyone", "properties": { - "count": { - "description": "Number of assets in this page", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "facets": { - "items": { - "$ref": "#/components/schemas/SearchFacetResponseDto" - }, - "type": "array" + "oauth": { + "$ref": "#/components/schemas/PublicConfigOAuthDto" }, - "items": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" + "passwordLogin": { + "$ref": "#/components/schemas/PublicConfigPasswordLoginDto" }, - "nextPage": { - "description": "Next page token", - "nullable": true, - "type": "string" + "server": { + "$ref": "#/components/schemas/PublicConfigServerDto" }, - "total": { - "description": "Total number of matching assets", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer", - "x-immich-history": [ - { - "version": "v3.0.0", - "state": "Deprecated" - } - ], - "x-immich-state": "Deprecated" + "theme": { + "$ref": "#/components/schemas/PublicConfigThemeDto" } }, "required": [ - "count", - "facets", - "items", - "nextPage", - "total" + "oauth", + "passwordLogin", + "server", + "theme" ], "type": "object" }, - "SearchExploreItem": { + "PublicConfigOAuthDto": { "properties": { - "data": { - "$ref": "#/components/schemas/AssetResponseDto" + "autoLaunch": { + "description": "Auto launch", + "type": "boolean" }, - "value": { - "description": "Explore value", + "buttonText": { + "description": "Button text", "type": "string" + }, + "enabled": { + "description": "Enabled", + "type": "boolean" } }, "required": [ - "data", - "value" + "autoLaunch", + "buttonText", + "enabled" ], "type": "object" }, - "SearchExploreResponseDto": { + "PublicConfigPasswordLoginDto": { "properties": { - "fieldName": { - "description": "Explore field name", - "type": "string" - }, - "items": { - "items": { - "$ref": "#/components/schemas/SearchExploreItem" - }, - "type": "array" + "enabled": { + "description": "Enabled", + "type": "boolean" } }, "required": [ - "fieldName", - "items" + "enabled" ], "type": "object" }, - "SearchFacetCountResponseDto": { + "PublicConfigServerDto": { "properties": { - "count": { - "description": "Number of assets with this facet value", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "value": { - "description": "Facet value", + "loginPageMessage": { + "description": "Login page message", "type": "string" } }, "required": [ - "count", - "value" + "loginPageMessage" ], "type": "object" }, - "SearchFacetResponseDto": { + "PublicConfigThemeDto": { "properties": { - "counts": { - "items": { - "$ref": "#/components/schemas/SearchFacetCountResponseDto" - }, - "type": "array" - }, - "fieldName": { - "description": "Facet field name", + "customCss": { + "description": "Custom CSS for theming", "type": "string" } }, "required": [ - "counts", - "fieldName" + "customCss" ], "type": "object" }, - "SearchResponseDto": { + "PurchaseResponse": { "properties": { - "albums": { - "$ref": "#/components/schemas/SearchAlbumResponseDto" + "hideBuyButtonUntil": { + "description": "Date until which to hide buy button", + "type": "string" }, - "assets": { - "$ref": "#/components/schemas/SearchAssetResponseDto" + "showSupportBadge": { + "description": "Whether to show support badge", + "type": "boolean" } }, "required": [ - "albums", - "assets" + "hideBuyButtonUntil", + "showSupportBadge" ], "type": "object" }, - "SearchStatisticsResponseDto": { + "PurchaseUpdate": { "properties": { - "total": { - "description": "Total number of matching assets", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "hideBuyButtonUntil": { + "description": "Date until which to hide buy button", + "type": "string" + }, + "showSupportBadge": { + "description": "Whether to show support badge", + "type": "boolean" } }, - "required": [ - "total" - ], "type": "object" }, - "SearchSuggestionType": { - "description": "Suggestion type", + "QueueCommand": { + "description": "Queue command to execute", "enum": [ - "country", - "state", - "city", - "camera-make", - "camera-model", - "camera-lens-model" + "start", + "pause", + "resume", + "empty", + "clear-failed" ], "type": "string" }, - "ServerAboutResponseDto": { + "QueueCommandDto": { "properties": { - "build": { - "description": "Build identifier", - "type": "string" - }, - "buildImage": { - "description": "Build image name", - "type": "string" - }, - "buildImageUrl": { - "description": "Build image URL", - "type": "string" - }, - "buildUrl": { - "description": "Build URL", - "type": "string" - }, - "exiftool": { - "description": "ExifTool version", - "type": "string" - }, - "ffmpeg": { - "description": "FFmpeg version", - "type": "string" - }, - "imagemagick": { - "description": "ImageMagick version", - "type": "string" - }, - "libvips": { - "description": "libvips version", - "type": "string" - }, - "licensed": { - "description": "Whether the server is licensed", - "type": "boolean" - }, - "nodejs": { - "description": "Node.js version", - "type": "string" - }, - "repository": { - "description": "Repository name", - "type": "string" - }, - "repositoryUrl": { - "description": "Repository URL", - "type": "string" - }, - "sourceCommit": { - "description": "Source commit hash", - "type": "string" - }, - "sourceRef": { - "description": "Source reference (branch/tag)", - "type": "string" - }, - "sourceUrl": { - "description": "Source URL", - "type": "string" - }, - "thirdPartyBugFeatureUrl": { - "description": "Third-party bug/feature URL", - "type": "string" - }, - "thirdPartyDocumentationUrl": { - "description": "Third-party documentation URL", - "type": "string" - }, - "thirdPartySourceUrl": { - "description": "Third-party source URL", - "type": "string" - }, - "thirdPartySupportUrl": { - "description": "Third-party support URL", - "type": "string" - }, - "version": { - "description": "Server version", - "type": "string" + "command": { + "$ref": "#/components/schemas/QueueCommand" }, - "versionUrl": { - "description": "URL to version information", - "type": "string" + "force": { + "description": "Force the command execution (if applicable)", + "type": "boolean" } }, "required": [ - "licensed", - "version", - "versionUrl" + "command" ], "type": "object" }, - "ServerApkLinksDto": { + "QueueDeleteDto": { "properties": { - "arm64v8a": { - "description": "APK download link for ARM64 v8a architecture", - "type": "string" - }, - "armeabiv7a": { - "description": "APK download link for ARM EABI v7a architecture", - "type": "string" - }, - "universal": { - "description": "APK download link for universal architecture", - "type": "string" - }, - "x86_64": { - "description": "APK download link for x86_64 architecture", - "type": "string" + "failed": { + "description": "If true, will also remove failed jobs from the queue.", + "type": "boolean", + "x-immich-history": [ + { + "version": "v2.4.0", + "state": "Added" + }, + { + "version": "v2.4.0", + "state": "Alpha" + } + ], + "x-immich-state": "Alpha" } }, - "required": [ - "arm64v8a", - "armeabiv7a", - "universal", - "x86_64" - ], "type": "object" }, - "ServerConfigDto": { + "QueueJobResponseDto": { "properties": { - "externalDomain": { - "description": "External domain URL", - "type": "string" - }, - "isInitialized": { - "description": "Whether the server has been initialized", - "type": "boolean" - }, - "isOnboarded": { - "description": "Whether the admin has completed onboarding", - "type": "boolean" - }, - "loginPageMessage": { - "description": "Login page message", - "type": "string" - }, - "maintenanceMode": { - "description": "Whether maintenance mode is active", - "type": "boolean" - }, - "mapDarkStyleUrl": { - "description": "Map dark style URL", - "type": "string" - }, - "mapLightStyleUrl": { - "description": "Map light style URL", - "type": "string" - }, - "minFaces": { - "description": "People min faces server default", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "data": { + "additionalProperties": {}, + "description": "Job data payload", + "type": "object" }, - "oauthButtonText": { - "description": "OAuth button text", + "id": { + "description": "Job ID", "type": "string" }, - "publicUsers": { - "description": "Whether public user registration is enabled", - "type": "boolean" - }, - "trashDays": { - "description": "Number of days before trashed assets are permanently deleted", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "name": { + "$ref": "#/components/schemas/JobName" }, - "userDeleteDelay": { - "description": "Delay in days before deleted users are permanently removed", + "timestamp": { + "description": "Job creation timestamp", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" } }, "required": [ - "externalDomain", - "isInitialized", - "isOnboarded", - "loginPageMessage", - "maintenanceMode", - "mapDarkStyleUrl", - "mapLightStyleUrl", - "minFaces", - "oauthButtonText", - "publicUsers", - "trashDays", - "userDeleteDelay" + "data", + "name", + "timestamp" ], "type": "object" }, - "ServerFeaturesDto": { - "properties": { - "configFile": { - "description": "Whether config file is available", - "type": "boolean" - }, - "duplicateDetection": { - "description": "Whether duplicate detection is enabled", - "type": "boolean" - }, - "email": { - "description": "Whether email notifications are enabled", - "type": "boolean" - }, - "facialRecognition": { - "description": "Whether facial recognition is enabled", - "type": "boolean" - }, - "importFaces": { - "description": "Whether face import is enabled", - "type": "boolean" - }, - "map": { - "description": "Whether map feature is enabled", - "type": "boolean" - }, - "oauth": { - "description": "Whether OAuth is enabled", - "type": "boolean" - }, - "oauthAutoLaunch": { - "description": "Whether OAuth auto-launch is enabled", - "type": "boolean" - }, - "ocr": { - "description": "Whether OCR is enabled", - "type": "boolean" - }, - "passwordLogin": { - "description": "Whether password login is enabled", - "type": "boolean" - }, - "realtimeTranscoding": { - "description": "Whether real-time transcoding is enabled", - "type": "boolean" - }, - "reverseGeocoding": { - "description": "Whether reverse geocoding is enabled", - "type": "boolean" - }, - "search": { - "description": "Whether search is enabled", - "type": "boolean" - }, - "sidecar": { - "description": "Whether sidecar files are supported", - "type": "boolean" - }, - "smartSearch": { - "description": "Whether smart search is enabled", - "type": "boolean" - }, - "trash": { - "description": "Whether trash feature is enabled", - "type": "boolean" - } - }, - "required": [ - "configFile", - "duplicateDetection", - "email", + "QueueJobStatus": { + "description": "Queue job status", + "enum": [ + "active", + "failed", + "completed", + "delayed", + "waiting", + "paused" + ], + "type": "string" + }, + "QueueName": { + "description": "Queue name", + "enum": [ + "thumbnailGeneration", + "metadataExtraction", + "videoConversion", + "faceDetection", "facialRecognition", - "importFaces", - "map", - "oauth", - "oauthAutoLaunch", - "ocr", - "passwordLogin", - "realtimeTranscoding", - "reverseGeocoding", + "smartSearch", + "duplicateDetection", + "backgroundTask", + "storageTemplateMigration", + "migration", "search", "sidecar", - "smartSearch", - "trash" + "library", + "notifications", + "backupDatabase", + "ocr", + "workflow", + "integrityCheck", + "editor" ], - "type": "object" + "type": "string" }, - "ServerMediaTypesResponseDto": { + "QueueResponseDto": { "properties": { - "image": { - "description": "Supported image MIME types", - "items": { - "type": "string" - }, - "type": "array" - }, - "sidecar": { - "description": "Supported sidecar MIME types", - "items": { - "type": "string" - }, - "type": "array" - }, - "video": { - "description": "Supported video MIME types", - "items": { - "type": "string" - }, - "type": "array" + "isPaused": { + "description": "Whether the queue is paused", + "type": "boolean" + }, + "name": { + "$ref": "#/components/schemas/QueueName" + }, + "statistics": { + "$ref": "#/components/schemas/QueueStatisticsDto" } }, "required": [ - "image", - "sidecar", - "video" + "isPaused", + "name", + "statistics" ], "type": "object" }, - "ServerPingResponse": { + "QueueResponseLegacyDto": { "properties": { - "res": { - "example": "pong", - "type": "string" + "jobCounts": { + "$ref": "#/components/schemas/QueueStatisticsDto" + }, + "queueStatus": { + "$ref": "#/components/schemas/QueueStatusLegacyDto" } }, "required": [ - "res" + "jobCounts", + "queueStatus" ], "type": "object" }, - "ServerStatsResponseDto": { + "QueueStatisticsDto": { "properties": { - "photos": { - "description": "Total number of photos", + "active": { + "description": "Number of active jobs", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" }, - "usage": { - "description": "Total storage usage in bytes", + "completed": { + "description": "Number of completed jobs", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" }, - "usageByUser": { - "description": "Array of usage for each user", - "items": { - "$ref": "#/components/schemas/UsageByUserDto" - }, - "type": "array" + "delayed": { + "description": "Number of delayed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "usagePhotos": { - "description": "Storage usage for photos in bytes", + "failed": { + "description": "Number of failed jobs", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" }, - "usageVideos": { - "description": "Storage usage for videos in bytes", + "paused": { + "description": "Number of paused jobs", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" }, - "videos": { - "description": "Total number of videos", + "waiting": { + "description": "Number of waiting jobs", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" } }, "required": [ - "photos", - "usage", - "usageByUser", - "usagePhotos", - "usageVideos", - "videos" + "active", + "completed", + "delayed", + "failed", + "paused", + "waiting" ], "type": "object" }, - "ServerStorageResponseDto": { + "QueueStatusLegacyDto": { "properties": { - "diskAvailable": { - "description": "Available disk space (human-readable format)", - "type": "string" + "isActive": { + "description": "Whether the queue is currently active (has running jobs)", + "type": "boolean" }, - "diskAvailableRaw": { - "description": "Available disk space in bytes", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "isPaused": { + "description": "Whether the queue is paused", + "type": "boolean" + } + }, + "required": [ + "isActive", + "isPaused" + ], + "type": "object" + }, + "QueueUpdateDto": { + "properties": { + "isPaused": { + "description": "Whether to pause the queue", + "type": "boolean" + } + }, + "type": "object" + }, + "QueuesResponseLegacyDto": { + "properties": { + "backgroundTask": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, - "diskSize": { - "description": "Total disk size (human-readable format)", - "type": "string" + "backupDatabase": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, - "diskSizeRaw": { - "description": "Total disk size in bytes", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "duplicateDetection": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, - "diskUsagePercentage": { - "description": "Disk usage percentage (0-100)", - "format": "double", - "type": "number" + "editor": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, - "diskUse": { - "description": "Used disk space (human-readable format)", - "type": "string" + "faceDetection": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" }, - "diskUseRaw": { - "description": "Used disk space in bytes", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "facialRecognition": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "integrityCheck": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "library": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "metadataExtraction": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "migration": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "notifications": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "ocr": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "search": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "sidecar": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "smartSearch": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "storageTemplateMigration": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "thumbnailGeneration": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "videoConversion": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" + }, + "workflow": { + "$ref": "#/components/schemas/QueueResponseLegacyDto" } }, "required": [ - "diskAvailable", - "diskAvailableRaw", - "diskSize", - "diskSizeRaw", - "diskUsagePercentage", - "diskUse", - "diskUseRaw" + "backgroundTask", + "backupDatabase", + "duplicateDetection", + "editor", + "faceDetection", + "facialRecognition", + "integrityCheck", + "library", + "metadataExtraction", + "migration", + "notifications", + "ocr", + "search", + "sidecar", + "smartSearch", + "storageTemplateMigration", + "thumbnailGeneration", + "videoConversion", + "workflow" ], "type": "object" }, - "ServerVersionHistoryResponseDto": { + "RandomSearchDto": { "properties": { - "createdAt": { - "description": "When this version was first seen", + "albumIds": { + "description": "Filter by album IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "city": { + "description": "Filter by city name", + "nullable": true, + "type": "string" + }, + "country": { + "description": "Filter by country name", + "nullable": true, + "type": "string" + }, + "createdAfter": { + "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "createdBefore": { + "description": "Filter by creation date (before)", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "id": { - "description": "Version history entry ID", + "isEncoded": { + "description": "Filter by encoded status", + "type": "boolean" + }, + "isFavorite": { + "description": "Filter by favorite status", + "type": "boolean" + }, + "isMotion": { + "description": "Filter by motion photo status", + "type": "boolean" + }, + "isNotInAlbum": { + "description": "Filter assets not in any album", + "type": "boolean" + }, + "isOffline": { + "description": "Filter by offline status", + "type": "boolean" + }, + "lensModel": { + "description": "Filter by lens model", + "nullable": true, + "type": "string" + }, + "libraryId": { + "description": "Library ID to filter by", "format": "uuid", + "nullable": true, "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "version": { - "description": "Version string", + "make": { + "description": "Filter by camera make", + "nullable": true, "type": "string" - } - }, - "required": [ - "createdAt", - "id", - "version" - ], - "type": "object" - }, - "ServerVersionResponseDto": { - "properties": { - "major": { - "description": "Major version number", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" }, - "minor": { - "description": "Minor version number", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "model": { + "description": "Filter by camera model", + "nullable": true, + "type": "string" }, - "patch": { - "description": "Patch version number", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "ocr": { + "description": "Filter by OCR text content", + "type": "string" }, - "prerelease": { - "description": "Pre-release version number", - "maximum": 9007199254740991, - "minimum": 0, + "personIds": { + "description": "Filter by person IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "rating": { + "description": "Filter by rating [1-5], or null for unrated", + "maximum": 5, + "minimum": 1, "nullable": true, "type": "integer", "x-immich-history": [ { - "version": "v3.0.0", + "version": "v1", "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + }, + { + "version": "v3", + "state": "Updated", + "description": "Using -1 as a rating is no longer valid." } - ] - } - }, - "required": [ - "major", - "minor", - "patch", - "prerelease" - ], - "type": "object" - }, - "SessionCreateDto": { - "properties": { - "deviceOS": { - "description": "Device OS", - "type": "string" - }, - "deviceType": { - "description": "Device type", - "type": "string" + ], + "x-immich-state": "Stable" }, - "duration": { - "description": "Session duration in seconds", - "maximum": 9007199254740991, + "size": { + "description": "Number of results to return", + "maximum": 1000, "minimum": 1, "type": "integer" - } - }, - "type": "object" - }, - "SessionCreateResponseDto": { - "properties": { - "appVersion": { - "description": "App version", + }, + "state": { + "description": "Filter by state/province name", "nullable": true, "type": "string" }, - "createdAt": { - "description": "Creation date", + "tagIds": { + "description": "Filter by tag IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "nullable": true, + "type": "array" + }, + "takenAfter": { + "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "current": { - "description": "Is current session", - "type": "boolean" + "takenBefore": { + "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "deviceOS": { - "description": "Device OS", + "trashedAfter": { + "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "deviceType": { - "description": "Device type", + "trashedBefore": { + "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "expiresAt": { - "description": "Expiration date", + "type": { + "$ref": "#/components/schemas/AssetTypeEnum" + }, + "updatedAfter": { + "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "id": { - "description": "Session ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "updatedBefore": { + "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "isPendingSyncReset": { - "description": "Is pending sync reset", + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" + }, + "withDeleted": { + "description": "Include deleted assets", "type": "boolean" }, - "token": { - "description": "Session token", - "type": "string" + "withExif": { + "description": "Include EXIF data in response", + "type": "boolean" }, - "updatedAt": { - "description": "Last update date", - "type": "string" + "withPeople": { + "description": "Include people data in response", + "type": "boolean" + }, + "withStacked": { + "description": "Include stacked assets", + "type": "boolean" + } + }, + "type": "object" + }, + "RatingsResponse": { + "properties": { + "enabled": { + "description": "Whether ratings are enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "RatingsUpdate": { + "properties": { + "enabled": { + "description": "Whether ratings are enabled", + "type": "boolean" + } + }, + "type": "object" + }, + "ReactionLevel": { + "description": "Reaction level", + "enum": [ + "album", + "asset" + ], + "type": "string" + }, + "ReactionType": { + "description": "Reaction type", + "enum": [ + "comment", + "like" + ], + "type": "string" + }, + "RecentlyAddedResponse": { + "properties": { + "sidebarWeb": { + "description": "Whether the recently added page appears in the web sidebar", + "type": "boolean" + } + }, + "required": [ + "sidebarWeb" + ], + "type": "object" + }, + "RecentlyAddedUpdate": { + "properties": { + "sidebarWeb": { + "description": "Whether the recently added page appears in the web sidebar", + "type": "boolean" } }, - "required": [ - "appVersion", - "createdAt", - "current", - "deviceOS", - "deviceType", - "id", - "isPendingSyncReset", - "token", - "updatedAt" - ], "type": "object" }, - "SessionResponseDto": { + "ReleaseChannel": { + "description": "Release channel", + "enum": [ + "stable", + "releaseCandidate" + ], + "type": "string" + }, + "ReleaseEventV1": { "properties": { - "appVersion": { - "description": "App version", - "nullable": true, - "type": "string" - }, - "createdAt": { - "description": "Creation date", + "checkedAt": { + "description": "When the server last checked for a latest version. As an ISO timestamp", "type": "string" }, - "current": { - "description": "Is current session", + "isAvailable": { + "description": "Whether a new version is available", "type": "boolean" }, - "deviceOS": { - "description": "Device OS", - "type": "string" - }, - "deviceType": { - "description": "Device type", - "type": "string" - }, - "expiresAt": { - "description": "Expiration date", - "type": "string" - }, - "id": { - "description": "Session ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "releaseVersion": { + "$ref": "#/components/schemas/ServerVersionResponseDto" }, - "isPendingSyncReset": { - "description": "Is pending sync reset", - "type": "boolean" + "serverVersion": { + "$ref": "#/components/schemas/ServerVersionResponseDto" }, - "updatedAt": { - "description": "Last update date", - "type": "string" + "type": { + "$ref": "#/components/schemas/ReleaseType", + "description": "Release type", + "nullable": true } }, "required": [ - "appVersion", - "createdAt", - "current", - "deviceOS", - "deviceType", - "id", - "isPendingSyncReset", - "updatedAt" + "checkedAt", + "isAvailable", + "releaseVersion", + "serverVersion", + "type" ], "type": "object" }, - "SessionUnlockDto": { + "ReleaseType": { + "enum": [ + "major", + "premajor", + "minor", + "preminor", + "patch", + "prepatch", + "prerelease" + ], + "type": "string" + }, + "ReverseGeocodingStateResponseDto": { "properties": { - "password": { - "description": "User password (required if PIN code is not provided)", - "example": "password", + "lastImportFileName": { + "description": "Last import file name", + "nullable": true, "type": "string" }, - "pinCode": { - "description": "New PIN code (4-6 digits)", - "example": "123456", - "pattern": "^\\d{6}$", + "lastUpdate": { + "description": "Last update timestamp", + "nullable": true, "type": "string" } }, + "required": [ + "lastImportFileName", + "lastUpdate" + ], "type": "object" }, - "SessionUpdateDto": { + "RotateParameters": { "properties": { - "isPendingSyncReset": { - "description": "Reset pending sync state", - "type": "boolean" + "angle": { + "description": "Rotation angle in degrees", + "type": "number" } }, + "required": [ + "angle" + ], "type": "object" }, - "SetMaintenanceModeDto": { + "SearchAlbumResponseDto": { "properties": { - "action": { - "$ref": "#/components/schemas/MaintenanceAction" + "count": { + "description": "Number of albums in this page", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" }, - "restoreBackupFilename": { - "description": "Restore backup filename", - "type": "string" + "facets": { + "items": { + "$ref": "#/components/schemas/SearchFacetResponseDto" + }, + "type": "array" + }, + "items": { + "items": { + "$ref": "#/components/schemas/AlbumResponseDto" + }, + "type": "array" + }, + "total": { + "description": "Total number of matching albums", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" } }, "required": [ - "action" + "count", + "facets", + "items", + "total" ], "type": "object" }, - "SharedLinkCreateDto": { + "SearchAssetResponseDto": { "properties": { - "albumId": { - "description": "Album ID (for album sharing)", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "allowDownload": { - "default": true, - "description": "Allow downloads", - "type": "boolean" + "count": { + "description": "Number of assets in this page", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" }, - "allowUpload": { - "description": "Allow uploads", - "type": "boolean" + "facets": { + "items": { + "$ref": "#/components/schemas/SearchFacetResponseDto" + }, + "type": "array" }, - "assetIds": { - "description": "Asset IDs (for individual assets)", + "items": { "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "$ref": "#/components/schemas/AssetResponseDto" }, "type": "array" }, - "description": { - "description": "Link description", + "nextPage": { + "description": "Next page token", "nullable": true, "type": "string" }, - "expiresAt": { - "default": null, - "description": "Expiration date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "total": { + "description": "Total number of matching assets", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer", + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Deprecated" + } + ], + "x-immich-state": "Deprecated" + } + }, + "required": [ + "count", + "facets", + "items", + "nextPage", + "total" + ], + "type": "object" + }, + "SearchExploreItem": { + "properties": { + "data": { + "$ref": "#/components/schemas/AssetResponseDto" }, - "password": { - "description": "Link password", - "nullable": true, + "value": { + "description": "Explore value", "type": "string" - }, - "showMetadata": { - "default": true, - "description": "Show metadata", - "type": "boolean" - }, - "slug": { - "description": "Custom URL slug", - "nullable": true, + } + }, + "required": [ + "data", + "value" + ], + "type": "object" + }, + "SearchExploreResponseDto": { + "properties": { + "fieldName": { + "description": "Explore field name", "type": "string" }, - "type": { - "$ref": "#/components/schemas/SharedLinkType" + "items": { + "items": { + "$ref": "#/components/schemas/SearchExploreItem" + }, + "type": "array" } }, "required": [ - "type" + "fieldName", + "items" ], "type": "object" }, - "SharedLinkEditDto": { + "SearchFacetCountResponseDto": { "properties": { - "allowDownload": { - "description": "Allow downloads", - "type": "boolean" - }, - "allowUpload": { - "description": "Allow uploads", - "type": "boolean" - }, - "description": { - "description": "Link description", - "nullable": true, - "type": "string" + "count": { + "description": "Number of assets with this facet value", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" }, - "expiresAt": { - "description": "Expiration date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "value": { + "description": "Facet value", "type": "string" + } + }, + "required": [ + "count", + "value" + ], + "type": "object" + }, + "SearchFacetResponseDto": { + "properties": { + "counts": { + "items": { + "$ref": "#/components/schemas/SearchFacetCountResponseDto" + }, + "type": "array" }, - "password": { - "description": "Link password", - "nullable": true, + "fieldName": { + "description": "Facet field name", "type": "string" + } + }, + "required": [ + "counts", + "fieldName" + ], + "type": "object" + }, + "SearchResponseDto": { + "properties": { + "albums": { + "$ref": "#/components/schemas/SearchAlbumResponseDto" }, - "showMetadata": { - "description": "Show metadata", - "type": "boolean" - }, - "slug": { - "description": "Custom URL slug", - "nullable": true, - "type": "string" + "assets": { + "$ref": "#/components/schemas/SearchAssetResponseDto" } }, + "required": [ + "albums", + "assets" + ], "type": "object" }, - "SharedLinkLoginDto": { + "SearchStatisticsResponseDto": { "properties": { - "password": { - "description": "Shared link password", - "example": "password", - "type": "string" + "total": { + "description": "Total number of matching assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" } }, "required": [ - "password" + "total" ], "type": "object" }, - "SharedLinkResponseDto": { - "description": "Shared link response", + "SearchSuggestionType": { + "description": "Suggestion type", + "enum": [ + "country", + "state", + "city", + "camera-make", + "camera-model", + "camera-lens-model" + ], + "type": "string" + }, + "ServerAboutResponseDto": { "properties": { - "album": { - "$ref": "#/components/schemas/AlbumResponseDto" + "build": { + "description": "Build identifier", + "type": "string" }, - "allowDownload": { - "description": "Allow downloads", - "type": "boolean" + "buildImage": { + "description": "Build image name", + "type": "string" }, - "allowUpload": { - "description": "Allow uploads", + "buildImageUrl": { + "description": "Build image URL", + "type": "string" + }, + "buildUrl": { + "description": "Build URL", + "type": "string" + }, + "exiftool": { + "description": "ExifTool version", + "type": "string" + }, + "ffmpeg": { + "description": "FFmpeg version", + "type": "string" + }, + "imagemagick": { + "description": "ImageMagick version", + "type": "string" + }, + "libvips": { + "description": "libvips version", + "type": "string" + }, + "licensed": { + "description": "Whether the server is licensed", "type": "boolean" }, - "assets": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" + "nodejs": { + "description": "Node.js version", + "type": "string" }, - "createdAt": { - "description": "Creation date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "repository": { + "description": "Repository name", "type": "string" }, - "description": { - "description": "Link description", - "nullable": true, + "repositoryUrl": { + "description": "Repository URL", "type": "string" }, - "expiresAt": { - "description": "Expiration date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "sourceCommit": { + "description": "Source commit hash", "type": "string" }, - "id": { - "description": "Shared link ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "sourceRef": { + "description": "Source reference (branch/tag)", "type": "string" }, - "key": { - "description": "Encryption key (base64url)", + "sourceUrl": { + "description": "Source URL", "type": "string" }, - "password": { - "description": "Has password", - "nullable": true, + "thirdPartyBugFeatureUrl": { + "description": "Third-party bug/feature URL", "type": "string" }, - "showMetadata": { - "description": "Show metadata", - "type": "boolean" + "thirdPartyDocumentationUrl": { + "description": "Third-party documentation URL", + "type": "string" }, - "slug": { - "description": "Custom URL slug", - "nullable": true, + "thirdPartySourceUrl": { + "description": "Third-party source URL", "type": "string" }, - "type": { - "$ref": "#/components/schemas/SharedLinkType" + "thirdPartySupportUrl": { + "description": "Third-party support URL", + "type": "string" }, - "userId": { - "description": "Owner user ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "version": { + "description": "Server version", + "type": "string" + }, + "versionUrl": { + "description": "URL to version information", "type": "string" } }, "required": [ - "allowDownload", - "allowUpload", - "assets", - "createdAt", - "description", - "expiresAt", - "id", - "key", - "password", - "showMetadata", - "slug", - "type", - "userId" + "licensed", + "version", + "versionUrl" ], "type": "object" }, - "SharedLinkType": { - "description": "Shared link type", - "enum": [ - "ALBUM", - "INDIVIDUAL" - ], - "type": "string" - }, - "SharedLinksResponse": { + "ServerApkLinksDto": { "properties": { - "enabled": { - "description": "Whether shared links are enabled", - "type": "boolean" + "arm64v8a": { + "description": "APK download link for ARM64 v8a architecture", + "type": "string" + }, + "armeabiv7a": { + "description": "APK download link for ARM EABI v7a architecture", + "type": "string" + }, + "universal": { + "description": "APK download link for universal architecture", + "type": "string" }, - "sidebarWeb": { - "description": "Whether shared links appear in web sidebar", - "type": "boolean" + "x86_64": { + "description": "APK download link for x86_64 architecture", + "type": "string" } }, "required": [ - "enabled", - "sidebarWeb" + "arm64v8a", + "armeabiv7a", + "universal", + "x86_64" ], "type": "object" }, - "SharedLinksUpdate": { + "ServerConfigDto": { "properties": { - "enabled": { - "description": "Whether shared links are enabled", + "externalDomain": { + "description": "External domain URL", + "type": "string" + }, + "isInitialized": { + "description": "Whether the server has been initialized", "type": "boolean" }, - "sidebarWeb": { - "description": "Whether shared links appear in web sidebar", + "isOnboarded": { + "description": "Whether the admin has completed onboarding", "type": "boolean" - } - }, - "type": "object" - }, - "SignUpDto": { - "properties": { - "email": { - "description": "User email", - "example": "testuser@email.com", - "format": "email", - "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", + }, + "loginPageMessage": { + "description": "Login page message", "type": "string" }, - "name": { - "description": "User name", - "example": "Admin", + "maintenanceMode": { + "description": "Whether maintenance mode is active", + "type": "boolean" + }, + "mapDarkStyleUrl": { + "description": "Map dark style URL", "type": "string" }, - "password": { - "description": "User password", - "example": "password", + "mapLightStyleUrl": { + "description": "Map light style URL", + "type": "string" + }, + "minFaces": { + "description": "People min faces server default", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "oauthButtonText": { + "description": "OAuth button text", "type": "string" + }, + "publicUsers": { + "description": "Whether public user registration is enabled", + "type": "boolean" + }, + "trashDays": { + "description": "Number of days before trashed assets are permanently deleted", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "userDeleteDelay": { + "description": "Delay in days before deleted users are permanently removed", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" } }, "required": [ - "email", - "name", - "password" + "externalDomain", + "isInitialized", + "isOnboarded", + "loginPageMessage", + "maintenanceMode", + "mapDarkStyleUrl", + "mapLightStyleUrl", + "minFaces", + "oauthButtonText", + "publicUsers", + "trashDays", + "userDeleteDelay" ], "type": "object" }, - "SmartSearchDto": { + "ServerFeaturesDto": { "properties": { - "albumIds": { - "description": "Filter by album IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "configFile": { + "description": "Whether config file is available", + "type": "boolean" }, - "city": { - "description": "Filter by city name", - "nullable": true, - "type": "string" + "duplicateDetection": { + "description": "Whether duplicate detection is enabled", + "type": "boolean" }, - "country": { - "description": "Filter by country name", - "nullable": true, - "type": "string" + "email": { + "description": "Whether email notifications are enabled", + "type": "boolean" }, - "createdAfter": { - "description": "Filter by creation date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "facialRecognition": { + "description": "Whether facial recognition is enabled", + "type": "boolean" }, - "createdBefore": { - "description": "Filter by creation date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "importFaces": { + "description": "Whether face import is enabled", + "type": "boolean" }, - "isEncoded": { - "description": "Filter by encoded status", + "map": { + "description": "Whether map feature is enabled", "type": "boolean" }, - "isFavorite": { - "description": "Filter by favorite status", + "oauth": { + "description": "Whether OAuth is enabled", "type": "boolean" }, - "isMotion": { - "description": "Filter by motion photo status", + "oauthAutoLaunch": { + "description": "Whether OAuth auto-launch is enabled", "type": "boolean" }, - "isNotInAlbum": { - "description": "Filter assets not in any album", + "ocr": { + "description": "Whether OCR is enabled", "type": "boolean" }, - "isOffline": { - "description": "Filter by offline status", + "passwordLogin": { + "description": "Whether password login is enabled", "type": "boolean" }, - "language": { - "description": "Search language code", - "type": "string" + "realtimeTranscoding": { + "description": "Whether real-time transcoding is enabled", + "type": "boolean" }, - "lensModel": { - "description": "Filter by lens model", - "nullable": true, - "type": "string" + "reverseGeocoding": { + "description": "Whether reverse geocoding is enabled", + "type": "boolean" }, - "libraryId": { - "description": "Library ID to filter by", - "format": "uuid", - "nullable": true, - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "search": { + "description": "Whether search is enabled", + "type": "boolean" }, - "make": { - "description": "Filter by camera make", - "nullable": true, - "type": "string" + "sidecar": { + "description": "Whether sidecar files are supported", + "type": "boolean" }, - "model": { - "description": "Filter by camera model", - "nullable": true, - "type": "string" + "smartSearch": { + "description": "Whether smart search is enabled", + "type": "boolean" }, - "ocr": { - "description": "Filter by OCR text content", + "trash": { + "description": "Whether trash feature is enabled", + "type": "boolean" + } + }, + "required": [ + "configFile", + "duplicateDetection", + "email", + "facialRecognition", + "importFaces", + "map", + "oauth", + "oauthAutoLaunch", + "ocr", + "passwordLogin", + "realtimeTranscoding", + "reverseGeocoding", + "search", + "sidecar", + "smartSearch", + "trash" + ], + "type": "object" + }, + "ServerMediaTypesResponseDto": { + "properties": { + "image": { + "description": "Supported image MIME types", + "items": { + "type": "string" + }, + "type": "array" + }, + "sidecar": { + "description": "Supported sidecar MIME types", + "items": { + "type": "string" + }, + "type": "array" + }, + "video": { + "description": "Supported video MIME types", + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "image", + "sidecar", + "video" + ], + "type": "object" + }, + "ServerPingResponse": { + "properties": { + "res": { + "example": "pong", "type": "string" + } + }, + "required": [ + "res" + ], + "type": "object" + }, + "ServerStatsResponseDto": { + "properties": { + "photos": { + "description": "Total number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "page": { - "description": "Page number", + "usage": { + "description": "Total storage usage in bytes", "maximum": 9007199254740991, - "minimum": 1, + "minimum": -9007199254740991, "type": "integer" }, - "personIds": { - "description": "Filter by person IDs", + "usageByUser": { + "description": "Array of usage for each user", "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "$ref": "#/components/schemas/UsageByUserDto" }, "type": "array" }, - "query": { - "description": "Natural language search query", - "type": "string" - }, - "queryAssetId": { - "description": "Asset ID to use as search reference", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "rating": { - "description": "Filter by rating [1-5], or null for unrated", - "maximum": 5, - "minimum": 1, - "nullable": true, - "type": "integer", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - }, - { - "version": "v2.6.0", - "state": "Updated", - "description": "Using -1 as a rating is deprecated and will be removed in the next major version." - }, - { - "version": "v3", - "state": "Updated", - "description": "Using -1 as a rating is no longer valid." - } - ], - "x-immich-state": "Stable" + "usagePhotos": { + "description": "Storage usage for photos in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "size": { - "description": "Number of results to return", - "maximum": 1000, - "minimum": 1, + "usageVideos": { + "description": "Storage usage for videos in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, - "state": { - "description": "Filter by state/province name", - "nullable": true, + "videos": { + "description": "Total number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + } + }, + "required": [ + "photos", + "usage", + "usageByUser", + "usagePhotos", + "usageVideos", + "videos" + ], + "type": "object" + }, + "ServerStorageResponseDto": { + "properties": { + "diskAvailable": { + "description": "Available disk space (human-readable format)", "type": "string" }, - "tagIds": { - "description": "Filter by tag IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "nullable": true, - "type": "array" + "diskAvailableRaw": { + "description": "Available disk space in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "takenAfter": { - "description": "Filter by taken date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "diskSize": { + "description": "Total disk size (human-readable format)", "type": "string" }, - "takenBefore": { - "description": "Filter by taken date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "diskSizeRaw": { + "description": "Total disk size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "trashedAfter": { - "description": "Filter by trash date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "diskUsagePercentage": { + "description": "Disk usage percentage (0-100)", + "format": "double", + "type": "number" }, - "trashedBefore": { - "description": "Filter by trash date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "diskUse": { + "description": "Used disk space (human-readable format)", "type": "string" }, - "type": { - "$ref": "#/components/schemas/AssetTypeEnum" - }, - "updatedAfter": { - "description": "Filter by update date (after)", + "diskUseRaw": { + "description": "Used disk space in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + } + }, + "required": [ + "diskAvailable", + "diskAvailableRaw", + "diskSize", + "diskSizeRaw", + "diskUsagePercentage", + "diskUse", + "diskUseRaw" + ], + "type": "object" + }, + "ServerVersionHistoryResponseDto": { + "properties": { + "createdAt": { + "description": "When this version was first seen", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "updatedBefore": { - "description": "Filter by update date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "id": { + "description": "Version history entry ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" - }, - "withDeleted": { - "description": "Include deleted assets", - "type": "boolean" - }, - "withExif": { - "description": "Include EXIF data in response", - "type": "boolean" + "version": { + "description": "Version string", + "type": "string" } }, - "type": "object" - }, - "SourceType": { - "description": "Face detection source type", - "enum": [ - "machine-learning", - "exif", - "manual" + "required": [ + "createdAt", + "id", + "version" ], - "type": "string" + "type": "object" }, - "StackCreateDto": { + "ServerVersionResponseDto": { "properties": { - "assetIds": { - "description": "Asset IDs (first becomes primary, min 2)", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "minItems": 2, - "type": "array" + "major": { + "description": "Major version number", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "minor": { + "description": "Minor version number", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "patch": { + "description": "Patch version number", + "maximum": 9007199254740991, + "minimum": 0, + "type": "integer" + }, + "prerelease": { + "description": "Pre-release version number", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer", + "x-immich-history": [ + { + "version": "v3.0.0", + "state": "Added" + } + ] } }, "required": [ - "assetIds" + "major", + "minor", + "patch", + "prerelease" ], "type": "object" }, - "StackResponseDto": { - "description": "Stack response", + "SessionCreateDto": { "properties": { - "assets": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - }, - "id": { - "description": "Stack ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "deviceOS": { + "description": "Device OS", "type": "string" }, - "primaryAssetId": { - "description": "Primary asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "deviceType": { + "description": "Device type", "type": "string" + }, + "duration": { + "description": "Session duration in seconds", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" } }, - "required": [ - "assets", - "id", - "primaryAssetId" - ], "type": "object" }, - "StackUpdateDto": { + "SessionCreateResponseDto": { "properties": { - "primaryAssetId": { - "description": "Primary asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "appVersion": { + "description": "App version", + "nullable": true, "type": "string" - } - }, - "type": "object" - }, - "StatisticsSearchDto": { - "properties": { - "albumIds": { - "description": "Filter by album IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" }, - "city": { - "description": "Filter by city name", - "nullable": true, + "createdAt": { + "description": "Creation date", "type": "string" }, - "country": { - "description": "Filter by country name", - "nullable": true, + "current": { + "description": "Is current session", + "type": "boolean" + }, + "deviceOS": { + "description": "Device OS", "type": "string" }, - "createdAfter": { - "description": "Filter by creation date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "deviceType": { + "description": "Device type", "type": "string" }, - "createdBefore": { - "description": "Filter by creation date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "expiresAt": { + "description": "Expiration date", "type": "string" }, - "description": { - "description": "Filter by description text", + "id": { + "description": "Session ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "isEncoded": { - "description": "Filter by encoded status", + "isPendingSyncReset": { + "description": "Is pending sync reset", "type": "boolean" }, - "isFavorite": { - "description": "Filter by favorite status", - "type": "boolean" + "token": { + "description": "Session token", + "type": "string" }, - "isMotion": { - "description": "Filter by motion photo status", - "type": "boolean" + "updatedAt": { + "description": "Last update date", + "type": "string" + } + }, + "required": [ + "appVersion", + "createdAt", + "current", + "deviceOS", + "deviceType", + "id", + "isPendingSyncReset", + "token", + "updatedAt" + ], + "type": "object" + }, + "SessionResponseDto": { + "properties": { + "appVersion": { + "description": "App version", + "nullable": true, + "type": "string" }, - "isNotInAlbum": { - "description": "Filter assets not in any album", - "type": "boolean" + "createdAt": { + "description": "Creation date", + "type": "string" }, - "isOffline": { - "description": "Filter by offline status", + "current": { + "description": "Is current session", "type": "boolean" }, - "lensModel": { - "description": "Filter by lens model", - "nullable": true, - "type": "string" - }, - "libraryId": { - "description": "Library ID to filter by", - "format": "uuid", - "nullable": true, - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "deviceOS": { + "description": "Device OS", "type": "string" }, - "make": { - "description": "Filter by camera make", - "nullable": true, + "deviceType": { + "description": "Device type", "type": "string" }, - "model": { - "description": "Filter by camera model", - "nullable": true, + "expiresAt": { + "description": "Expiration date", "type": "string" }, - "ocr": { - "description": "Filter by OCR text content", + "id": { + "description": "Session ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "personIds": { - "description": "Filter by person IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "isPendingSyncReset": { + "description": "Is pending sync reset", + "type": "boolean" }, - "rating": { - "description": "Filter by rating [1-5], or null for unrated", - "maximum": 5, - "minimum": 1, - "nullable": true, - "type": "integer", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - }, - { - "version": "v2.6.0", - "state": "Updated", - "description": "Using -1 as a rating is deprecated and will be removed in the next major version." - }, - { - "version": "v3", - "state": "Updated", - "description": "Using -1 as a rating is no longer valid." - } - ], - "x-immich-state": "Stable" + "updatedAt": { + "description": "Last update date", + "type": "string" + } + }, + "required": [ + "appVersion", + "createdAt", + "current", + "deviceOS", + "deviceType", + "id", + "isPendingSyncReset", + "updatedAt" + ], + "type": "object" + }, + "SessionUnlockDto": { + "properties": { + "password": { + "description": "User password (required if PIN code is not provided)", + "example": "password", + "type": "string" }, - "state": { - "description": "Filter by state/province name", - "nullable": true, + "pinCode": { + "description": "New PIN code (4-6 digits)", + "example": "123456", + "pattern": "^\\d{6}$", + "type": "string" + } + }, + "type": "object" + }, + "SessionUpdateDto": { + "properties": { + "isPendingSyncReset": { + "description": "Reset pending sync state", + "type": "boolean" + } + }, + "type": "object" + }, + "SetMaintenanceModeDto": { + "properties": { + "action": { + "$ref": "#/components/schemas/MaintenanceAction" + }, + "restoreBackupFilename": { + "description": "Restore backup filename", + "type": "string" + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "SharedLinkCreateDto": { + "properties": { + "albumId": { + "description": "Album ID (for album sharing)", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "tagIds": { - "description": "Filter by tag IDs", + "allowDownload": { + "default": true, + "description": "Allow downloads", + "type": "boolean" + }, + "allowUpload": { + "description": "Allow uploads", + "type": "boolean" + }, + "assetIds": { + "description": "Asset IDs (for individual assets)", "items": { "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "nullable": true, "type": "array" }, - "takenAfter": { - "description": "Filter by taken date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "description": { + "description": "Link description", + "nullable": true, "type": "string" }, - "takenBefore": { - "description": "Filter by taken date (before)", + "expiresAt": { + "default": null, + "description": "Expiration date", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "trashedAfter": { - "description": "Filter by trash date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "password": { + "description": "Link password", + "nullable": true, "type": "string" }, - "trashedBefore": { - "description": "Filter by trash date (before)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "showMetadata": { + "default": true, + "description": "Show metadata", + "type": "boolean" + }, + "slug": { + "description": "Custom URL slug", + "nullable": true, "type": "string" }, "type": { - "$ref": "#/components/schemas/AssetTypeEnum" + "$ref": "#/components/schemas/SharedLinkType" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "SharedLinkEditDto": { + "properties": { + "allowDownload": { + "description": "Allow downloads", + "type": "boolean" }, - "updatedAfter": { - "description": "Filter by update date (after)", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "allowUpload": { + "description": "Allow uploads", + "type": "boolean" + }, + "description": { + "description": "Link description", + "nullable": true, "type": "string" }, - "updatedBefore": { - "description": "Filter by update date (before)", + "expiresAt": { + "description": "Expiration date", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" - } - }, - "type": "object" - }, - "StorageFolder": { - "description": "Storage folder", - "enum": [ - "encoded-video", - "library", - "upload", - "profile", - "thumbs", - "backups" - ], - "type": "string" - }, - "SyncAckDeleteDto": { - "properties": { - "types": { - "description": "Sync entity types to delete acks for", - "items": { - "$ref": "#/components/schemas/SyncEntityType" - }, - "type": "array" + "password": { + "description": "Link password", + "nullable": true, + "type": "string" + }, + "showMetadata": { + "description": "Show metadata", + "type": "boolean" + }, + "slug": { + "description": "Custom URL slug", + "nullable": true, + "type": "string" } }, "type": "object" }, - "SyncAckDto": { + "SharedLinkLoginDto": { "properties": { - "ack": { - "description": "Acknowledgment ID", + "password": { + "description": "Shared link password", + "example": "password", "type": "string" - }, - "type": { - "$ref": "#/components/schemas/SyncEntityType" } }, "required": [ - "ack", - "type" + "password" ], "type": "object" }, - "SyncAckSetDto": { + "SharedLinkResponseDto": { + "description": "Shared link response", "properties": { - "acks": { - "description": "Acknowledgment IDs (max 1000)", + "album": { + "$ref": "#/components/schemas/AlbumResponseDto" + }, + "allowDownload": { + "description": "Allow downloads", + "type": "boolean" + }, + "allowUpload": { + "description": "Allow uploads", + "type": "boolean" + }, + "assets": { "items": { - "type": "string" + "$ref": "#/components/schemas/AssetResponseDto" }, - "maxItems": 1000, "type": "array" - } - }, - "required": [ - "acks" - ], - "type": "object" - }, - "SyncAckV1": { - "properties": {}, - "type": "object" - }, - "SyncAlbumDeleteV1": { - "properties": { - "albumId": { - "description": "Album ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + }, + "createdAt": { + "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - } - }, - "required": [ - "albumId" - ], - "type": "object" - }, - "SyncAlbumToAssetDeleteV1": { - "properties": { - "albumId": { - "description": "Album ID", + }, + "description": { + "description": "Link description", + "nullable": true, + "type": "string" + }, + "expiresAt": { + "description": "Expiration date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "id": { + "description": "Shared link ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "assetId": { - "description": "Asset ID", + "key": { + "description": "Encryption key (base64url)", + "type": "string" + }, + "password": { + "description": "Has password", + "nullable": true, + "type": "string" + }, + "showMetadata": { + "description": "Show metadata", + "type": "boolean" + }, + "slug": { + "description": "Custom URL slug", + "nullable": true, + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/SharedLinkType" + }, + "userId": { + "description": "Owner user ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, "required": [ - "albumId", - "assetId" + "allowDownload", + "allowUpload", + "assets", + "createdAt", + "description", + "expiresAt", + "id", + "key", + "password", + "showMetadata", + "slug", + "type", + "userId" ], "type": "object" }, - "SyncAlbumToAssetV1": { + "SharedLinkType": { + "description": "Shared link type", + "enum": [ + "ALBUM", + "INDIVIDUAL" + ], + "type": "string" + }, + "SharedLinksResponse": { "properties": { - "albumId": { - "description": "Album ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "enabled": { + "description": "Whether shared links are enabled", + "type": "boolean" }, - "assetId": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "sidebarWeb": { + "description": "Whether shared links appear in web sidebar", + "type": "boolean" } }, "required": [ - "albumId", - "assetId" + "enabled", + "sidebarWeb" ], "type": "object" }, - "SyncAlbumUserDeleteV1": { + "SharedLinksUpdate": { "properties": { - "albumId": { - "description": "Album ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "enabled": { + "description": "Whether shared links are enabled", + "type": "boolean" }, - "userId": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "sidebarWeb": { + "description": "Whether shared links appear in web sidebar", + "type": "boolean" } }, - "required": [ - "albumId", - "userId" - ], "type": "object" }, - "SyncAlbumUserV1": { + "SignUpDto": { "properties": { - "albumId": { - "description": "Album ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "email": { + "description": "User email", + "example": "testuser@email.com", + "format": "email", + "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", "type": "string" }, - "role": { - "$ref": "#/components/schemas/AlbumUserRole" + "name": { + "description": "User name", + "example": "Admin", + "type": "string" }, - "userId": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "password": { + "description": "User password", + "example": "password", "type": "string" } }, "required": [ - "albumId", - "role", - "userId" + "email", + "name", + "password" ], "type": "object" }, - "SyncAlbumV1": { + "SmartSearchDto": { "properties": { - "createdAt": { - "description": "Created at", + "albumIds": { + "description": "Filter by album IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "city": { + "description": "Filter by city name", + "nullable": true, + "type": "string" + }, + "country": { + "description": "Filter by country name", + "nullable": true, + "type": "string" + }, + "createdAfter": { + "description": "Filter by creation date (after)", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "description": { - "description": "Album description", + "createdBefore": { + "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "id": { - "description": "Album ID", + "isEncoded": { + "description": "Filter by encoded status", + "type": "boolean" + }, + "isFavorite": { + "description": "Filter by favorite status", + "type": "boolean" + }, + "isMotion": { + "description": "Filter by motion photo status", + "type": "boolean" + }, + "isNotInAlbum": { + "description": "Filter assets not in any album", + "type": "boolean" + }, + "isOffline": { + "description": "Filter by offline status", + "type": "boolean" + }, + "language": { + "description": "Search language code", + "type": "string" + }, + "lensModel": { + "description": "Filter by lens model", + "nullable": true, + "type": "string" + }, + "libraryId": { + "description": "Library ID to filter by", "format": "uuid", + "nullable": true, "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "isActivityEnabled": { - "description": "Is activity enabled", - "type": "boolean" + "make": { + "description": "Filter by camera make", + "nullable": true, + "type": "string" }, - "name": { - "description": "Album name", + "model": { + "description": "Filter by camera model", + "nullable": true, "type": "string" }, - "order": { - "$ref": "#/components/schemas/AssetOrder" + "ocr": { + "description": "Filter by OCR text content", + "type": "string" }, - "ownerId": { - "description": "Owner ID", + "page": { + "description": "Page number", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + }, + "personIds": { + "description": "Filter by person IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "query": { + "description": "Natural language search query", + "type": "string" + }, + "queryAssetId": { + "description": "Asset ID to use as search reference", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "thumbnailAssetId": { - "description": "Thumbnail asset ID", + "rating": { + "description": "Filter by rating [1-5], or null for unrated", + "maximum": 5, + "minimum": 1, "nullable": true, - "type": "string" + "type": "integer", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + }, + { + "version": "v3", + "state": "Updated", + "description": "Using -1 as a rating is no longer valid." + } + ], + "x-immich-state": "Stable" }, - "updatedAt": { - "description": "Updated at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "size": { + "description": "Number of results to return", + "maximum": 1000, + "minimum": 1, + "type": "integer" + }, + "state": { + "description": "Filter by state/province name", + "nullable": true, "type": "string" - } - }, - "required": [ - "createdAt", - "description", - "id", - "isActivityEnabled", - "name", - "order", - "ownerId", - "thumbnailAssetId", - "updatedAt" - ], - "type": "object" - }, - "SyncAlbumV2": { - "properties": { - "createdAt": { - "description": "Created at", + }, + "tagIds": { + "description": "Filter by tag IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "nullable": true, + "type": "array" + }, + "takenAfter": { + "description": "Filter by taken date (after)", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "description": { - "description": "Album description", + "takenBefore": { + "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "id": { - "description": "Album ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "trashedAfter": { + "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "isActivityEnabled": { - "description": "Is activity enabled", - "type": "boolean" - }, - "name": { - "description": "Album name", + "trashedBefore": { + "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "order": { - "$ref": "#/components/schemas/AssetOrder" + "type": { + "$ref": "#/components/schemas/AssetTypeEnum" }, - "thumbnailAssetId": { - "description": "Thumbnail asset ID", - "nullable": true, + "updatedAfter": { + "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "updatedAt": { - "description": "Updated at", + "updatedBefore": { + "description": "Filter by update date (before)", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" + }, + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" + }, + "withDeleted": { + "description": "Include deleted assets", + "type": "boolean" + }, + "withExif": { + "description": "Include EXIF data in response", + "type": "boolean" } }, - "required": [ - "createdAt", - "description", - "id", - "isActivityEnabled", - "name", - "order", - "thumbnailAssetId", - "updatedAt" - ], "type": "object" }, - "SyncAssetDeleteV1": { - "properties": { - "assetId": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - "required": [ - "assetId" + "SourceType": { + "description": "Face detection source type", + "enum": [ + "machine-learning", + "exif", + "manual" ], - "type": "object" + "type": "string" }, - "SyncAssetEditDeleteV1": { + "StackCreateDto": { "properties": { - "editId": { - "description": "Edit ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "assetIds": { + "description": "Asset IDs (first becomes primary, min 2)", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "minItems": 2, + "type": "array" } }, "required": [ - "editId" + "assetIds" ], "type": "object" }, - "SyncAssetEditV1": { + "StackResponseDto": { + "description": "Stack response", "properties": { - "action": { - "$ref": "#/components/schemas/AssetEditAction" + "assets": { + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" }, - "assetId": { - "description": "Asset ID", + "id": { + "description": "Stack ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "id": { - "description": "Edit ID", + "primaryAssetId": { + "description": "Primary asset ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "parameters": { - "additionalProperties": {}, - "description": "Edit parameters", - "type": "object" - }, - "sequence": { - "description": "Edit sequence", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" } }, "required": [ - "action", - "assetId", + "assets", "id", - "parameters", - "sequence" + "primaryAssetId" ], "type": "object" }, - "SyncAssetExifV1": { + "StackUpdateDto": { "properties": { - "assetId": { - "description": "Asset ID", + "primaryAssetId": { + "description": "Primary asset ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" + } + }, + "type": "object" + }, + "StatisticsSearchDto": { + "properties": { + "albumIds": { + "description": "Filter by album IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" }, "city": { - "description": "City", + "description": "Filter by city name", "nullable": true, "type": "string" }, "country": { - "description": "Country", + "description": "Filter by country name", "nullable": true, "type": "string" }, - "dateTimeOriginal": { - "description": "Date time original", + "createdAfter": { + "description": "Filter by creation date (after)", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", - "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "description": { - "description": "Description", - "nullable": true, + "createdBefore": { + "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "exifImageHeight": { - "description": "Exif image height", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "nullable": true, - "type": "integer" + "description": { + "description": "Filter by description text", + "type": "string" }, - "exifImageWidth": { - "description": "Exif image width", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "nullable": true, - "type": "integer" + "isEncoded": { + "description": "Filter by encoded status", + "type": "boolean" }, - "exposureTime": { - "description": "Exposure time", - "nullable": true, - "type": "string" + "isFavorite": { + "description": "Filter by favorite status", + "type": "boolean" }, - "fNumber": { - "description": "F number", - "format": "double", - "nullable": true, - "type": "number" + "isMotion": { + "description": "Filter by motion photo status", + "type": "boolean" }, - "fileSizeInByte": { - "description": "File size in byte", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "nullable": true, - "type": "integer" + "isNotInAlbum": { + "description": "Filter assets not in any album", + "type": "boolean" }, - "focalLength": { - "description": "Focal length", - "format": "double", - "nullable": true, - "type": "number" + "isOffline": { + "description": "Filter by offline status", + "type": "boolean" }, - "fps": { - "description": "FPS", - "format": "double", + "lensModel": { + "description": "Filter by lens model", "nullable": true, - "type": "number" + "type": "string" }, - "iso": { - "description": "ISO", - "maximum": 9007199254740991, - "minimum": -9007199254740991, + "libraryId": { + "description": "Library ID to filter by", + "format": "uuid", "nullable": true, - "type": "integer" + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "latitude": { - "description": "Latitude", - "format": "double", + "make": { + "description": "Filter by camera make", "nullable": true, - "type": "number" + "type": "string" }, - "lensModel": { - "description": "Lens model", + "model": { + "description": "Filter by camera model", "nullable": true, "type": "string" }, - "longitude": { - "description": "Longitude", - "format": "double", + "ocr": { + "description": "Filter by OCR text content", + "type": "string" + }, + "personIds": { + "description": "Filter by person IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" + }, + "rating": { + "description": "Filter by rating [1-5], or null for unrated", + "maximum": 5, + "minimum": 1, "nullable": true, - "type": "number" + "type": "integer", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v2.6.0", + "state": "Updated", + "description": "Using -1 as a rating is deprecated and will be removed in the next major version." + }, + { + "version": "v3", + "state": "Updated", + "description": "Using -1 as a rating is no longer valid." + } + ], + "x-immich-state": "Stable" }, - "make": { - "description": "Make", + "state": { + "description": "Filter by state/province name", "nullable": true, "type": "string" }, - "model": { - "description": "Model", + "tagIds": { + "description": "Filter by tag IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, "nullable": true, - "type": "string" + "type": "array" }, - "modifyDate": { - "description": "Modify date", + "takenAfter": { + "description": "Filter by taken date (after)", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", - "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "orientation": { - "description": "Orientation", - "nullable": true, + "takenBefore": { + "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "profileDescription": { - "description": "Profile description", - "nullable": true, + "trashedAfter": { + "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "projectionType": { - "description": "Projection type", - "nullable": true, + "trashedBefore": { + "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "rating": { - "description": "Rating", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "nullable": true, - "type": "integer" + "type": { + "$ref": "#/components/schemas/AssetTypeEnum" }, - "state": { - "description": "State", - "nullable": true, + "updatedAfter": { + "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "timeZone": { - "description": "Time zone", - "nullable": true, + "updatedBefore": { + "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" + }, + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" } }, - "required": [ - "assetId", - "city", - "country", - "dateTimeOriginal", - "description", - "exifImageHeight", - "exifImageWidth", - "exposureTime", - "fNumber", - "fileSizeInByte", - "focalLength", - "fps", - "iso", - "latitude", - "lensModel", - "longitude", - "make", - "model", - "modifyDate", - "orientation", - "profileDescription", - "projectionType", - "rating", - "state", - "timeZone" - ], "type": "object" }, - "SyncAssetFaceDeleteV1": { + "StorageFolder": { + "description": "Storage folder", + "enum": [ + "encoded-video", + "library", + "upload", + "profile", + "thumbs", + "backups" + ], + "type": "string" + }, + "SyncAckDeleteDto": { "properties": { - "assetFaceId": { - "description": "Asset face ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "types": { + "description": "Sync entity types to delete acks for", + "items": { + "$ref": "#/components/schemas/SyncEntityType" + }, + "type": "array" } }, - "required": [ - "assetFaceId" - ], "type": "object" }, - "SyncAssetFaceV1": { + "SyncAckDto": { "properties": { - "assetId": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "boundingBoxX1": { - "description": "Bounding box X1", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "boundingBoxX2": { - "description": "Bounding box X2", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "boundingBoxY1": { - "description": "Bounding box Y1", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "boundingBoxY2": { - "description": "Bounding box Y2", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "id": { - "description": "Asset face ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "imageHeight": { - "description": "Image height", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "imageWidth": { - "description": "Image width", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "personId": { - "description": "Person ID", - "nullable": true, + "ack": { + "description": "Acknowledgment ID", "type": "string" }, - "sourceType": { - "description": "Source type", - "type": "string" + "type": { + "$ref": "#/components/schemas/SyncEntityType" } }, "required": [ - "assetId", - "boundingBoxX1", - "boundingBoxX2", - "boundingBoxY1", - "boundingBoxY2", - "id", - "imageHeight", - "imageWidth", - "personId", - "sourceType" + "ack", + "type" + ], + "type": "object" + }, + "SyncAckSetDto": { + "properties": { + "acks": { + "description": "Acknowledgment IDs (max 1000)", + "items": { + "type": "string" + }, + "maxItems": 1000, + "type": "array" + } + }, + "required": [ + "acks" ], "type": "object" }, - "SyncAssetFaceV2": { + "SyncAckV1": { + "properties": {}, + "type": "object" + }, + "SyncAlbumDeleteV1": { "properties": { - "assetId": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "boundingBoxX1": { - "description": "Bounding box X1", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "boundingBoxX2": { - "description": "Bounding box X2", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "boundingBoxY1": { - "description": "Bounding box Y1", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "boundingBoxY2": { - "description": "Bounding box Y2", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "deletedAt": { - "description": "Face deleted at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "id": { - "description": "Asset face ID", + "albumId": { + "description": "Album ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "imageHeight": { - "description": "Image height", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "imageWidth": { - "description": "Image width", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "isVisible": { - "description": "Is the face visible in the asset", - "type": "boolean" - }, - "personId": { - "description": "Person ID", - "nullable": true, - "type": "string" - }, - "sourceType": { - "description": "Source type", - "type": "string" } }, "required": [ - "assetId", - "boundingBoxX1", - "boundingBoxX2", - "boundingBoxY1", - "boundingBoxY2", - "deletedAt", - "id", - "imageHeight", - "imageWidth", - "isVisible", - "personId", - "sourceType" + "albumId" ], "type": "object" }, - "SyncAssetMetadataDeleteV1": { + "SyncAlbumToAssetDeleteV1": { "properties": { - "assetId": { - "description": "Asset ID", + "albumId": { + "description": "Album ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "key": { - "description": "Key", + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, "required": [ - "assetId", - "key" + "albumId", + "assetId" ], "type": "object" }, - "SyncAssetMetadataV1": { + "SyncAlbumToAssetV1": { "properties": { - "assetId": { - "description": "Asset ID", + "albumId": { + "description": "Album ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "key": { - "description": "Key", + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "value": { - "additionalProperties": {}, - "description": "Value", - "type": "object" } }, "required": [ - "assetId", - "key", - "value" + "albumId", + "assetId" ], "type": "object" }, - "SyncAssetOcrDeleteV1": { + "SyncAlbumUserDeleteV1": { "properties": { - "assetId": { - "description": "Original asset ID of the deleted OCR entry", - "type": "string" - }, - "deletedAt": { - "description": "Timestamp when the OCR entry was deleted", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "albumId": { + "description": "Album ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "id": { - "description": "Audit row ID of the deleted OCR entry", + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, "required": [ - "assetId", - "deletedAt", - "id" + "albumId", + "userId" ], "type": "object" }, - "SyncAssetOcrV1": { + "SyncAlbumUserV1": { "properties": { - "assetId": { - "description": "Asset ID", + "albumId": { + "description": "Album ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "boxScore": { - "description": "Confidence score of the bounding box", - "format": "double", - "type": "number" + "role": { + "$ref": "#/components/schemas/AlbumUserRole" }, - "id": { - "description": "OCR entry ID", + "userId": { + "description": "User ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "isVisible": { - "description": "Whether the OCR entry is visible", - "type": "boolean" - }, - "text": { - "description": "Recognized text content", - "type": "string" - }, - "textScore": { - "description": "Confidence score of the recognized text", - "format": "double", - "type": "number" - }, - "x1": { - "description": "Top-left X coordinate (normalized 0–1)", - "format": "double", - "type": "number" - }, - "x2": { - "description": "Top-right X coordinate (normalized 0–1)", - "format": "double", - "type": "number" - }, - "x3": { - "description": "Bottom-right X coordinate (normalized 0–1)", - "format": "double", - "type": "number" - }, - "x4": { - "description": "Bottom-left X coordinate (normalized 0–1)", - "format": "double", - "type": "number" - }, - "y1": { - "description": "Top-left Y coordinate (normalized 0–1)", - "format": "double", - "type": "number" - }, - "y2": { - "description": "Top-right Y coordinate (normalized 0–1)", - "format": "double", - "type": "number" - }, - "y3": { - "description": "Bottom-right Y coordinate (normalized 0–1)", - "format": "double", - "type": "number" - }, - "y4": { - "description": "Bottom-left Y coordinate (normalized 0–1)", - "format": "double", - "type": "number" } }, "required": [ - "assetId", - "boxScore", - "id", - "isVisible", - "text", - "textScore", - "x1", - "x2", - "x3", - "x4", - "y1", - "y2", - "y3", - "y4" + "albumId", + "role", + "userId" ], "type": "object" }, - "SyncAssetV1": { + "SyncAlbumV1": { "properties": { - "checksum": { - "description": "Checksum", + "createdAt": { + "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "description": { + "description": "Album description", + "type": "string" + }, + "id": { + "description": "Album ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "createdAt": { - "description": "Uploaded to Immich at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "isActivityEnabled": { + "description": "Is activity enabled", + "type": "boolean" + }, + "name": { + "description": "Album name", "type": "string" }, - "deletedAt": { - "description": "Deleted at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "order": { + "$ref": "#/components/schemas/AssetOrder" + }, + "ownerId": { + "description": "Owner ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "duration": { - "description": "Duration", + "thumbnailAssetId": { + "description": "Thumbnail asset ID", "nullable": true, "type": "string" }, - "fileCreatedAt": { - "description": "File created at", + "updatedAt": { + "description": "Updated at", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", - "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - }, - "fileModifiedAt": { - "description": "File modified at", + } + }, + "required": [ + "createdAt", + "description", + "id", + "isActivityEnabled", + "name", + "order", + "ownerId", + "thumbnailAssetId", + "updatedAt" + ], + "type": "object" + }, + "SyncAlbumV2": { + "properties": { + "createdAt": { + "description": "Created at", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", - "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "height": { - "description": "Asset height", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "nullable": true, - "type": "integer" + "description": { + "description": "Album description", + "type": "string" }, "id": { - "description": "Asset ID", + "description": "Album ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "isEdited": { - "description": "Is edited", - "type": "boolean" - }, - "isFavorite": { - "description": "Is favorite", + "isActivityEnabled": { + "description": "Is activity enabled", "type": "boolean" }, - "libraryId": { - "description": "Library ID", - "nullable": true, + "name": { + "description": "Album name", "type": "string" }, - "livePhotoVideoId": { - "description": "Live photo video ID", + "order": { + "$ref": "#/components/schemas/AssetOrder" + }, + "thumbnailAssetId": { + "description": "Thumbnail asset ID", "nullable": true, "type": "string" }, - "localDateTime": { - "description": "Local date time", + "updatedAt": { + "description": "Updated at", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", - "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" - }, - "originalFileName": { - "description": "Original file name", + } + }, + "required": [ + "createdAt", + "description", + "id", + "isActivityEnabled", + "name", + "order", + "thumbnailAssetId", + "updatedAt" + ], + "type": "object" + }, + "SyncAssetDeleteV1": { + "properties": { + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "ownerId": { - "description": "Owner ID", + } + }, + "required": [ + "assetId" + ], + "type": "object" + }, + "SyncAssetEditDeleteV1": { + "properties": { + "editId": { + "description": "Edit ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" + } + }, + "required": [ + "editId" + ], + "type": "object" + }, + "SyncAssetEditV1": { + "properties": { + "action": { + "$ref": "#/components/schemas/AssetEditAction" }, - "stackId": { - "description": "Stack ID", - "nullable": true, + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "thumbhash": { - "description": "Thumbhash", - "nullable": true, + "id": { + "description": "Edit ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "type": { - "$ref": "#/components/schemas/AssetTypeEnum" - }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" + "parameters": { + "additionalProperties": {}, + "description": "Edit parameters", + "type": "object" }, - "width": { - "description": "Asset width", + "sequence": { + "description": "Edit sequence", "maximum": 9007199254740991, "minimum": -9007199254740991, - "nullable": true, "type": "integer" } }, "required": [ - "checksum", - "createdAt", - "deletedAt", - "duration", - "fileCreatedAt", - "fileModifiedAt", - "height", + "action", + "assetId", "id", - "isEdited", - "isFavorite", - "libraryId", - "livePhotoVideoId", - "localDateTime", - "originalFileName", - "ownerId", - "stackId", - "thumbhash", - "type", - "visibility", - "width" + "parameters", + "sequence" ], "type": "object" }, - "SyncAssetV2": { + "SyncAssetExifV1": { "properties": { - "checksum": { - "description": "Checksum", + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "createdAt": { - "description": "Uploaded to Immich at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", + "city": { + "description": "City", "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "deletedAt": { - "description": "Deleted at", + "country": { + "description": "Country", + "nullable": true, + "type": "string" + }, + "dateTimeOriginal": { + "description": "Date time original", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "duration": { - "description": "Duration", - "maximum": 2147483647, - "minimum": 0, + "description": { + "description": "Description", + "nullable": true, + "type": "string" + }, + "exifImageHeight": { + "description": "Exif image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "nullable": true, + "type": "integer" + }, + "exifImageWidth": { + "description": "Exif image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "nullable": true, + "type": "integer" + }, + "exposureTime": { + "description": "Exposure time", + "nullable": true, + "type": "string" + }, + "fNumber": { + "description": "F number", + "format": "double", + "nullable": true, + "type": "number" + }, + "fileSizeInByte": { + "description": "File size in byte", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, - "fileCreatedAt": { - "description": "File created at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", + "focalLength": { + "description": "Focal length", + "format": "double", "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "type": "number" }, - "fileModifiedAt": { - "description": "File modified at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", + "fps": { + "description": "FPS", + "format": "double", "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "type": "number" }, - "height": { - "description": "Asset height", + "iso": { + "description": "ISO", "maximum": 9007199254740991, "minimum": -9007199254740991, "nullable": true, "type": "integer" }, - "id": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "latitude": { + "description": "Latitude", + "format": "double", + "nullable": true, + "type": "number" }, - "isEdited": { - "description": "Is edited", - "type": "boolean" + "lensModel": { + "description": "Lens model", + "nullable": true, + "type": "string" }, - "isFavorite": { - "description": "Is favorite", - "type": "boolean" + "longitude": { + "description": "Longitude", + "format": "double", + "nullable": true, + "type": "number" }, - "libraryId": { - "description": "Library ID", + "make": { + "description": "Make", "nullable": true, "type": "string" }, - "livePhotoVideoId": { - "description": "Live photo video ID", + "model": { + "description": "Model", "nullable": true, "type": "string" }, - "localDateTime": { - "description": "Local date time", + "modifyDate": { + "description": "Modify date", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "originalFileName": { - "description": "Original file name", - "type": "string" - }, - "ownerId": { - "description": "Owner ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "orientation": { + "description": "Orientation", + "nullable": true, "type": "string" }, - "stackId": { - "description": "Stack ID", + "profileDescription": { + "description": "Profile description", "nullable": true, "type": "string" }, - "thumbhash": { - "description": "Thumbhash", + "projectionType": { + "description": "Projection type", "nullable": true, "type": "string" }, - "type": { - "$ref": "#/components/schemas/AssetTypeEnum" - }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" - }, - "width": { - "description": "Asset width", + "rating": { + "description": "Rating", "maximum": 9007199254740991, "minimum": -9007199254740991, "nullable": true, "type": "integer" + }, + "state": { + "description": "State", + "nullable": true, + "type": "string" + }, + "timeZone": { + "description": "Time zone", + "nullable": true, + "type": "string" } }, "required": [ - "checksum", - "createdAt", - "deletedAt", - "duration", - "fileCreatedAt", - "fileModifiedAt", - "height", - "id", - "isEdited", - "isFavorite", - "libraryId", - "livePhotoVideoId", - "localDateTime", - "originalFileName", - "ownerId", - "stackId", - "thumbhash", - "type", - "visibility", - "width" + "assetId", + "city", + "country", + "dateTimeOriginal", + "description", + "exifImageHeight", + "exifImageWidth", + "exposureTime", + "fNumber", + "fileSizeInByte", + "focalLength", + "fps", + "iso", + "latitude", + "lensModel", + "longitude", + "make", + "model", + "modifyDate", + "orientation", + "profileDescription", + "projectionType", + "rating", + "state", + "timeZone" ], "type": "object" }, - "SyncAuthUserV1": { + "SyncAssetFaceDeleteV1": { "properties": { - "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "nullable": true - }, - "deletedAt": { - "description": "User deleted at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "assetFaceId": { + "description": "Asset face ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "email": { - "description": "User email", + } + }, + "required": [ + "assetFaceId" + ], + "type": "object" + }, + "SyncAssetFaceV1": { + "properties": { + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "hasProfileImage": { - "description": "User has profile image", - "type": "boolean" + "boundingBoxX1": { + "description": "Bounding box X1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "boundingBoxX2": { + "description": "Bounding box X2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "boundingBoxY1": { + "description": "Bounding box Y1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "boundingBoxY2": { + "description": "Bounding box Y2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, "id": { - "description": "User ID", + "description": "Asset face ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "imageHeight": { + "description": "Image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "imageWidth": { + "description": "Image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "personId": { + "description": "Person ID", + "nullable": true, + "type": "string" + }, + "sourceType": { + "description": "Source type", + "type": "string" + } + }, + "required": [ + "assetId", + "boundingBoxX1", + "boundingBoxX2", + "boundingBoxY1", + "boundingBoxY2", + "id", + "imageHeight", + "imageWidth", + "personId", + "sourceType" + ], + "type": "object" + }, + "SyncAssetFaceV2": { + "properties": { + "assetId": { + "description": "Asset ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "isAdmin": { - "description": "User is admin", - "type": "boolean" + "boundingBoxX1": { + "description": "Bounding box X1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "name": { - "description": "User name", - "type": "string" + "boundingBoxX2": { + "description": "Bounding box X2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "oauthId": { - "description": "User OAuth ID", - "type": "string" + "boundingBoxY1": { + "description": "Bounding box Y1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "pinCode": { - "description": "User pin code", - "nullable": true, - "type": "string" + "boundingBoxY2": { + "description": "Bounding box Y2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "profileChangedAt": { - "description": "User profile changed at", + "deletedAt": { + "description": "Face deleted at", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "quotaSizeInBytes": { - "description": "Quota size in bytes", + "id": { + "description": "Asset face ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "imageHeight": { + "description": "Image height", "maximum": 9007199254740991, "minimum": -9007199254740991, - "nullable": true, "type": "integer" }, - "quotaUsageInBytes": { - "description": "Quota usage in bytes", + "imageWidth": { + "description": "Image width", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" }, - "storageLabel": { - "description": "User storage label", + "isVisible": { + "description": "Is the face visible in the asset", + "type": "boolean" + }, + "personId": { + "description": "Person ID", "nullable": true, "type": "string" + }, + "sourceType": { + "description": "Source type", + "type": "string" } }, "required": [ + "assetId", + "boundingBoxX1", + "boundingBoxX2", + "boundingBoxY1", + "boundingBoxY2", "deletedAt", - "email", - "hasProfileImage", "id", - "isAdmin", - "name", - "oauthId", - "pinCode", - "profileChangedAt", - "quotaSizeInBytes", - "quotaUsageInBytes", - "storageLabel" + "imageHeight", + "imageWidth", + "isVisible", + "personId", + "sourceType" ], "type": "object" }, - "SyncCompleteV1": { - "properties": {}, + "SyncAssetMetadataDeleteV1": { + "properties": { + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "key": { + "description": "Key", + "type": "string" + } + }, + "required": [ + "assetId", + "key" + ], "type": "object" }, - "SyncEntityType": { - "description": "Sync entity type", - "enum": [ - "AuthUserV1", - "UserV1", - "UserDeleteV1", - "AssetV1", - "AssetV2", - "AssetDeleteV1", - "AssetExifV1", - "AssetEditV1", - "AssetEditDeleteV1", - "AssetMetadataV1", - "AssetMetadataDeleteV1", - "AssetOcrV1", - "AssetOcrDeleteV1", - "PartnerV1", - "PartnerDeleteV1", - "PartnerAssetV1", - "PartnerAssetV2", - "PartnerAssetBackfillV1", - "PartnerAssetBackfillV2", - "PartnerAssetDeleteV1", - "PartnerAssetExifV1", - "PartnerAssetExifBackfillV1", - "PartnerStackBackfillV1", - "PartnerStackDeleteV1", - "PartnerStackV1", - "AlbumV1", - "AlbumV2", - "AlbumDeleteV1", - "AlbumUserV1", - "AlbumUserBackfillV1", - "AlbumUserDeleteV1", - "AlbumAssetCreateV1", - "AlbumAssetCreateV2", - "AlbumAssetUpdateV1", - "AlbumAssetUpdateV2", - "AlbumAssetBackfillV1", - "AlbumAssetBackfillV2", - "AlbumAssetExifCreateV1", - "AlbumAssetExifUpdateV1", - "AlbumAssetExifBackfillV1", - "AlbumToAssetV1", - "AlbumToAssetDeleteV1", - "AlbumToAssetBackfillV1", - "MemoryV1", - "MemoryDeleteV1", - "MemoryToAssetV1", - "MemoryToAssetDeleteV1", - "StackV1", - "StackDeleteV1", - "PersonV1", - "PersonDeleteV1", - "AssetFaceV1", - "AssetFaceV2", - "AssetFaceDeleteV1", - "UserMetadataV1", - "UserMetadataDeleteV1", - "SyncAckV1", - "SyncResetV1", - "SyncCompleteV1" + "SyncAssetMetadataV1": { + "properties": { + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "key": { + "description": "Key", + "type": "string" + }, + "value": { + "additionalProperties": {}, + "description": "Value", + "type": "object" + } + }, + "required": [ + "assetId", + "key", + "value" ], - "type": "string" + "type": "object" }, - "SyncMemoryAssetDeleteV1": { + "SyncAssetOcrDeleteV1": { + "properties": { + "assetId": { + "description": "Original asset ID of the deleted OCR entry", + "type": "string" + }, + "deletedAt": { + "description": "Timestamp when the OCR entry was deleted", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "id": { + "description": "Audit row ID of the deleted OCR entry", + "type": "string" + } + }, + "required": [ + "assetId", + "deletedAt", + "id" + ], + "type": "object" + }, + "SyncAssetOcrV1": { "properties": { "assetId": { "description": "Asset ID", @@ -25233,68 +26623,103 @@ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "memoryId": { - "description": "Memory ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - "required": [ - "assetId", - "memoryId" - ], - "type": "object" - }, - "SyncMemoryAssetV1": { - "properties": { - "assetId": { - "description": "Asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "boxScore": { + "description": "Confidence score of the bounding box", + "format": "double", + "type": "number" + }, + "id": { + "description": "OCR entry ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "isVisible": { + "description": "Whether the OCR entry is visible", + "type": "boolean" + }, + "text": { + "description": "Recognized text content", + "type": "string" + }, + "textScore": { + "description": "Confidence score of the recognized text", + "format": "double", + "type": "number" + }, + "x1": { + "description": "Top-left X coordinate (normalized 0–1)", + "format": "double", + "type": "number" + }, + "x2": { + "description": "Top-right X coordinate (normalized 0–1)", + "format": "double", + "type": "number" + }, + "x3": { + "description": "Bottom-right X coordinate (normalized 0–1)", + "format": "double", + "type": "number" + }, + "x4": { + "description": "Bottom-left X coordinate (normalized 0–1)", + "format": "double", + "type": "number" }, - "memoryId": { - "description": "Memory ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "y1": { + "description": "Top-left Y coordinate (normalized 0–1)", + "format": "double", + "type": "number" + }, + "y2": { + "description": "Top-right Y coordinate (normalized 0–1)", + "format": "double", + "type": "number" + }, + "y3": { + "description": "Bottom-right Y coordinate (normalized 0–1)", + "format": "double", + "type": "number" + }, + "y4": { + "description": "Bottom-left Y coordinate (normalized 0–1)", + "format": "double", + "type": "number" } }, "required": [ "assetId", - "memoryId" + "boxScore", + "id", + "isVisible", + "text", + "textScore", + "x1", + "x2", + "x3", + "x4", + "y1", + "y2", + "y3", + "y4" ], "type": "object" }, - "SyncMemoryDeleteV1": { + "SyncAssetV1": { "properties": { - "memoryId": { - "description": "Memory ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "checksum": { + "description": "Checksum", "type": "string" - } - }, - "required": [ - "memoryId" - ], - "type": "object" - }, - "SyncMemoryV1": { - "properties": { + }, "createdAt": { - "description": "Created at", + "description": "Uploaded to Immich at", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "data": { - "additionalProperties": {}, - "description": "Data", - "type": "object" - }, "deletedAt": { "description": "Deleted at", "example": "2024-01-01T00:00:00.000Z", @@ -25303,278 +26728,210 @@ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "hideAt": { - "description": "Hide at", + "duration": { + "description": "Duration", + "nullable": true, + "type": "string" + }, + "fileCreatedAt": { + "description": "File created at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" + }, + "fileModifiedAt": { + "description": "File modified at", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, + "height": { + "description": "Asset height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "nullable": true, + "type": "integer" + }, "id": { - "description": "Memory ID", + "description": "Asset ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "isSaved": { - "description": "Is saved", + "isEdited": { + "description": "Is edited", "type": "boolean" }, - "memoryAt": { - "description": "Memory at", + "isFavorite": { + "description": "Is favorite", + "type": "boolean" + }, + "libraryId": { + "description": "Library ID", + "nullable": true, + "type": "string" + }, + "livePhotoVideoId": { + "description": "Live photo video ID", + "nullable": true, + "type": "string" + }, + "localDateTime": { + "description": "Local date time", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, + "originalFileName": { + "description": "Original file name", + "type": "string" + }, "ownerId": { "description": "Owner ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "seenAt": { - "description": "Seen at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", + "stackId": { + "description": "Stack ID", "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "showAt": { - "description": "Show at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", + "thumbhash": { + "description": "Thumbhash", "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, "type": { - "$ref": "#/components/schemas/MemoryType" + "$ref": "#/components/schemas/AssetTypeEnum" }, - "updatedAt": { - "description": "Updated at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" + }, + "width": { + "description": "Asset width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "nullable": true, + "type": "integer" } }, "required": [ + "checksum", "createdAt", - "data", "deletedAt", - "hideAt", + "duration", + "fileCreatedAt", + "fileModifiedAt", + "height", "id", - "isSaved", - "memoryAt", + "isEdited", + "isFavorite", + "libraryId", + "livePhotoVideoId", + "localDateTime", + "originalFileName", "ownerId", - "seenAt", - "showAt", + "stackId", + "thumbhash", "type", - "updatedAt" - ], - "type": "object" - }, - "SyncPartnerDeleteV1": { - "properties": { - "sharedById": { - "description": "Shared by ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "sharedWithId": { - "description": "Shared with ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - "required": [ - "sharedById", - "sharedWithId" - ], - "type": "object" - }, - "SyncPartnerV1": { - "properties": { - "inTimeline": { - "description": "In timeline", - "type": "boolean" - }, - "sharedById": { - "description": "Shared by ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "sharedWithId": { - "description": "Shared with ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - "required": [ - "inTimeline", - "sharedById", - "sharedWithId" - ], - "type": "object" - }, - "SyncPersonDeleteV1": { - "properties": { - "personId": { - "description": "Person ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - "required": [ - "personId" + "visibility", + "width" ], "type": "object" }, - "SyncPersonV1": { + "SyncAssetV2": { "properties": { - "birthDate": { - "description": "Birth date", + "checksum": { + "description": "Checksum", + "type": "string" + }, + "createdAt": { + "description": "Uploaded to Immich at", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "color": { - "description": "Color", + "deletedAt": { + "description": "Deleted at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "createdAt": { - "description": "Created at", + "duration": { + "description": "Duration", + "maximum": 2147483647, + "minimum": 0, + "nullable": true, + "type": "integer" + }, + "fileCreatedAt": { + "description": "File created at", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "faceAssetId": { - "description": "Face asset ID", + "fileModifiedAt": { + "description": "File modified at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, + "height": { + "description": "Asset height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "nullable": true, + "type": "integer" + }, "id": { - "description": "Person ID", + "description": "Asset ID", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "isFavorite": { - "description": "Is favorite", + "isEdited": { + "description": "Is edited", "type": "boolean" }, - "isHidden": { - "description": "Is hidden", + "isFavorite": { + "description": "Is favorite", "type": "boolean" }, - "name": { - "description": "Person name", + "libraryId": { + "description": "Library ID", + "nullable": true, "type": "string" }, - "ownerId": { - "description": "Owner ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "livePhotoVideoId": { + "description": "Live photo video ID", + "nullable": true, "type": "string" }, - "updatedAt": { - "description": "Updated at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - } - }, - "required": [ - "birthDate", - "color", - "createdAt", - "faceAssetId", - "id", - "isFavorite", - "isHidden", - "name", - "ownerId", - "updatedAt" - ], - "type": "object" - }, - "SyncRequestType": { - "description": "Sync request type", - "enum": [ - "AlbumsV1", - "AlbumsV2", - "AlbumUsersV1", - "AlbumToAssetsV1", - "AlbumAssetsV1", - "AlbumAssetsV2", - "AlbumAssetExifsV1", - "AssetsV1", - "AssetsV2", - "AssetExifsV1", - "AssetEditsV1", - "AssetMetadataV1", - "AssetOcrV1", - "AuthUsersV1", - "MemoriesV1", - "MemoryToAssetsV1", - "PartnersV1", - "PartnerAssetsV1", - "PartnerAssetsV2", - "PartnerAssetExifsV1", - "PartnerStacksV1", - "StacksV1", - "UsersV1", - "PeopleV1", - "AssetFacesV1", - "AssetFacesV2", - "UserMetadataV1" - ], - "type": "string" - }, - "SyncResetV1": { - "properties": {}, - "type": "object" - }, - "SyncStackDeleteV1": { - "properties": { - "stackId": { - "description": "Stack ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - "required": [ - "stackId" - ], - "type": "object" - }, - "SyncStackV1": { - "properties": { - "createdAt": { - "description": "Created at", + "localDateTime": { + "description": "Local date time", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "nullable": true, "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "id": { - "description": "Stack ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "originalFileName": { + "description": "Original file name", "type": "string" }, "ownerId": { @@ -25583,105 +26940,55 @@ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "primaryAssetId": { - "description": "Primary asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "updatedAt": { - "description": "Updated at", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "stackId": { + "description": "Stack ID", + "nullable": true, "type": "string" - } - }, - "required": [ - "createdAt", - "id", - "ownerId", - "primaryAssetId", - "updatedAt" - ], - "type": "object" - }, - "SyncStreamDto": { - "properties": { - "reset": { - "description": "Reset sync state", - "type": "boolean" }, - "types": { - "description": "Sync request types", - "items": { - "$ref": "#/components/schemas/SyncRequestType" - }, - "type": "array" - } - }, - "required": [ - "types" - ], - "type": "object" - }, - "SyncUserDeleteV1": { - "properties": { - "userId": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "thumbhash": { + "description": "Thumbhash", + "nullable": true, "type": "string" - } - }, - "required": [ - "userId" - ], - "type": "object" - }, - "SyncUserMetadataDeleteV1": { - "properties": { - "key": { - "$ref": "#/components/schemas/UserMetadataKey" }, - "userId": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - "required": [ - "key", - "userId" - ], - "type": "object" - }, - "SyncUserMetadataV1": { - "properties": { - "key": { - "$ref": "#/components/schemas/UserMetadataKey" + "type": { + "$ref": "#/components/schemas/AssetTypeEnum" }, - "userId": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" }, - "value": { - "additionalProperties": {}, - "description": "User metadata value", - "type": "object" + "width": { + "description": "Asset width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "nullable": true, + "type": "integer" } }, "required": [ - "key", - "userId", - "value" + "checksum", + "createdAt", + "deletedAt", + "duration", + "fileCreatedAt", + "fileModifiedAt", + "height", + "id", + "isEdited", + "isFavorite", + "libraryId", + "livePhotoVideoId", + "localDateTime", + "originalFileName", + "ownerId", + "stackId", + "thumbhash", + "type", + "visibility", + "width" ], "type": "object" }, - "SyncUserV1": { + "SyncAuthUserV1": { "properties": { "avatarColor": { "allOf": [ @@ -25713,16 +27020,47 @@ "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, + "isAdmin": { + "description": "User is admin", + "type": "boolean" + }, "name": { "description": "User name", "type": "string" }, + "oauthId": { + "description": "User OAuth ID", + "type": "string" + }, + "pinCode": { + "description": "User pin code", + "nullable": true, + "type": "string" + }, "profileChangedAt": { "description": "User profile changed at", "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" + }, + "quotaSizeInBytes": { + "description": "Quota size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "nullable": true, + "type": "integer" + }, + "quotaUsageInBytes": { + "description": "Quota usage in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "storageLabel": { + "description": "User storage label", + "nullable": true, + "type": "string" } }, "required": [ @@ -25730,1139 +27068,1101 @@ "email", "hasProfileImage", "id", + "isAdmin", "name", - "profileChangedAt" - ], - "type": "object" - }, - "SystemConfigBackupsDto": { - "properties": { - "database": { - "$ref": "#/components/schemas/DatabaseBackupConfig" - } - }, - "required": [ - "database" + "oauthId", + "pinCode", + "profileChangedAt", + "quotaSizeInBytes", + "quotaUsageInBytes", + "storageLabel" ], "type": "object" }, - "SystemConfigDto": { - "description": "System configuration", - "properties": { - "backup": { - "$ref": "#/components/schemas/SystemConfigBackupsDto" - }, - "ffmpeg": { - "$ref": "#/components/schemas/SystemConfigFFmpegDto" - }, - "image": { - "$ref": "#/components/schemas/SystemConfigImageDto" - }, - "integrityChecks": { - "$ref": "#/components/schemas/SystemConfigIntegrityChecks" - }, - "job": { - "$ref": "#/components/schemas/SystemConfigJobDto" - }, - "library": { - "$ref": "#/components/schemas/SystemConfigLibraryDto" - }, - "logging": { - "$ref": "#/components/schemas/SystemConfigLoggingDto" - }, - "machineLearning": { - "$ref": "#/components/schemas/SystemConfigMachineLearningDto" - }, - "map": { - "$ref": "#/components/schemas/SystemConfigMapDto" - }, - "metadata": { - "$ref": "#/components/schemas/SystemConfigMetadataDto" - }, - "newVersionCheck": { - "$ref": "#/components/schemas/SystemConfigNewVersionCheckDto" - }, - "nightlyTasks": { - "$ref": "#/components/schemas/SystemConfigNightlyTasksDto" - }, - "notifications": { - "$ref": "#/components/schemas/SystemConfigNotificationsDto" - }, - "oauth": { - "$ref": "#/components/schemas/SystemConfigOAuthDto" - }, - "passwordLogin": { - "$ref": "#/components/schemas/SystemConfigPasswordLoginDto" - }, - "reverseGeocoding": { - "$ref": "#/components/schemas/SystemConfigReverseGeocodingDto" - }, - "server": { - "$ref": "#/components/schemas/SystemConfigServerDto" - }, - "storageTemplate": { - "$ref": "#/components/schemas/SystemConfigStorageTemplateDto" - }, - "templates": { - "$ref": "#/components/schemas/SystemConfigTemplatesDto" - }, - "theme": { - "$ref": "#/components/schemas/SystemConfigThemeDto" - }, - "trash": { - "$ref": "#/components/schemas/SystemConfigTrashDto" - }, - "user": { - "$ref": "#/components/schemas/SystemConfigUserDto" - } - }, - "required": [ - "backup", - "ffmpeg", - "image", - "integrityChecks", - "job", - "library", - "logging", - "machineLearning", - "map", - "metadata", - "newVersionCheck", - "nightlyTasks", - "notifications", - "oauth", - "passwordLogin", - "reverseGeocoding", - "server", - "storageTemplate", - "templates", - "theme", - "trash", - "user" - ], + "SyncCompleteV1": { + "properties": {}, "type": "object" }, - "SystemConfigFFmpegDto": { - "properties": { - "accel": { - "$ref": "#/components/schemas/TranscodeHWAccel" - }, - "accelDecode": { - "description": "Accelerated decode", - "type": "boolean" - }, - "acceptedAudioCodecs": { - "description": "Accepted audio codecs", - "items": { - "$ref": "#/components/schemas/AudioCodec" - }, - "type": "array" - }, - "acceptedContainers": { - "description": "Accepted containers", - "items": { - "$ref": "#/components/schemas/VideoContainer" - }, - "type": "array" - }, - "acceptedVideoCodecs": { - "description": "Accepted video codecs", - "items": { - "$ref": "#/components/schemas/VideoCodec" - }, - "type": "array" - }, - "bframes": { - "description": "B-frames", - "maximum": 16, - "minimum": -1, - "type": "integer" - }, - "cqMode": { - "$ref": "#/components/schemas/CQMode" - }, - "crf": { - "description": "CRF", - "maximum": 51, - "minimum": 0, - "type": "integer" - }, - "gopSize": { - "description": "GOP size", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "maxBitrate": { - "description": "Max bitrate", - "type": "string" - }, - "preferredHwDevice": { - "description": "Preferred hardware device", - "type": "string" - }, - "preset": { - "description": "Preset", + "SyncEntityType": { + "description": "Sync entity type", + "enum": [ + "AuthUserV1", + "UserV1", + "UserDeleteV1", + "AssetV1", + "AssetV2", + "AssetDeleteV1", + "AssetExifV1", + "AssetEditV1", + "AssetEditDeleteV1", + "AssetMetadataV1", + "AssetMetadataDeleteV1", + "AssetOcrV1", + "AssetOcrDeleteV1", + "PartnerV1", + "PartnerDeleteV1", + "PartnerAssetV1", + "PartnerAssetV2", + "PartnerAssetBackfillV1", + "PartnerAssetBackfillV2", + "PartnerAssetDeleteV1", + "PartnerAssetExifV1", + "PartnerAssetExifBackfillV1", + "PartnerStackBackfillV1", + "PartnerStackDeleteV1", + "PartnerStackV1", + "AlbumV1", + "AlbumV2", + "AlbumDeleteV1", + "AlbumUserV1", + "AlbumUserBackfillV1", + "AlbumUserDeleteV1", + "AlbumAssetCreateV1", + "AlbumAssetCreateV2", + "AlbumAssetUpdateV1", + "AlbumAssetUpdateV2", + "AlbumAssetBackfillV1", + "AlbumAssetBackfillV2", + "AlbumAssetExifCreateV1", + "AlbumAssetExifUpdateV1", + "AlbumAssetExifBackfillV1", + "AlbumToAssetV1", + "AlbumToAssetDeleteV1", + "AlbumToAssetBackfillV1", + "MemoryV1", + "MemoryDeleteV1", + "MemoryToAssetV1", + "MemoryToAssetDeleteV1", + "StackV1", + "StackDeleteV1", + "PersonV1", + "PersonDeleteV1", + "AssetFaceV1", + "AssetFaceV2", + "AssetFaceDeleteV1", + "UserMetadataV1", + "UserMetadataDeleteV1", + "SyncAckV1", + "SyncResetV1", + "SyncCompleteV1" + ], + "type": "string" + }, + "SyncMemoryAssetDeleteV1": { + "properties": { + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "realtime": { - "$ref": "#/components/schemas/SystemConfigFFmpegRealtimeDto" - }, - "refs": { - "description": "References", - "maximum": 6, - "minimum": 0, - "type": "integer" - }, - "targetAudioCodec": { - "$ref": "#/components/schemas/AudioCodec" - }, - "targetResolution": { - "description": "Target resolution", + "memoryId": { + "description": "Memory ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "targetVideoCodec": { - "$ref": "#/components/schemas/VideoCodec" - }, - "temporalAQ": { - "description": "Temporal AQ", - "type": "boolean" - }, - "threads": { - "description": "Threads", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" - }, - "tonemap": { - "$ref": "#/components/schemas/ToneMapping" - }, - "transcode": { - "$ref": "#/components/schemas/TranscodePolicy" - }, - "twoPass": { - "description": "Two pass", - "type": "boolean" } }, "required": [ - "accel", - "accelDecode", - "acceptedAudioCodecs", - "acceptedContainers", - "acceptedVideoCodecs", - "bframes", - "cqMode", - "crf", - "gopSize", - "maxBitrate", - "preferredHwDevice", - "preset", - "realtime", - "refs", - "targetAudioCodec", - "targetResolution", - "targetVideoCodec", - "temporalAQ", - "threads", - "tonemap", - "transcode", - "twoPass" + "assetId", + "memoryId" ], "type": "object" }, - "SystemConfigFFmpegRealtimeDto": { + "SyncMemoryAssetV1": { "properties": { - "enabled": { - "description": "Enable real-time HLS transcoding (alpha)", - "type": "boolean" - }, - "resolutions": { - "description": "Resolutions to use for real-time HLS transcoding", - "items": { - "$ref": "#/components/schemas/HlsVideoResolution" - }, - "type": "array" + "assetId": { + "description": "Asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "videoCodecs": { - "description": "Video codecs to use for real-time HLS transcoding", - "items": { - "$ref": "#/components/schemas/VideoCodec" - }, - "type": "array" + "memoryId": { + "description": "Memory ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "enabled", - "resolutions", - "videoCodecs" + "assetId", + "memoryId" ], "type": "object" }, - "SystemConfigFacesDto": { + "SyncMemoryDeleteV1": { "properties": { - "import": { - "description": "Import", - "type": "boolean" + "memoryId": { + "description": "Memory ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "import" + "memoryId" ], "type": "object" }, - "SystemConfigGeneratedFullsizeImageDto": { + "SyncMemoryV1": { "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" + "createdAt": { + "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "format": { - "$ref": "#/components/schemas/ImageFormat" + "data": { + "additionalProperties": {}, + "description": "Data", + "type": "object" }, - "progressive": { - "description": "Progressive", - "type": "boolean" + "deletedAt": { + "description": "Deleted at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "quality": { - "description": "Quality", - "maximum": 100, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "enabled", - "format", - "quality" - ], - "type": "object" - }, - "SystemConfigGeneratedImageDto": { - "properties": { - "format": { - "$ref": "#/components/schemas/ImageFormat" + "hideAt": { + "description": "Hide at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "progressive": { - "description": "Progressive", + "id": { + "description": "Memory ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "isSaved": { + "description": "Is saved", "type": "boolean" }, - "quality": { - "description": "Quality", - "maximum": 100, - "minimum": 1, - "type": "integer" + "memoryAt": { + "description": "Memory at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "size": { - "description": "Size", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - } - }, - "required": [ - "format", - "quality", - "size" - ], - "type": "object" - }, - "SystemConfigImageDto": { - "properties": { - "colorspace": { - "$ref": "#/components/schemas/Colorspace" + "ownerId": { + "description": "Owner ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "extractEmbedded": { - "description": "Extract embedded", - "type": "boolean" + "seenAt": { + "description": "Seen at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "fullsize": { - "$ref": "#/components/schemas/SystemConfigGeneratedFullsizeImageDto" + "showAt": { + "description": "Show at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "preview": { - "$ref": "#/components/schemas/SystemConfigGeneratedImageDto" + "type": { + "$ref": "#/components/schemas/MemoryType" }, - "thumbnail": { - "$ref": "#/components/schemas/SystemConfigGeneratedImageDto" + "updatedAt": { + "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" } }, "required": [ - "colorspace", - "extractEmbedded", - "fullsize", - "preview", - "thumbnail" + "createdAt", + "data", + "deletedAt", + "hideAt", + "id", + "isSaved", + "memoryAt", + "ownerId", + "seenAt", + "showAt", + "type", + "updatedAt" ], "type": "object" }, - "SystemConfigIntegrityChecks": { - "description": "Integrity checks config", + "SyncPartnerDeleteV1": { "properties": { - "checksumFiles": { - "$ref": "#/components/schemas/SystemConfigIntegrityChecksumJob" - }, - "missingFiles": { - "$ref": "#/components/schemas/SystemConfigIntegrityJob" + "sharedById": { + "description": "Shared by ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "untrackedFiles": { - "$ref": "#/components/schemas/SystemConfigIntegrityJob" + "sharedWithId": { + "description": "Shared with ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "checksumFiles", - "missingFiles", - "untrackedFiles" + "sharedById", + "sharedWithId" ], "type": "object" }, - "SystemConfigIntegrityChecksumJob": { - "description": "Integrity checksum job config", + "SyncPartnerV1": { "properties": { - "cronExpression": { - "description": "Cron expression for when the integrity check should run", - "type": "string" - }, - "enabled": { - "description": "Enabled", + "inTimeline": { + "description": "In timeline", "type": "boolean" }, - "percentageLimit": { - "description": "Percentage limit of the integrity checksum job", - "format": "double", - "maximum": 1, - "minimum": 0, - "type": "number" + "sharedById": { + "description": "Shared by ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "timeLimit": { - "description": "How long the integrity checksum job may run for", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" + "sharedWithId": { + "description": "Shared with ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "cronExpression", - "enabled", - "percentageLimit", - "timeLimit" + "inTimeline", + "sharedById", + "sharedWithId" ], "type": "object" }, - "SystemConfigIntegrityJob": { - "description": "Integrity job config", + "SyncPersonDeleteV1": { "properties": { - "cronExpression": { - "description": "Cron expression for when the integrity check should run", + "personId": { + "description": "Person ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" } }, "required": [ - "cronExpression", - "enabled" + "personId" ], "type": "object" }, - "SystemConfigJobDto": { + "SyncPersonV1": { "properties": { - "backgroundTask": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "editor": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "faceDetection": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "integrityCheck": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "library": { - "$ref": "#/components/schemas/JobSettingsDto" - }, - "metadataExtraction": { - "$ref": "#/components/schemas/JobSettingsDto" + "birthDate": { + "description": "Birth date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "migration": { - "$ref": "#/components/schemas/JobSettingsDto" + "color": { + "description": "Color", + "nullable": true, + "type": "string" }, - "notifications": { - "$ref": "#/components/schemas/JobSettingsDto" + "createdAt": { + "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "ocr": { - "$ref": "#/components/schemas/JobSettingsDto" + "faceAssetId": { + "description": "Face asset ID", + "nullable": true, + "type": "string" }, - "search": { - "$ref": "#/components/schemas/JobSettingsDto" + "id": { + "description": "Person ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "sidecar": { - "$ref": "#/components/schemas/JobSettingsDto" + "isFavorite": { + "description": "Is favorite", + "type": "boolean" }, - "smartSearch": { - "$ref": "#/components/schemas/JobSettingsDto" + "isHidden": { + "description": "Is hidden", + "type": "boolean" }, - "thumbnailGeneration": { - "$ref": "#/components/schemas/JobSettingsDto" + "name": { + "description": "Person name", + "type": "string" }, - "videoConversion": { - "$ref": "#/components/schemas/JobSettingsDto" + "ownerId": { + "description": "Owner ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "workflow": { - "$ref": "#/components/schemas/JobSettingsDto" + "updatedAt": { + "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" } }, "required": [ - "backgroundTask", - "editor", - "faceDetection", - "integrityCheck", - "library", - "metadataExtraction", - "migration", - "notifications", - "ocr", - "search", - "sidecar", - "smartSearch", - "thumbnailGeneration", - "videoConversion", - "workflow" + "birthDate", + "color", + "createdAt", + "faceAssetId", + "id", + "isFavorite", + "isHidden", + "name", + "ownerId", + "updatedAt" + ], + "type": "object" + }, + "SyncRequestType": { + "description": "Sync request type", + "enum": [ + "AlbumsV1", + "AlbumsV2", + "AlbumUsersV1", + "AlbumToAssetsV1", + "AlbumAssetsV1", + "AlbumAssetsV2", + "AlbumAssetExifsV1", + "AssetsV1", + "AssetsV2", + "AssetExifsV1", + "AssetEditsV1", + "AssetMetadataV1", + "AssetOcrV1", + "AuthUsersV1", + "MemoriesV1", + "MemoryToAssetsV1", + "PartnersV1", + "PartnerAssetsV1", + "PartnerAssetsV2", + "PartnerAssetExifsV1", + "PartnerStacksV1", + "StacksV1", + "UsersV1", + "PeopleV1", + "AssetFacesV1", + "AssetFacesV2", + "UserMetadataV1" ], + "type": "string" + }, + "SyncResetV1": { + "properties": {}, "type": "object" }, - "SystemConfigLibraryDto": { + "SyncStackDeleteV1": { "properties": { - "scan": { - "$ref": "#/components/schemas/SystemConfigLibraryScanDto" + "stackId": { + "description": "Stack ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + } + }, + "required": [ + "stackId" + ], + "type": "object" + }, + "SyncStackV1": { + "properties": { + "createdAt": { + "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "watch": { - "$ref": "#/components/schemas/SystemConfigLibraryWatchDto" + "id": { + "description": "Stack ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "ownerId": { + "description": "Owner ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "primaryAssetId": { + "description": "Primary asset ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "updatedAt": { + "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" } }, "required": [ - "scan", - "watch" + "createdAt", + "id", + "ownerId", + "primaryAssetId", + "updatedAt" ], "type": "object" }, - "SystemConfigLibraryScanDto": { + "SyncStreamDto": { "properties": { - "cronExpression": { - "description": "Cron expression", - "type": "string" - }, - "enabled": { - "description": "Enabled", + "reset": { + "description": "Reset sync state", "type": "boolean" + }, + "types": { + "description": "Sync request types", + "items": { + "$ref": "#/components/schemas/SyncRequestType" + }, + "type": "array" } }, "required": [ - "cronExpression", - "enabled" + "types" ], "type": "object" }, - "SystemConfigLibraryWatchDto": { + "SyncUserDeleteV1": { "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "enabled" + "userId" ], "type": "object" }, - "SystemConfigLoggingDto": { + "SyncUserMetadataDeleteV1": { "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" + "key": { + "$ref": "#/components/schemas/UserMetadataKey" }, - "level": { - "$ref": "#/components/schemas/LogLevel" + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" } }, "required": [ - "enabled", - "level" + "key", + "userId" ], "type": "object" }, - "SystemConfigMachineLearningDto": { + "SyncUserMetadataV1": { "properties": { - "availabilityChecks": { - "$ref": "#/components/schemas/MachineLearningAvailabilityChecksDto" - }, - "clip": { - "$ref": "#/components/schemas/CLIPConfig" - }, - "duplicateDetection": { - "$ref": "#/components/schemas/DuplicateDetectionConfig" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "facialRecognition": { - "$ref": "#/components/schemas/FacialRecognitionConfig" + "key": { + "$ref": "#/components/schemas/UserMetadataKey" }, - "ocr": { - "$ref": "#/components/schemas/OcrConfig" + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "urls": { - "description": "ML service URLs", - "items": { - "type": "string" - }, - "minItems": 1, - "type": "array" + "value": { + "additionalProperties": {}, + "description": "User metadata value", + "type": "object" } }, "required": [ - "availabilityChecks", - "clip", - "duplicateDetection", - "enabled", - "facialRecognition", - "ocr", - "urls" + "key", + "userId", + "value" ], "type": "object" }, - "SystemConfigMapDto": { + "SyncUserV1": { "properties": { - "darkStyle": { - "description": "Dark map style URL", - "format": "uri", + "avatarColor": { + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ], + "nullable": true + }, + "deletedAt": { + "description": "User deleted at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" }, - "enabled": { - "description": "Enabled", + "email": { + "description": "User email", + "type": "string" + }, + "hasProfileImage": { + "description": "User has profile image", "type": "boolean" }, - "lightStyle": { - "description": "Light map style URL", - "format": "uri", + "id": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "name": { + "description": "User name", + "type": "string" + }, + "profileChangedAt": { + "description": "User profile changed at", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", "type": "string" } }, "required": [ - "darkStyle", - "enabled", - "lightStyle" - ], - "type": "object" - }, - "SystemConfigMetadataDto": { - "properties": { - "faces": { - "$ref": "#/components/schemas/SystemConfigFacesDto" - } - }, - "required": [ - "faces" + "deletedAt", + "email", + "hasProfileImage", + "id", + "name", + "profileChangedAt" ], "type": "object" }, - "SystemConfigNewVersionCheckDto": { + "SystemConfigTemplateStorageOptionDto": { "properties": { - "channel": { - "$ref": "#/components/schemas/ReleaseChannel" + "dayOptions": { + "description": "Available day format options for storage template", + "items": { + "type": "string" + }, + "type": "array" }, - "enabled": { - "description": "Enabled", - "type": "boolean" + "hourOptions": { + "description": "Available hour format options for storage template", + "items": { + "type": "string" + }, + "type": "array" + }, + "minuteOptions": { + "description": "Available minute format options for storage template", + "items": { + "type": "string" + }, + "type": "array" + }, + "monthOptions": { + "description": "Available month format options for storage template", + "items": { + "type": "string" + }, + "type": "array" + }, + "presetOptions": { + "description": "Available preset template options", + "items": { + "type": "string" + }, + "type": "array" + }, + "secondOptions": { + "description": "Available second format options for storage template", + "items": { + "type": "string" + }, + "type": "array" + }, + "weekOptions": { + "description": "Available week format options for storage template", + "items": { + "type": "string" + }, + "type": "array" + }, + "yearOptions": { + "description": "Available year format options for storage template", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ - "channel", - "enabled" + "dayOptions", + "hourOptions", + "minuteOptions", + "monthOptions", + "presetOptions", + "secondOptions", + "weekOptions", + "yearOptions" ], "type": "object" }, - "SystemConfigNightlyTasksDto": { + "TagBulkAssetsDto": { "properties": { - "clusterNewFaces": { - "description": "Cluster new faces", - "type": "boolean" - }, - "databaseCleanup": { - "description": "Database cleanup", - "type": "boolean" - }, - "generateMemories": { - "description": "Generate memories", - "type": "boolean" - }, - "missingThumbnails": { - "description": "Missing thumbnails", - "type": "boolean" - }, - "startTime": { - "description": "Start time (HH:MM)", - "pattern": "^(?:[01]\\d|2[0-3]):[0-5]\\d$", - "type": "string" + "assetIds": { + "description": "Asset IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" }, - "syncQuotaUsage": { - "description": "Sync quota usage", - "type": "boolean" + "tagIds": { + "description": "Tag IDs", + "items": { + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" + }, + "type": "array" } }, "required": [ - "clusterNewFaces", - "databaseCleanup", - "generateMemories", - "missingThumbnails", - "startTime", - "syncQuotaUsage" + "assetIds", + "tagIds" ], "type": "object" }, - "SystemConfigNotificationsDto": { + "TagBulkAssetsResponseDto": { "properties": { - "smtp": { - "$ref": "#/components/schemas/SystemConfigSmtpDto" + "count": { + "description": "Number of assets tagged", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" } }, "required": [ - "smtp" + "count" ], "type": "object" }, - "SystemConfigOAuthDto": { + "TagCreateDto": { "properties": { - "allowInsecureRequests": { - "description": "Allow insecure requests", - "type": "boolean" - }, - "autoLaunch": { - "description": "Auto launch", - "type": "boolean" - }, - "autoRegister": { - "description": "Auto register", - "type": "boolean" - }, - "buttonText": { - "description": "Button text", - "type": "string" - }, - "clientId": { - "description": "Client ID", - "type": "string" - }, - "clientSecret": { - "description": "Client secret", - "type": "string" - }, - "defaultStorageQuota": { - "description": "Default storage quota", - "maximum": 9007199254740991, - "minimum": 0, + "color": { + "description": "Tag color (hex)", "nullable": true, - "type": "integer" - }, - "enabled": { - "description": "Enabled", - "type": "boolean" - }, - "endSessionEndpoint": { - "description": "End session endpoint", + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, - "issuerUrl": { - "description": "Issuer URL", + "name": { + "description": "Tag name", "type": "string" }, - "mobileOverrideEnabled": { - "description": "Mobile override enabled", - "type": "boolean" - }, - "mobileRedirectUri": { - "description": "Mobile redirect URI (set to empty string to disable)", + "parentId": { + "description": "Parent tag ID", + "format": "uuid", + "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" - }, - "profileSigningAlgorithm": { - "description": "Profile signing algorithm", + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "TagResponseDto": { + "properties": { + "color": { + "description": "Tag color (hex)", "type": "string" }, - "prompt": { - "description": "OAuth prompt parameter (e.g. select_account, login, consent)", + "createdAt": { + "description": "Creation date", + "format": "date-time", "type": "string" }, - "roleClaim": { - "description": "Role claim", + "id": { + "description": "Tag ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "scope": { - "description": "Scope", + "name": { + "description": "Tag name", "type": "string" }, - "signingAlgorithm": { - "description": "Signing algorithm", + "parentId": { + "description": "Parent tag ID", "type": "string" }, - "storageLabelClaim": { - "description": "Storage label claim", + "updatedAt": { + "description": "Last update date", + "format": "date-time", "type": "string" }, - "storageQuotaClaim": { - "description": "Storage quota claim", + "value": { + "description": "Tag value (full path)", "type": "string" - }, - "timeout": { - "description": "Timeout", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" - }, - "tokenEndpointAuthMethod": { - "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" - } - }, - "required": [ - "allowInsecureRequests", - "autoLaunch", - "autoRegister", - "buttonText", - "clientId", - "clientSecret", - "defaultStorageQuota", - "enabled", - "endSessionEndpoint", - "issuerUrl", - "mobileOverrideEnabled", - "mobileRedirectUri", - "profileSigningAlgorithm", - "prompt", - "roleClaim", - "scope", - "signingAlgorithm", - "storageLabelClaim", - "storageQuotaClaim", - "timeout", - "tokenEndpointAuthMethod" - ], - "type": "object" - }, - "SystemConfigPasswordLoginDto": { - "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, - "SystemConfigReverseGeocodingDto": { - "properties": { - "enabled": { - "description": "Enabled", - "type": "boolean" } }, "required": [ - "enabled" + "createdAt", + "id", + "name", + "updatedAt", + "value" ], "type": "object" }, - "SystemConfigServerDto": { + "TagUpdateDto": { "properties": { - "externalDomain": { - "description": "External domain", - "type": "string" - }, - "loginPageMessage": { - "description": "Login page message", + "color": { + "description": "Tag color (hex)", + "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" - }, - "publicUsers": { - "description": "Public users", - "type": "boolean" } }, - "required": [ - "externalDomain", - "loginPageMessage", - "publicUsers" - ], "type": "object" }, - "SystemConfigSmtpDto": { + "TagUpsertDto": { "properties": { - "enabled": { - "description": "Whether SMTP email notifications are enabled", - "type": "boolean" - }, - "from": { - "description": "Email address to send from", - "type": "string" - }, - "replyTo": { - "description": "Email address for replies", - "type": "string" - }, - "transport": { - "$ref": "#/components/schemas/SystemConfigSmtpTransportDto" + "tags": { + "description": "Tag names to upsert", + "items": { + "type": "string" + }, + "type": "array" } }, "required": [ - "enabled", - "from", - "replyTo", - "transport" + "tags" ], "type": "object" }, - "SystemConfigSmtpTransportDto": { + "TagsResponse": { "properties": { - "host": { - "description": "SMTP server hostname", - "type": "string" - }, - "ignoreCert": { - "description": "Whether to ignore SSL certificate errors", + "enabled": { + "description": "Whether tags are enabled", "type": "boolean" }, - "password": { - "description": "SMTP password", - "type": "string" - }, - "port": { - "description": "SMTP server port", - "maximum": 65535, - "minimum": 0, - "type": "integer" - }, - "secure": { - "description": "Whether to use secure connection (TLS/SSL)", + "sidebarWeb": { + "description": "Whether tags appear in web sidebar", "type": "boolean" - }, - "username": { - "description": "SMTP username", - "type": "string" } }, "required": [ - "host", - "ignoreCert", - "password", - "port", - "secure", - "username" + "enabled", + "sidebarWeb" ], "type": "object" }, - "SystemConfigStorageTemplateDto": { + "TagsUpdate": { "properties": { "enabled": { - "description": "Enabled", + "description": "Whether tags are enabled", "type": "boolean" }, - "hashVerificationEnabled": { - "description": "Hash verification enabled", + "sidebarWeb": { + "description": "Whether tags appear in web sidebar", "type": "boolean" - }, + } + }, + "type": "object" + }, + "TemplateDto": { + "properties": { "template": { - "description": "Template", + "description": "Template name", "type": "string" } }, "required": [ - "enabled", - "hashVerificationEnabled", "template" ], "type": "object" }, - "SystemConfigTemplateEmailsDto": { + "TemplateResponseDto": { "properties": { - "albumInviteTemplate": { - "description": "Album invite template", + "html": { + "description": "Template HTML content", "type": "string" }, - "albumUpdateTemplate": { - "description": "Album update template", + "name": { + "description": "Template name", "type": "string" - }, - "welcomeTemplate": { - "description": "Welcome template", + } + }, + "required": [ + "html", + "name" + ], + "type": "object" + }, + "TestEmailResponseDto": { + "properties": { + "messageId": { + "description": "Email message ID", "type": "string" } }, "required": [ - "albumInviteTemplate", - "albumUpdateTemplate", - "welcomeTemplate" + "messageId" ], "type": "object" }, - "SystemConfigTemplateStorageOptionDto": { + "TimeBucketAssetResponseDto": { "properties": { - "dayOptions": { - "description": "Available day format options for storage template", + "city": { + "description": "Array of city names extracted from EXIF GPS data", "items": { + "nullable": true, "type": "string" }, "type": "array" }, - "hourOptions": { - "description": "Available hour format options for storage template", + "country": { + "description": "Array of country names extracted from EXIF GPS data", "items": { + "nullable": true, "type": "string" }, "type": "array" }, - "minuteOptions": { - "description": "Available minute format options for storage template", + "createdAt": { + "description": "Array of UTC timestamps when each asset was originally uploaded to Immich", "items": { "type": "string" }, "type": "array" }, - "monthOptions": { - "description": "Available month format options for storage template", + "duration": { + "description": "Array of video/gif durations in milliseconds (null for static images)", + "items": { + "maximum": 2147483647, + "minimum": 0, + "nullable": true, + "type": "integer" + }, + "type": "array" + }, + "fileCreatedAt": { + "description": "Array of file creation timestamps in UTC", "items": { "type": "string" }, "type": "array" }, - "presetOptions": { - "description": "Available preset template options", + "id": { + "description": "Array of asset IDs in the time bucket", "items": { "type": "string" }, "type": "array" }, - "secondOptions": { - "description": "Available second format options for storage template", + "isFavorite": { + "description": "Array indicating whether each asset is favorited", + "items": { + "type": "boolean" + }, + "type": "array" + }, + "isImage": { + "description": "Array indicating whether each asset is an image (false for videos)", + "items": { + "type": "boolean" + }, + "type": "array" + }, + "isTrashed": { + "description": "Array indicating whether each asset is in the trash", + "items": { + "type": "boolean" + }, + "type": "array" + }, + "latitude": { + "description": "Array of latitude coordinates extracted from EXIF GPS data", + "items": { + "nullable": true, + "type": "number" + }, + "type": "array" + }, + "livePhotoVideoId": { + "description": "Array of live photo video asset IDs (null for non-live photos)", "items": { + "nullable": true, "type": "string" }, "type": "array" }, - "weekOptions": { - "description": "Available week format options for storage template", + "localOffsetHours": { + "description": "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.", + "items": { + "type": "number" + }, + "type": "array" + }, + "longitude": { + "description": "Array of longitude coordinates extracted from EXIF GPS data", + "items": { + "nullable": true, + "type": "number" + }, + "type": "array" + }, + "ownerId": { + "description": "Array of owner IDs for each asset", "items": { "type": "string" }, "type": "array" }, - "yearOptions": { - "description": "Available year format options for storage template", + "projectionType": { + "description": "Array of projection types for 360° content (e.g., \"EQUIRECTANGULAR\", \"CUBEFACE\", \"CYLINDRICAL\")", "items": { + "nullable": true, "type": "string" }, "type": "array" - } - }, - "required": [ - "dayOptions", - "hourOptions", - "minuteOptions", - "monthOptions", - "presetOptions", - "secondOptions", - "weekOptions", - "yearOptions" - ], - "type": "object" - }, - "SystemConfigTemplatesDto": { - "properties": { - "email": { - "$ref": "#/components/schemas/SystemConfigTemplateEmailsDto" - } - }, - "required": [ - "email" - ], - "type": "object" - }, - "SystemConfigThemeDto": { - "properties": { - "customCss": { - "description": "Custom CSS for theming", - "type": "string" - } - }, - "required": [ - "customCss" - ], - "type": "object" - }, - "SystemConfigTrashDto": { - "properties": { - "days": { - "description": "Days", - "maximum": 9007199254740991, - "minimum": 0, - "type": "integer" }, - "enabled": { - "description": "Enabled", - "type": "boolean" - } - }, - "required": [ - "days", - "enabled" - ], - "type": "object" - }, - "SystemConfigUserDto": { - "properties": { - "deleteDelay": { - "description": "Delete delay", - "maximum": 9007199254740991, - "minimum": 1, - "type": "integer" + "ratio": { + "description": "Array of aspect ratios (width/height) for each asset", + "items": { + "type": "number" + }, + "type": "array" + }, + "stack": { + "description": "Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)", + "items": { + "items": { + "type": "string" + }, + "maxItems": 2, + "minItems": 2, + "nullable": true, + "type": "array" + }, + "type": "array" + }, + "thumbhash": { + "description": "Array of BlurHash strings for generating asset previews (base64 encoded)", + "items": { + "nullable": true, + "type": "string" + }, + "type": "array" + }, + "visibility": { + "description": "Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)", + "items": { + "$ref": "#/components/schemas/AssetVisibility" + }, + "type": "array" } }, "required": [ - "deleteDelay" + "createdAt", + "duration", + "fileCreatedAt", + "id", + "isFavorite", + "isImage", + "isTrashed", + "livePhotoVideoId", + "localOffsetHours", + "ownerId", + "projectionType", + "ratio", + "thumbhash", + "visibility" ], "type": "object" }, - "TagBulkAssetsDto": { - "properties": { - "assetIds": { - "description": "Asset IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "TimeBucketsResponseDto": { + "properties": { + "count": { + "description": "Number of assets in this time bucket", + "example": 42, + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" }, - "tagIds": { - "description": "Tag IDs", - "items": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "type": "array" + "timeBucket": { + "description": "Time bucket identifier in YYYY-MM-DD format representing the start of the time period", + "example": "2024-01-01", + "type": "string" } }, "required": [ - "assetIds", - "tagIds" + "count", + "timeBucket" ], "type": "object" }, - "TagBulkAssetsResponseDto": { + "ToneMapping": { + "description": "Tone mapping", + "enum": [ + "hable", + "mobius", + "reinhard", + "disabled" + ], + "type": "string" + }, + "TranscodeHWAccel": { + "description": "Transcode hardware acceleration", + "enum": [ + "nvenc", + "qsv", + "vaapi", + "rkmpp", + "disabled" + ], + "type": "string" + }, + "TranscodePolicy": { + "description": "Transcode policy", + "enum": [ + "all", + "optimal", + "bitrate", + "required", + "disabled" + ], + "type": "string" + }, + "TrashResponseDto": { "properties": { "count": { - "description": "Number of assets tagged", + "description": "Number of items in trash", "maximum": 9007199254740991, "minimum": -9007199254740991, "type": "integer" @@ -26873,865 +28173,831 @@ ], "type": "object" }, - "TagCreateDto": { + "UpdateAlbumDto": { "properties": { - "color": { - "description": "Tag color (hex)", - "nullable": true, - "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", - "type": "string" - }, - "name": { - "description": "Tag name", + "albumName": { + "description": "Album name", "type": "string" }, - "parentId": { - "description": "Parent tag ID", + "albumThumbnailAssetId": { + "description": "Album thumbnail asset ID", "format": "uuid", - "nullable": true, "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" + }, + "description": { + "description": "Album description", + "nullable": true, + "type": "string", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v3", + "state": "Updated", + "description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4." + } + ] + }, + "isActivityEnabled": { + "description": "Enable activity feed", + "type": "boolean" + }, + "order": { + "$ref": "#/components/schemas/AssetOrder" + } + }, + "type": "object" + }, + "UpdateAlbumUserDto": { + "properties": { + "role": { + "$ref": "#/components/schemas/AlbumUserRole" } }, "required": [ - "name" + "role" ], "type": "object" }, - "TagResponseDto": { + "UpdateAssetDto": { "properties": { - "color": { - "description": "Tag color (hex)", + "dateTimeOriginal": { + "description": "Original date and time", "type": "string" }, - "createdAt": { - "description": "Creation date", - "format": "date-time", + "description": { + "description": "Asset description", "type": "string" }, - "id": { - "description": "Tag ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "isFavorite": { + "description": "Mark as favorite", + "type": "boolean" }, - "name": { - "description": "Tag name", - "type": "string" + "latitude": { + "description": "Latitude coordinate", + "maximum": 90, + "minimum": -90, + "type": "number" }, - "parentId": { - "description": "Parent tag ID", + "livePhotoVideoId": { + "description": "Live photo video ID", + "format": "uuid", + "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, - "updatedAt": { - "description": "Last update date", - "format": "date-time", - "type": "string" + "longitude": { + "description": "Longitude coordinate", + "maximum": 180, + "minimum": -180, + "type": "number" }, - "value": { - "description": "Tag value (full path)", - "type": "string" - } - }, - "required": [ - "createdAt", - "id", - "name", - "updatedAt", - "value" - ], - "type": "object" - }, - "TagUpdateDto": { - "properties": { - "color": { - "description": "Tag color (hex)", + "rating": { + "description": "Rating in range [1-5] (starred), -1 (rejected), or null (unrated)", + "maximum": 5, + "minimum": -1, "nullable": true, - "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", - "type": "string" + "type": "integer", + "x-immich-history": [ + { + "version": "v1", + "state": "Added" + }, + { + "version": "v2", + "state": "Stable" + }, + { + "version": "v3", + "state": "Updated", + "description": "Using 0 as a rating is no longer valid." + } + ], + "x-immich-state": "Stable" + }, + "visibility": { + "$ref": "#/components/schemas/AssetVisibility" } }, "type": "object" }, - "TagUpsertDto": { + "UpdateLibraryDto": { "properties": { - "tags": { - "description": "Tag names to upsert", + "exclusionPatterns": { + "description": "Exclusion patterns (max 128)", + "items": { + "type": "string" + }, + "maxItems": 128, + "type": "array" + }, + "importPaths": { + "description": "Import paths (max 128)", "items": { "type": "string" }, + "maxItems": 128, "type": "array" + }, + "name": { + "description": "Library name", + "minLength": 1, + "type": "string" } }, - "required": [ - "tags" - ], "type": "object" }, - "TagsResponse": { + "UsageByUserDto": { "properties": { - "enabled": { - "description": "Whether tags are enabled", - "type": "boolean" + "photos": { + "description": "Number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "quotaSizeInBytes": { + "description": "User quota size in bytes (null if unlimited)", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "nullable": true, + "type": "integer" + }, + "usage": { + "description": "Total storage usage in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "usagePhotos": { + "description": "Storage usage for photos in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "usageVideos": { + "description": "Storage usage for videos in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" + }, + "userId": { + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "sidebarWeb": { - "description": "Whether tags appear in web sidebar", - "type": "boolean" + "userName": { + "description": "User name", + "type": "string" + }, + "videos": { + "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, + "type": "integer" } }, "required": [ - "enabled", - "sidebarWeb" + "photos", + "quotaSizeInBytes", + "usage", + "usagePhotos", + "usageVideos", + "userId", + "userName", + "videos" ], "type": "object" }, - "TagsUpdate": { + "UserAdminCreateDto": { "properties": { - "enabled": { - "description": "Whether tags are enabled", + "avatarColor": { + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ], + "nullable": true + }, + "email": { + "description": "User email", + "format": "email", + "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", + "type": "string" + }, + "isAdmin": { + "description": "Grant admin privileges", "type": "boolean" }, - "sidebarWeb": { - "description": "Whether tags appear in web sidebar", + "name": { + "description": "User name", + "type": "string" + }, + "notify": { + "description": "Send notification email", "type": "boolean" - } - }, - "type": "object" - }, - "TemplateDto": { - "properties": { - "template": { - "description": "Template name", + }, + "password": { + "description": "User password", "type": "string" - } - }, - "required": [ - "template" - ], - "type": "object" - }, - "TemplateResponseDto": { - "properties": { - "html": { - "description": "Template HTML content", + }, + "pinCode": { + "description": "PIN code", + "example": "123456", + "nullable": true, + "pattern": "^\\d{6}$", "type": "string" }, - "name": { - "description": "Template name", + "quotaSizeInBytes": { + "description": "Storage quota in bytes", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer" + }, + "shouldChangePassword": { + "description": "Require password change on next login", + "type": "boolean" + }, + "storageLabel": { + "description": "Storage label", + "nullable": true, "type": "string" } }, "required": [ - "html", - "name" + "email", + "name", + "password" ], "type": "object" }, - "TestEmailResponseDto": { + "UserAdminDeleteDto": { "properties": { - "messageId": { - "description": "Email message ID", - "type": "string" + "force": { + "description": "Force delete even if user has assets", + "type": "boolean" } }, - "required": [ - "messageId" - ], "type": "object" }, - "TimeBucketAssetResponseDto": { + "UserAdminResponseDto": { "properties": { - "city": { - "description": "Array of city names extracted from EXIF GPS data", - "items": { - "nullable": true, - "type": "string" - }, - "type": "array" + "avatarColor": { + "$ref": "#/components/schemas/UserAvatarColor" }, - "country": { - "description": "Array of country names extracted from EXIF GPS data", - "items": { - "nullable": true, - "type": "string" - }, - "type": "array" + "clusterGroupId": { + "description": "Cluster group the user is a member of", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string", + "x-immich-history": [ + { + "version": "v3.2.0", + "state": "Added" + } + ] }, "createdAt": { - "description": "Array of UTC timestamps when each asset was originally uploaded to Immich", - "items": { - "type": "string" - }, - "type": "array" + "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "duration": { - "description": "Array of video/gif durations in milliseconds (null for static images)", - "items": { - "maximum": 2147483647, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "type": "array" + "deletedAt": { + "description": "Deletion date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" }, - "fileCreatedAt": { - "description": "Array of file creation timestamps in UTC", - "items": { - "type": "string" - }, - "type": "array" + "email": { + "description": "User email", + "format": "email", + "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", + "type": "string" }, "id": { - "description": "Array of asset IDs in the time bucket", - "items": { - "type": "string" - }, - "type": "array" - }, - "isFavorite": { - "description": "Array indicating whether each asset is favorited", - "items": { - "type": "boolean" - }, - "type": "array" + "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "type": "string" }, - "isImage": { - "description": "Array indicating whether each asset is an image (false for videos)", - "items": { - "type": "boolean" - }, - "type": "array" + "isAdmin": { + "description": "Is admin user", + "type": "boolean" }, - "isTrashed": { - "description": "Array indicating whether each asset is in the trash", - "items": { - "type": "boolean" - }, - "type": "array" + "license": { + "allOf": [ + { + "$ref": "#/components/schemas/UserLicense" + } + ], + "nullable": true }, - "latitude": { - "description": "Array of latitude coordinates extracted from EXIF GPS data", - "items": { - "nullable": true, - "type": "number" - }, - "type": "array" + "name": { + "description": "User name", + "type": "string" }, - "livePhotoVideoId": { - "description": "Array of live photo video asset IDs (null for non-live photos)", - "items": { - "nullable": true, - "type": "string" - }, - "type": "array" + "oauthId": { + "description": "OAuth ID", + "type": "string" }, - "localOffsetHours": { - "description": "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.", - "items": { - "type": "number" - }, - "type": "array" + "profileChangedAt": { + "description": "Profile change date", + "format": "date-time", + "type": "string" }, - "longitude": { - "description": "Array of longitude coordinates extracted from EXIF GPS data", - "items": { - "nullable": true, - "type": "number" - }, - "type": "array" + "profileImagePath": { + "description": "Profile image path", + "type": "string" }, - "ownerId": { - "description": "Array of owner IDs for each asset", - "items": { - "type": "string" - }, - "type": "array" + "quotaSizeInBytes": { + "description": "Storage quota in bytes", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer" }, - "projectionType": { - "description": "Array of projection types for 360° content (e.g., \"EQUIRECTANGULAR\", \"CUBEFACE\", \"CYLINDRICAL\")", - "items": { - "nullable": true, - "type": "string" - }, - "type": "array" + "quotaUsageInBytes": { + "description": "Storage usage in bytes", + "maximum": 9007199254740991, + "minimum": 0, + "nullable": true, + "type": "integer" }, - "ratio": { - "description": "Array of aspect ratios (width/height) for each asset", - "items": { - "type": "number" - }, - "type": "array" + "shouldChangePassword": { + "description": "Require password change on next login", + "type": "boolean" }, - "stack": { - "description": "Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)", - "items": { - "items": { - "type": "string" - }, - "maxItems": 2, - "minItems": 2, - "nullable": true, - "type": "array" - }, - "type": "array" + "status": { + "$ref": "#/components/schemas/UserStatus" }, - "thumbhash": { - "description": "Array of BlurHash strings for generating asset previews (base64 encoded)", - "items": { - "nullable": true, - "type": "string" - }, - "type": "array" + "storageLabel": { + "description": "Storage label", + "nullable": true, + "type": "string" }, - "visibility": { - "description": "Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)", - "items": { - "$ref": "#/components/schemas/AssetVisibility" - }, - "type": "array" + "updatedAt": { + "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", + "type": "string" } }, "required": [ + "avatarColor", + "clusterGroupId", "createdAt", - "duration", - "fileCreatedAt", + "deletedAt", + "email", "id", - "isFavorite", - "isImage", - "isTrashed", - "livePhotoVideoId", - "localOffsetHours", - "ownerId", - "projectionType", - "ratio", - "thumbhash", - "visibility" + "isAdmin", + "license", + "name", + "oauthId", + "profileChangedAt", + "profileImagePath", + "quotaSizeInBytes", + "quotaUsageInBytes", + "shouldChangePassword", + "status", + "storageLabel", + "updatedAt" ], "type": "object" }, - "TimeBucketsResponseDto": { + "UserAdminUpdateDto": { "properties": { - "count": { - "description": "Number of assets in this time bucket", - "example": 42, + "avatarColor": { + "allOf": [ + { + "$ref": "#/components/schemas/UserAvatarColor" + } + ], + "nullable": true + }, + "email": { + "description": "User email", + "format": "email", + "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", + "type": "string" + }, + "isAdmin": { + "description": "Grant admin privileges", + "type": "boolean" + }, + "name": { + "description": "User name", + "type": "string" + }, + "password": { + "description": "User password", + "type": "string" + }, + "pinCode": { + "description": "PIN code", + "example": "123456", + "nullable": true, + "pattern": "^\\d{6}$", + "type": "string" + }, + "quotaSizeInBytes": { + "description": "Storage quota in bytes", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 0, + "nullable": true, "type": "integer" }, - "timeBucket": { - "description": "Time bucket identifier in YYYY-MM-DD format representing the start of the time period", - "example": "2024-01-01", + "shouldChangePassword": { + "description": "Require password change on next login", + "type": "boolean" + }, + "storageLabel": { + "description": "Storage label", + "nullable": true, "type": "string" } }, - "required": [ - "count", - "timeBucket" - ], "type": "object" }, - "ToneMapping": { - "description": "Tone mapping", - "enum": [ - "hable", - "mobius", - "reinhard", - "disabled" - ], - "type": "string" - }, - "TranscodeHWAccel": { - "description": "Transcode hardware acceleration", - "enum": [ - "nvenc", - "qsv", - "vaapi", - "rkmpp", - "disabled" - ], - "type": "string" - }, - "TranscodePolicy": { - "description": "Transcode policy", + "UserAvatarColor": { + "description": "User avatar color", "enum": [ - "all", - "optimal", - "bitrate", - "required", - "disabled" + "primary", + "pink", + "red", + "yellow", + "blue", + "green", + "purple", + "orange", + "gray", + "amber" ], "type": "string" }, - "TrashResponseDto": { + "UserConfigClipDto": { "properties": { - "count": { - "description": "Number of items in trash", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" } }, "required": [ - "count" + "enabled" ], "type": "object" }, - "UpdateAlbumDto": { + "UserConfigDto": { + "description": "Configuration properties that are visible to a logged user", "properties": { - "albumName": { - "description": "Album name", - "type": "string" + "ffmpeg": { + "$ref": "#/components/schemas/UserConfigFFmpegDto" }, - "albumThumbnailAssetId": { - "description": "Album thumbnail asset ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" + "image": { + "$ref": "#/components/schemas/UserConfigImageDto" }, - "description": { - "description": "Album description", - "nullable": true, - "type": "string", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v3", - "state": "Updated", - "description": "Sending an empty string is deprecated; send null instead. Empty strings will no longer be coerced to null in v4." - } - ] + "machineLearning": { + "$ref": "#/components/schemas/UserConfigMachineLearningDto" }, - "isActivityEnabled": { - "description": "Enable activity feed", - "type": "boolean" + "map": { + "$ref": "#/components/schemas/UserConfigMapDto" }, - "order": { - "$ref": "#/components/schemas/AssetOrder" + "oauth": { + "$ref": "#/components/schemas/UserConfigOAuthDto" + }, + "passwordLogin": { + "$ref": "#/components/schemas/UserConfigPasswordLoginDto" + }, + "reverseGeocoding": { + "$ref": "#/components/schemas/UserConfigReverseGeocodingDto" + }, + "server": { + "$ref": "#/components/schemas/UserConfigServerDto" + }, + "theme": { + "$ref": "#/components/schemas/UserConfigThemeDto" + }, + "trash": { + "$ref": "#/components/schemas/UserConfigTrashDto" + }, + "user": { + "$ref": "#/components/schemas/UserConfigUserDto" } }, + "required": [ + "ffmpeg", + "image", + "machineLearning", + "map", + "oauth", + "passwordLogin", + "reverseGeocoding", + "server", + "theme", + "trash", + "user" + ], "type": "object" }, - "UpdateAlbumUserDto": { + "UserConfigDuplicateDetectionDto": { "properties": { - "role": { - "$ref": "#/components/schemas/AlbumUserRole" + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" } }, "required": [ - "role" + "enabled" ], "type": "object" }, - "UpdateAssetDto": { + "UserConfigFFmpegDto": { "properties": { - "dateTimeOriginal": { - "description": "Original date and time", - "type": "string" - }, - "description": { - "description": "Asset description", - "type": "string" - }, - "isFavorite": { - "description": "Mark as favorite", - "type": "boolean" - }, - "latitude": { - "description": "Latitude coordinate", - "maximum": 90, - "minimum": -90, - "type": "number" - }, - "livePhotoVideoId": { - "description": "Live photo video ID", - "format": "uuid", - "nullable": true, - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "longitude": { - "description": "Longitude coordinate", - "maximum": 180, - "minimum": -180, - "type": "number" - }, - "rating": { - "description": "Rating in range [1-5] (starred), -1 (rejected), or null (unrated)", - "maximum": 5, - "minimum": -1, - "nullable": true, - "type": "integer", - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v2", - "state": "Stable" - }, - { - "version": "v3", - "state": "Updated", - "description": "Using 0 as a rating is no longer valid." - } - ], - "x-immich-state": "Stable" - }, - "visibility": { - "$ref": "#/components/schemas/AssetVisibility" + "realtime": { + "$ref": "#/components/schemas/UserConfigFFmpegRealtimeDto" } }, + "required": [ + "realtime" + ], "type": "object" }, - "UpdateLibraryDto": { + "UserConfigFFmpegRealtimeDto": { "properties": { - "exclusionPatterns": { - "description": "Exclusion patterns (max 128)", + "enabled": { + "description": "Enable real-time HLS transcoding (alpha)", + "type": "boolean" + }, + "resolutions": { + "description": "Resolutions to use for real-time HLS transcoding", "items": { - "type": "string" + "$ref": "#/components/schemas/HlsVideoResolution" }, - "maxItems": 128, "type": "array" }, - "importPaths": { - "description": "Import paths (max 128)", + "videoCodecs": { + "description": "Video codecs to use for real-time HLS transcoding", "items": { - "type": "string" + "$ref": "#/components/schemas/VideoCodec" }, - "maxItems": 128, "type": "array" - }, - "name": { - "description": "Library name", - "minLength": 1, - "type": "string" } }, + "required": [ + "enabled", + "resolutions", + "videoCodecs" + ], "type": "object" }, - "UsageByUserDto": { + "UserConfigFacialRecognitionDto": { "properties": { - "photos": { - "description": "Number of photos", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" - }, - "quotaSizeInBytes": { - "description": "User quota size in bytes (null if unlimited)", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "nullable": true, - "type": "integer" - }, - "usage": { - "description": "Total storage usage in bytes", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "enabled": { + "description": "Whether the task is enabled", + "type": "boolean" }, - "usagePhotos": { - "description": "Storage usage for photos in bytes", + "minFaces": { + "description": "Minimum number of faces required for recognition", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 1, "type": "integer" - }, - "usageVideos": { - "description": "Storage usage for videos in bytes", + } + }, + "required": [ + "enabled", + "minFaces" + ], + "type": "object" + }, + "UserConfigGeneratedFullsizeImageDto": { + "properties": { + "enabled": { + "description": "Enabled", + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "UserConfigGeneratedImageDto": { + "properties": { + "size": { + "description": "Size", "maximum": 9007199254740991, - "minimum": -9007199254740991, + "minimum": 1, "type": "integer" + } + }, + "required": [ + "size" + ], + "type": "object" + }, + "UserConfigImageDto": { + "properties": { + "fullsize": { + "$ref": "#/components/schemas/UserConfigGeneratedFullsizeImageDto" }, - "userId": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "userName": { - "description": "User name", - "type": "string" + "preview": { + "$ref": "#/components/schemas/UserConfigGeneratedImageDto" }, - "videos": { - "description": "Number of videos", - "maximum": 9007199254740991, - "minimum": -9007199254740991, - "type": "integer" + "thumbnail": { + "$ref": "#/components/schemas/UserConfigGeneratedImageDto" } }, "required": [ - "photos", - "quotaSizeInBytes", - "usage", - "usagePhotos", - "usageVideos", - "userId", - "userName", - "videos" + "fullsize", + "preview", + "thumbnail" ], "type": "object" }, - "UserAdminCreateDto": { + "UserConfigMachineLearningDto": { "properties": { - "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "nullable": true + "clip": { + "$ref": "#/components/schemas/UserConfigClipDto" }, - "email": { - "description": "User email", - "format": "email", - "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", - "type": "string" + "duplicateDetection": { + "$ref": "#/components/schemas/UserConfigDuplicateDetectionDto" }, - "isAdmin": { - "description": "Grant admin privileges", + "enabled": { + "description": "Enabled", "type": "boolean" }, - "name": { - "description": "User name", + "facialRecognition": { + "$ref": "#/components/schemas/UserConfigFacialRecognitionDto" + }, + "ocr": { + "$ref": "#/components/schemas/UserConfigOcrDto" + } + }, + "required": [ + "clip", + "duplicateDetection", + "enabled", + "facialRecognition", + "ocr" + ], + "type": "object" + }, + "UserConfigMapDto": { + "properties": { + "darkStyle": { + "description": "Dark map style URL", + "format": "uri", "type": "string" }, - "notify": { - "description": "Send notification email", + "enabled": { + "description": "Enabled", "type": "boolean" }, - "password": { - "description": "User password", + "lightStyle": { + "description": "Light map style URL", + "format": "uri", "type": "string" + } + }, + "required": [ + "darkStyle", + "enabled", + "lightStyle" + ], + "type": "object" + }, + "UserConfigOAuthDto": { + "properties": { + "autoLaunch": { + "description": "Auto launch", + "type": "boolean" }, - "pinCode": { - "description": "PIN code", - "example": "123456", - "nullable": true, - "pattern": "^\\d{6}$", + "buttonText": { + "description": "Button text", "type": "string" }, - "quotaSizeInBytes": { - "description": "Storage quota in bytes", - "maximum": 9007199254740991, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "shouldChangePassword": { - "description": "Require password change on next login", + "enabled": { + "description": "Enabled", "type": "boolean" - }, - "storageLabel": { - "description": "Storage label", - "nullable": true, - "type": "string" } }, "required": [ - "email", - "name", - "password" + "autoLaunch", + "buttonText", + "enabled" ], "type": "object" }, - "UserAdminDeleteDto": { + "UserConfigOcrDto": { "properties": { - "force": { - "description": "Force delete even if user has assets", + "enabled": { + "description": "Whether the task is enabled", "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, - "UserAdminResponseDto": { + "UserConfigPasswordLoginDto": { "properties": { - "avatarColor": { - "$ref": "#/components/schemas/UserAvatarColor" - }, - "createdAt": { - "description": "Creation date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "deletedAt": { - "description": "Deletion date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "nullable": true, - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" - }, - "email": { - "description": "User email", - "format": "email", - "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", - "type": "string" - }, - "id": { - "description": "User ID", - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - }, - "isAdmin": { - "description": "Is admin user", - "type": "boolean" - }, - "license": { - "allOf": [ - { - "$ref": "#/components/schemas/UserLicense" - } - ], - "nullable": true - }, - "name": { - "description": "User name", - "type": "string" - }, - "oauthId": { - "description": "OAuth ID", - "type": "string" - }, - "profileChangedAt": { - "description": "Profile change date", - "format": "date-time", - "type": "string" - }, - "profileImagePath": { - "description": "Profile image path", - "type": "string" - }, - "quotaSizeInBytes": { - "description": "Storage quota in bytes", - "maximum": 9007199254740991, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "quotaUsageInBytes": { - "description": "Storage usage in bytes", - "maximum": 9007199254740991, - "minimum": 0, - "nullable": true, - "type": "integer" - }, - "shouldChangePassword": { - "description": "Require password change on next login", + "enabled": { + "description": "Enabled", "type": "boolean" - }, - "status": { - "$ref": "#/components/schemas/UserStatus" - }, - "storageLabel": { - "description": "Storage label", - "nullable": true, - "type": "string" - }, - "updatedAt": { - "description": "Last update date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$", - "type": "string" } }, "required": [ - "avatarColor", - "createdAt", - "deletedAt", - "email", - "id", - "isAdmin", - "license", - "name", - "oauthId", - "profileChangedAt", - "profileImagePath", - "quotaSizeInBytes", - "quotaUsageInBytes", - "shouldChangePassword", - "status", - "storageLabel", - "updatedAt" + "enabled" ], "type": "object" }, - "UserAdminUpdateDto": { + "UserConfigReverseGeocodingDto": { "properties": { - "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "nullable": true - }, - "email": { - "description": "User email", - "format": "email", - "pattern": "^[\\p{L}\\p{M}\\p{N}.!#$%&'*+/=?^_`{|}~-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?(?:\\.[\\p{L}\\p{N}](?:[\\p{L}\\p{M}\\p{N}-]{0,61}[\\p{L}\\p{M}\\p{N}])?)*$", - "type": "string" - }, - "isAdmin": { - "description": "Grant admin privileges", + "enabled": { + "description": "Enabled", "type": "boolean" - }, - "name": { - "description": "User name", + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "UserConfigServerDto": { + "properties": { + "externalDomain": { + "description": "External domain", "type": "string" }, - "password": { - "description": "User password", + "loginPageMessage": { + "description": "Login page message", "type": "string" }, - "pinCode": { - "description": "PIN code", - "example": "123456", - "nullable": true, - "pattern": "^\\d{6}$", + "publicUsers": { + "description": "Public users", + "type": "boolean" + } + }, + "required": [ + "externalDomain", + "loginPageMessage", + "publicUsers" + ], + "type": "object" + }, + "UserConfigThemeDto": { + "properties": { + "customCss": { + "description": "Custom CSS for theming", "type": "string" - }, - "quotaSizeInBytes": { - "description": "Storage quota in bytes", + } + }, + "required": [ + "customCss" + ], + "type": "object" + }, + "UserConfigTrashDto": { + "properties": { + "days": { + "description": "Days", "maximum": 9007199254740991, "minimum": 0, - "nullable": true, "type": "integer" }, - "shouldChangePassword": { - "description": "Require password change on next login", + "enabled": { + "description": "Enabled", "type": "boolean" - }, - "storageLabel": { - "description": "Storage label", - "nullable": true, - "type": "string" } }, + "required": [ + "days", + "enabled" + ], "type": "object" }, - "UserAvatarColor": { - "description": "User avatar color", - "enum": [ - "primary", - "pink", - "red", - "yellow", - "blue", - "green", - "purple", - "orange", - "gray", - "amber" + "UserConfigUserDto": { + "properties": { + "deleteDelay": { + "description": "Delete delay", + "maximum": 9007199254740991, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "deleteDelay" ], - "type": "string" + "type": "object" }, "UserLicense": { "properties": { diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 9a1f93cb30236..f0ee028324330 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -54,118 +54,239 @@ export type ActivityStatisticsResponseDto = { /** Number of likes */ likes: number; }; -export type DatabaseBackupDeleteDto = { - /** Backup filenames to delete */ - backups: string[]; +export type AdminConfigDatabaseBackupDto = { + /** Cron expression */ + cronExpression: string; + /** Enabled */ + enabled: boolean; + /** Keep last amount */ + keepLastAmount: number; }; -export type DatabaseBackupDto = { - /** Backup filename */ - filename: string; - /** Backup file size */ - filesize: number; - /** Backup timezone */ - timezone: string; +export type AdminConfigBackupsDto = { + database: AdminConfigDatabaseBackupDto; }; -export type DatabaseBackupListResponseDto = { - /** List of backups */ - backups: DatabaseBackupDto[]; +export type AdminConfigFFmpegRealtimeDto = { + /** Enable real-time HLS transcoding (alpha) */ + enabled: boolean; + /** Resolutions to use for real-time HLS transcoding */ + resolutions: HlsVideoResolution[]; + /** Video codecs to use for real-time HLS transcoding */ + videoCodecs: VideoCodec[]; }; -export type DatabaseBackupUploadDto = { - /** Database backup file */ - file?: Blob; +export type AdminConfigFFmpegDto = { + accel: TranscodeHWAccel; + /** Accelerated decode */ + accelDecode: boolean; + /** Accepted audio codecs */ + acceptedAudioCodecs: AudioCodec[]; + /** Accepted containers */ + acceptedContainers: VideoContainer[]; + /** Accepted video codecs */ + acceptedVideoCodecs: VideoCodec[]; + /** B-frames */ + bframes: number; + cqMode: CQMode; + /** CRF */ + crf: number; + /** GOP size */ + gopSize: number; + /** Max bitrate */ + maxBitrate: string; + /** Preferred hardware device */ + preferredHwDevice: string; + /** Preset */ + preset: string; + realtime: AdminConfigFFmpegRealtimeDto; + /** References */ + refs: number; + targetAudioCodec: AudioCodec; + /** Target resolution */ + targetResolution: string; + targetVideoCodec: VideoCodec; + /** Temporal AQ */ + temporalAQ: boolean; + /** Threads */ + threads: number; + tonemap: ToneMapping; + transcode: TranscodePolicy; + /** Two pass */ + twoPass: boolean; }; -export type IntegrityReportResponseDto = { - items: { - /** Integrity report item id */ - id: string; - /** Integrity report item path */ - path: string; - "type": IntegrityReport; - }[]; - nextCursor?: string; +export type AdminConfigGeneratedFullsizeImageDto = { + /** Enabled */ + enabled: boolean; + format: ImageFormat; + /** Progressive */ + progressive?: boolean; + /** Quality */ + quality: number; }; -export type IntegrityReportSummaryResponseDto = { - checksum_mismatch: number; - missing_file: number; - untracked_file: number; +export type AdminConfigGeneratedImageDto = { + format: ImageFormat; + /** Progressive */ + progressive?: boolean; + /** Quality */ + quality: number; + /** Size */ + size: number; }; -export type SetMaintenanceModeDto = { - action: MaintenanceAction; - /** Restore backup filename */ - restoreBackupFilename?: string; +export type AdminConfigImageDto = { + colorspace: Colorspace; + /** Extract embedded */ + extractEmbedded: boolean; + fullsize: AdminConfigGeneratedFullsizeImageDto; + preview: AdminConfigGeneratedImageDto; + thumbnail: AdminConfigGeneratedImageDto; }; -export type MaintenanceDetectInstallStorageFolderDto = { - /** Number of files in the folder */ - files: number; - folder: StorageFolder; - /** Whether the folder is readable */ - readable: boolean; - /** Whether the folder is writable */ - writable: boolean; +export type AdminConfigIntegrityChecksumJobDto = { + /** Cron expression for when the integrity check should run */ + cronExpression: string; + /** Enabled */ + enabled: boolean; + /** Percentage limit of the integrity checksum job */ + percentageLimit: number; + /** How long the integrity checksum job may run for */ + timeLimit: number; }; -export type MaintenanceDetectInstallResponseDto = { - storage: MaintenanceDetectInstallStorageFolderDto[]; +export type AdminConfigIntegrityJobDto = { + /** Cron expression for when the integrity check should run */ + cronExpression: string; + /** Enabled */ + enabled: boolean; }; -export type MaintenanceLoginDto = { - /** Maintenance token */ - token?: string; +export type AdminConfigIntegrityChecksDto = { + checksumFiles: AdminConfigIntegrityChecksumJobDto; + missingFiles: AdminConfigIntegrityJobDto; + untrackedFiles: AdminConfigIntegrityJobDto; }; -export type MaintenanceAuthDto = { - /** Maintenance username */ - username: string; +export type AdminConfigJobSettingsDto = { + /** Concurrency */ + concurrency: number; }; -export type MaintenanceStatusResponseDto = { - action: MaintenanceAction; - active: boolean; - error?: string; - progress?: number; - task?: string; +export type AdminConfigJobDto = { + backgroundTask: AdminConfigJobSettingsDto; + editor: AdminConfigJobSettingsDto; + faceDetection: AdminConfigJobSettingsDto; + integrityCheck: AdminConfigJobSettingsDto; + library: AdminConfigJobSettingsDto; + metadataExtraction: AdminConfigJobSettingsDto; + migration: AdminConfigJobSettingsDto; + notifications: AdminConfigJobSettingsDto; + ocr: AdminConfigJobSettingsDto; + search: AdminConfigJobSettingsDto; + sidecar: AdminConfigJobSettingsDto; + smartSearch: AdminConfigJobSettingsDto; + thumbnailGeneration: AdminConfigJobSettingsDto; + videoConversion: AdminConfigJobSettingsDto; + workflow: AdminConfigJobSettingsDto; +}; +export type AdminConfigLibraryScanDto = { + /** Cron expression */ + cronExpression: string; + /** Enabled */ + enabled: boolean; }; -export type NotificationCreateDto = { - /** Additional notification data */ - data?: { - [key: string]: any; - }; - /** Notification description */ - description?: string | null; - level?: NotificationLevel; - /** Date when notification was read */ - readAt?: string | null; - /** Notification title */ - title: string; - "type"?: NotificationType; - /** User ID to send notification to */ - userId: string; +export type AdminConfigLibraryWatchDto = { + /** Enabled */ + enabled: boolean; }; -export type NotificationDto = { - /** Creation date */ - createdAt: string; - /** Additional notification data */ - data?: { - [key: string]: any; - }; - /** Notification description */ - description?: string; - /** Notification ID */ - id: string; - level: NotificationLevel; - /** Date when notification was read */ - readAt?: string; - /** Notification title */ - title: string; - "type": NotificationType; +export type AdminConfigLibraryDto = { + scan: AdminConfigLibraryScanDto; + watch: AdminConfigLibraryWatchDto; }; -export type TemplateDto = { - /** Template name */ - template: string; +export type AdminConfigLoggingDto = { + /** Enabled */ + enabled: boolean; + level: LogLevel; }; -export type TemplateResponseDto = { - /** Template HTML content */ - html: string; - /** Template name */ - name: string; +export type AdminConfigMachineLearningAvailabilityChecksDto = { + /** Enabled */ + enabled: boolean; + interval: number; + timeout: number; +}; +export type AdminConfigClipDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Name of the model to use */ + modelName: string; }; -export type SystemConfigSmtpTransportDto = { +export type AdminConfigDuplicateDetectionDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Maximum distance threshold for duplicate detection */ + maxDistance: number; +}; +export type AdminConfigFacialRecognitionDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Maximum distance threshold for face recognition */ + maxDistance: number; + /** Minimum number of faces required for recognition */ + minFaces: number; + /** Minimum confidence score for face detection */ + minScore: number; + /** Name of the model to use */ + modelName: string; +}; +export type AdminConfigOcrDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Maximum resolution for OCR processing */ + maxResolution: number; + /** Minimum confidence score for text detection */ + minDetectionScore: number; + /** Minimum confidence score for text recognition */ + minRecognitionScore: number; + /** Name of the model to use */ + modelName: string; +}; +export type AdminConfigMachineLearningDto = { + availabilityChecks: AdminConfigMachineLearningAvailabilityChecksDto; + clip: AdminConfigClipDto; + duplicateDetection: AdminConfigDuplicateDetectionDto; + /** Enabled */ + enabled: boolean; + facialRecognition: AdminConfigFacialRecognitionDto; + ocr: AdminConfigOcrDto; + /** ML service URLs */ + urls: string[]; +}; +export type AdminConfigMapDto = { + /** Dark map style URL */ + darkStyle: string; + /** Enabled */ + enabled: boolean; + /** Light map style URL */ + lightStyle: string; +}; +export type AdminConfigFacesDto = { + /** Import */ + "import": boolean; +}; +export type AdminConfigMetadataDto = { + faces: AdminConfigFacesDto; +}; +export type AdminConfigNewVersionCheckDto = { + channel: ReleaseChannel; + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigNightlyTasksDto = { + /** Cluster new faces */ + clusterNewFaces: boolean; + /** Database cleanup */ + databaseCleanup: boolean; + /** Generate memories */ + generateMemories: boolean; + /** Missing thumbnails */ + missingThumbnails: boolean; + /** Start time (HH:MM) */ + startTime: string; + /** Sync quota usage */ + syncQuotaUsage: boolean; +}; +export type AdminConfigSmtpTransportDto = { /** SMTP server hostname */ host: string; /** Whether to ignore SSL certificate errors */ @@ -179,20 +300,250 @@ export type SystemConfigSmtpTransportDto = { /** SMTP username */ username: string; }; -export type SystemConfigSmtpDto = { +export type AdminConfigSmtpDto = { /** Whether SMTP email notifications are enabled */ enabled: boolean; /** Email address to send from */ "from": string; /** Email address for replies */ replyTo: string; - transport: SystemConfigSmtpTransportDto; + transport: AdminConfigSmtpTransportDto; }; -export type TestEmailResponseDto = { - /** Email message ID */ - messageId: string; +export type AdminConfigNotificationsDto = { + smtp: AdminConfigSmtpDto; }; -export type UserLicense = { +export type AdminConfigOAuthDto = { + /** Allow insecure requests */ + allowInsecureRequests: boolean; + /** Auto launch */ + autoLaunch: boolean; + /** Auto register */ + autoRegister: boolean; + /** Button text */ + buttonText: string; + /** Client ID */ + clientId: string; + /** Client secret */ + clientSecret: string; + /** Default storage quota */ + defaultStorageQuota: number | null; + /** Enabled */ + enabled: boolean; + /** End session endpoint */ + endSessionEndpoint: string; + /** Issuer URL */ + issuerUrl: string; + /** Mobile override enabled */ + mobileOverrideEnabled: boolean; + /** Mobile redirect URI (set to empty string to disable) */ + mobileRedirectUri: string; + /** Profile signing algorithm */ + profileSigningAlgorithm: string; + /** OAuth prompt parameter (e.g. select_account, login, consent) */ + prompt: string; + /** Role claim */ + roleClaim: string; + /** Scope */ + scope: string; + /** Signing algorithm */ + signingAlgorithm: string; + /** Storage label claim */ + storageLabelClaim: string; + /** Storage quota claim */ + storageQuotaClaim: string; + /** Timeout */ + timeout: number; + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; +}; +export type AdminConfigPasswordLoginDto = { + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigReverseGeocodingDto = { + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigServerDto = { + /** External domain */ + externalDomain: string; + /** Login page message */ + loginPageMessage: string; + /** Public users */ + publicUsers: boolean; +}; +export type AdminConfigStorageTemplateDto = { + /** Enabled */ + enabled: boolean; + /** Hash verification enabled */ + hashVerificationEnabled: boolean; + /** Template */ + template: string; +}; +export type AdminConfigTemplateEmailsDto = { + /** Album invite template */ + albumInviteTemplate: string; + /** Album update template */ + albumUpdateTemplate: string; + /** Welcome template */ + welcomeTemplate: string; +}; +export type AdminConfigTemplatesDto = { + email: AdminConfigTemplateEmailsDto; +}; +export type AdminConfigThemeDto = { + /** Custom CSS for theming */ + customCss: string; +}; +export type AdminConfigTrashDto = { + /** Days */ + days: number; + /** Enabled */ + enabled: boolean; +}; +export type AdminConfigUserDto = { + /** Delete delay */ + deleteDelay: number; +}; +export type AdminConfigDto = { + backup: AdminConfigBackupsDto; + ffmpeg: AdminConfigFFmpegDto; + image: AdminConfigImageDto; + integrityChecks: AdminConfigIntegrityChecksDto; + job: AdminConfigJobDto; + library: AdminConfigLibraryDto; + logging: AdminConfigLoggingDto; + machineLearning: AdminConfigMachineLearningDto; + map: AdminConfigMapDto; + metadata: AdminConfigMetadataDto; + newVersionCheck: AdminConfigNewVersionCheckDto; + nightlyTasks: AdminConfigNightlyTasksDto; + notifications: AdminConfigNotificationsDto; + oauth: AdminConfigOAuthDto; + passwordLogin: AdminConfigPasswordLoginDto; + reverseGeocoding: AdminConfigReverseGeocodingDto; + server: AdminConfigServerDto; + storageTemplate: AdminConfigStorageTemplateDto; + templates: AdminConfigTemplatesDto; + theme: AdminConfigThemeDto; + trash: AdminConfigTrashDto; + user: AdminConfigUserDto; +}; +export type DatabaseBackupDeleteDto = { + /** Backup filenames to delete */ + backups: string[]; +}; +export type DatabaseBackupDto = { + /** Backup filename */ + filename: string; + /** Backup file size */ + filesize: number; + /** Backup timezone */ + timezone: string; +}; +export type DatabaseBackupListResponseDto = { + /** List of backups */ + backups: DatabaseBackupDto[]; +}; +export type DatabaseBackupUploadDto = { + /** Database backup file */ + file?: Blob; +}; +export type IntegrityReportResponseDto = { + items: { + /** Integrity report item id */ + id: string; + /** Integrity report item path */ + path: string; + "type": IntegrityReport; + }[]; + nextCursor?: string; +}; +export type IntegrityReportSummaryResponseDto = { + checksum_mismatch: number; + missing_file: number; + untracked_file: number; +}; +export type SetMaintenanceModeDto = { + action: MaintenanceAction; + /** Restore backup filename */ + restoreBackupFilename?: string; +}; +export type MaintenanceDetectInstallStorageFolderDto = { + /** Number of files in the folder */ + files: number; + folder: StorageFolder; + /** Whether the folder is readable */ + readable: boolean; + /** Whether the folder is writable */ + writable: boolean; +}; +export type MaintenanceDetectInstallResponseDto = { + storage: MaintenanceDetectInstallStorageFolderDto[]; +}; +export type MaintenanceLoginDto = { + /** Maintenance token */ + token?: string; +}; +export type MaintenanceAuthDto = { + /** Maintenance username */ + username: string; +}; +export type MaintenanceStatusResponseDto = { + action: MaintenanceAction; + active: boolean; + error?: string; + progress?: number; + task?: string; +}; +export type NotificationCreateDto = { + /** Additional notification data */ + data?: { + [key: string]: any; + }; + /** Notification description */ + description?: string | null; + level?: NotificationLevel; + /** Date when notification was read */ + readAt?: string | null; + /** Notification title */ + title: string; + "type"?: NotificationType; + /** User ID to send notification to */ + userId: string; +}; +export type NotificationDto = { + /** Creation date */ + createdAt: string; + /** Additional notification data */ + data?: { + [key: string]: any; + }; + /** Notification description */ + description?: string; + /** Notification ID */ + id: string; + level: NotificationLevel; + /** Date when notification was read */ + readAt?: string; + /** Notification title */ + title: string; + "type": NotificationType; +}; +export type TemplateDto = { + /** Template name */ + template: string; +}; +export type TemplateResponseDto = { + /** Template HTML content */ + html: string; + /** Template name */ + name: string; +}; +export type TestEmailResponseDto = { + /** Email message ID */ + messageId: string; +}; +export type UserLicense = { /** Activation date */ activatedAt: string; /** Activation key */ @@ -202,6 +553,8 @@ export type UserLicense = { }; export type UserAdminResponseDto = { avatarColor: UserAvatarColor; + /** Cluster group the user is a member of */ + clusterGroupId: string; /** Creation date */ createdAt: string; /** Deletion date */ @@ -631,8 +984,18 @@ export type ApiKeyCreateDto = { }; export type ApiKeyCreateResponseDto = { apiKey: ApiKeyResponseDto; + /** Creation date */ + createdAt: string; + /** API key ID */ + id: string; + /** API key name */ + name: string; + /** List of permissions */ + permissions: Permission[]; /** API key secret (only shown once) */ secret: string; + /** Last update date */ + updatedAt: string; }; export type ApiKeyUpdateDto = { /** API key name */ @@ -1113,46 +1476,169 @@ export type ValidateAccessTokenResponseDto = { /** Authentication status */ authStatus: boolean; }; -export type DownloadArchiveDto = { - /** Asset IDs */ - assetIds: string[]; - /** Download edited asset if available */ - edited?: boolean; +export type ClusterGroupRequestResponseDto = { + /** Cluster group the user is invited to join */ + clusterGroupId: string; + /** Creation date */ + createdAt: string; + /** Request ID */ + id: string; + /** User the request was created for */ + userId: string; }; -export type DownloadInfoDto = { - /** Album ID to download */ - albumId?: string; - /** Archive size limit in bytes */ - archiveSize?: number; - /** Asset IDs to download */ - assetIds?: string[]; - /** User ID to download assets from */ - userId?: string; +export type ClusterGroupRequestCreateDto = { + /** User to invite into the cluster group */ + userId: string; }; -export type DownloadArchiveInfo = { - /** Asset IDs in this archive */ - assetIds: string[]; - /** Archive size in bytes */ - size: number; +export type UserConfigFFmpegRealtimeDto = { + /** Enable real-time HLS transcoding (alpha) */ + enabled: boolean; + /** Resolutions to use for real-time HLS transcoding */ + resolutions: HlsVideoResolution[]; + /** Video codecs to use for real-time HLS transcoding */ + videoCodecs: VideoCodec[]; }; -export type DownloadResponseDto = { - /** Archive information */ - archives: DownloadArchiveInfo[]; - /** Total size in bytes */ - totalSize: number; +export type UserConfigFFmpegDto = { + realtime: UserConfigFFmpegRealtimeDto; }; -export type DuplicateResponseDto = { - /** Duplicate assets */ - assets: AssetResponseDto[]; - /** Duplicate group ID */ - duplicateId: string; - /** Suggested asset IDs to keep based on file size and EXIF data */ - suggestedKeepAssetIds: string[]; +export type UserConfigGeneratedFullsizeImageDto = { + /** Enabled */ + enabled: boolean; }; -export type DuplicateResolveGroupDto = { - duplicateId: string; - /** Asset IDs to keep */ - keepAssetIds: string[]; +export type UserConfigGeneratedImageDto = { + /** Size */ + size: number; +}; +export type UserConfigImageDto = { + fullsize: UserConfigGeneratedFullsizeImageDto; + preview: UserConfigGeneratedImageDto; + thumbnail: UserConfigGeneratedImageDto; +}; +export type UserConfigClipDto = { + /** Whether the task is enabled */ + enabled: boolean; +}; +export type UserConfigDuplicateDetectionDto = { + /** Whether the task is enabled */ + enabled: boolean; +}; +export type UserConfigFacialRecognitionDto = { + /** Whether the task is enabled */ + enabled: boolean; + /** Minimum number of faces required for recognition */ + minFaces: number; +}; +export type UserConfigOcrDto = { + /** Whether the task is enabled */ + enabled: boolean; +}; +export type UserConfigMachineLearningDto = { + clip: UserConfigClipDto; + duplicateDetection: UserConfigDuplicateDetectionDto; + /** Enabled */ + enabled: boolean; + facialRecognition: UserConfigFacialRecognitionDto; + ocr: UserConfigOcrDto; +}; +export type UserConfigMapDto = { + /** Dark map style URL */ + darkStyle: string; + /** Enabled */ + enabled: boolean; + /** Light map style URL */ + lightStyle: string; +}; +export type UserConfigOAuthDto = { + /** Auto launch */ + autoLaunch: boolean; + /** Button text */ + buttonText: string; + /** Enabled */ + enabled: boolean; +}; +export type UserConfigPasswordLoginDto = { + /** Enabled */ + enabled: boolean; +}; +export type UserConfigReverseGeocodingDto = { + /** Enabled */ + enabled: boolean; +}; +export type UserConfigServerDto = { + /** External domain */ + externalDomain: string; + /** Login page message */ + loginPageMessage: string; + /** Public users */ + publicUsers: boolean; +}; +export type UserConfigThemeDto = { + /** Custom CSS for theming */ + customCss: string; +}; +export type UserConfigTrashDto = { + /** Days */ + days: number; + /** Enabled */ + enabled: boolean; +}; +export type UserConfigUserDto = { + /** Delete delay */ + deleteDelay: number; +}; +export type UserConfigDto = { + ffmpeg: UserConfigFFmpegDto; + image: UserConfigImageDto; + machineLearning: UserConfigMachineLearningDto; + map: UserConfigMapDto; + oauth: UserConfigOAuthDto; + passwordLogin: UserConfigPasswordLoginDto; + reverseGeocoding: UserConfigReverseGeocodingDto; + server: UserConfigServerDto; + theme: UserConfigThemeDto; + trash: UserConfigTrashDto; + user: UserConfigUserDto; +}; +export type DownloadArchiveDto = { + /** Asset IDs */ + assetIds: string[]; + /** Download edited asset if available */ + edited?: boolean; +}; +export type DownloadInfoDto = { + /** Album ID to download */ + albumId?: string; + /** Archive size limit in bytes */ + archiveSize?: number; + /** Asset IDs to download */ + assetIds?: string[]; + /** User ID to download assets from */ + userId?: string; +}; +export type DownloadArchiveInfo = { + /** Asset IDs in this archive */ + assetIds: string[]; + /** Archive size in bytes */ + size: number; +}; +export type DownloadResponseDto = { + /** Archive information */ + archives: DownloadArchiveInfo[]; + /** Total size in bytes */ + totalSize: number; +}; +export type DuplicateResponseDto = { + /** Duplicate assets */ + assets: AssetResponseDto[]; + /** Duplicate group ID */ + duplicateId: string; + /** Suggested asset IDs to keep based on file size and EXIF data */ + suggestedKeepAssetIds: string[]; +}; +export type DuplicateResolveGroupDto = { + duplicateId: string; + /** Asset IDs to keep */ + keepAssetIds: string[]; /** Asset IDs to trash or delete */ trashAssetIds: string[]; }; @@ -1582,6 +2068,32 @@ export type PluginTemplateResponseDto = { /** Ui hints, for example "smart-album" */ uiHints: string[]; }; +export type PublicConfigOAuthDto = { + /** Auto launch */ + autoLaunch: boolean; + /** Button text */ + buttonText: string; + /** Enabled */ + enabled: boolean; +}; +export type PublicConfigPasswordLoginDto = { + /** Enabled */ + enabled: boolean; +}; +export type PublicConfigServerDto = { + /** Login page message */ + loginPageMessage: string; +}; +export type PublicConfigThemeDto = { + /** Custom CSS for theming */ + customCss: string; +}; +export type PublicConfigDto = { + oauth: PublicConfigOAuthDto; + passwordLogin: PublicConfigPasswordLoginDto; + server: PublicConfigServerDto; + theme: PublicConfigThemeDto; +}; export type QueueResponseDto = { /** Whether the queue is paused */ isPaused: boolean; @@ -2128,516 +2640,165 @@ export type ServerVersionResponseDto = { prerelease: number | null; }; export type VersionCheckStateResponseDto = { - /** Last check timestamp */ - checkedAt: string | null; - /** Release version */ - releaseVersion: string | null; -}; -export type ServerVersionHistoryResponseDto = { - /** When this version was first seen */ - createdAt: string; - /** Version history entry ID */ - id: string; - /** Version string */ - version: string; -}; -export type SessionCreateDto = { - /** Device OS */ - deviceOS?: string; - /** Device type */ - deviceType?: string; - /** Session duration in seconds */ - duration?: number; -}; -export type SessionCreateResponseDto = { - /** App version */ - appVersion: string | null; - /** Creation date */ - createdAt: string; - /** Is current session */ - current: boolean; - /** Device OS */ - deviceOS: string; - /** Device type */ - deviceType: string; - /** Expiration date */ - expiresAt?: string; - /** Session ID */ - id: string; - /** Is pending sync reset */ - isPendingSyncReset: boolean; - /** Session token */ - token: string; - /** Last update date */ - updatedAt: string; -}; -export type SessionUpdateDto = { - /** Reset pending sync state */ - isPendingSyncReset?: boolean; -}; -export type SharedLinkResponseDto = { - album?: AlbumResponseDto; - /** Allow downloads */ - allowDownload: boolean; - /** Allow uploads */ - allowUpload: boolean; - assets: AssetResponseDto[]; - /** Creation date */ - createdAt: string; - /** Link description */ - description: string | null; - /** Expiration date */ - expiresAt: string | null; - /** Shared link ID */ - id: string; - /** Encryption key (base64url) */ - key: string; - /** Has password */ - password: string | null; - /** Show metadata */ - showMetadata: boolean; - /** Custom URL slug */ - slug: string | null; - "type": SharedLinkType; - /** Owner user ID */ - userId: string; -}; -export type SharedLinkCreateDto = { - /** Album ID (for album sharing) */ - albumId?: string; - /** Allow downloads */ - allowDownload?: boolean; - /** Allow uploads */ - allowUpload?: boolean; - /** Asset IDs (for individual assets) */ - assetIds?: string[]; - /** Link description */ - description?: string | null; - /** Expiration date */ - expiresAt?: string | null; - /** Link password */ - password?: string | null; - /** Show metadata */ - showMetadata?: boolean; - /** Custom URL slug */ - slug?: string | null; - "type": SharedLinkType; -}; -export type SharedLinkLoginDto = { - /** Shared link password */ - password: string; -}; -export type SharedLinkEditDto = { - /** Allow downloads */ - allowDownload?: boolean; - /** Allow uploads */ - allowUpload?: boolean; - /** Link description */ - description?: string | null; - /** Expiration date */ - expiresAt?: string | null; - /** Link password */ - password?: string | null; - /** Show metadata */ - showMetadata?: boolean; - /** Custom URL slug */ - slug?: string | null; -}; -export type AssetIdsDto = { - /** Asset IDs */ - assetIds: string[]; -}; -export type AssetIdsResponseDto = { - /** Asset ID */ - assetId: string; - error?: AssetIdErrorReason; - /** Whether operation succeeded */ - success: boolean; -}; -export type StackResponseDto = { - assets: AssetResponseDto[]; - /** Stack ID */ - id: string; - /** Primary asset ID */ - primaryAssetId: string; -}; -export type StackCreateDto = { - /** Asset IDs (first becomes primary, min 2) */ - assetIds: string[]; -}; -export type StackUpdateDto = { - /** Primary asset ID */ - primaryAssetId?: string; -}; -export type SyncAckDeleteDto = { - /** Sync entity types to delete acks for */ - types?: SyncEntityType[]; -}; -export type SyncAckDto = { - /** Acknowledgment ID */ - ack: string; - "type": SyncEntityType; -}; -export type SyncAckSetDto = { - /** Acknowledgment IDs (max 1000) */ - acks: string[]; -}; -export type SyncStreamDto = { - /** Reset sync state */ - reset?: boolean; - /** Sync request types */ - types: SyncRequestType[]; -}; -export type DatabaseBackupConfig = { - /** Cron expression */ - cronExpression: string; - /** Enabled */ - enabled: boolean; - /** Keep last amount */ - keepLastAmount: number; -}; -export type SystemConfigBackupsDto = { - database: DatabaseBackupConfig; -}; -export type SystemConfigFFmpegRealtimeDto = { - /** Enable real-time HLS transcoding (alpha) */ - enabled: boolean; - /** Resolutions to use for real-time HLS transcoding */ - resolutions: HlsVideoResolution[]; - /** Video codecs to use for real-time HLS transcoding */ - videoCodecs: VideoCodec[]; -}; -export type SystemConfigFFmpegDto = { - accel: TranscodeHWAccel; - /** Accelerated decode */ - accelDecode: boolean; - /** Accepted audio codecs */ - acceptedAudioCodecs: AudioCodec[]; - /** Accepted containers */ - acceptedContainers: VideoContainer[]; - /** Accepted video codecs */ - acceptedVideoCodecs: VideoCodec[]; - /** B-frames */ - bframes: number; - cqMode: CQMode; - /** CRF */ - crf: number; - /** GOP size */ - gopSize: number; - /** Max bitrate */ - maxBitrate: string; - /** Preferred hardware device */ - preferredHwDevice: string; - /** Preset */ - preset: string; - realtime: SystemConfigFFmpegRealtimeDto; - /** References */ - refs: number; - targetAudioCodec: AudioCodec; - /** Target resolution */ - targetResolution: string; - targetVideoCodec: VideoCodec; - /** Temporal AQ */ - temporalAQ: boolean; - /** Threads */ - threads: number; - tonemap: ToneMapping; - transcode: TranscodePolicy; - /** Two pass */ - twoPass: boolean; -}; -export type SystemConfigGeneratedFullsizeImageDto = { - /** Enabled */ - enabled: boolean; - format: ImageFormat; - /** Progressive */ - progressive?: boolean; - /** Quality */ - quality: number; -}; -export type SystemConfigGeneratedImageDto = { - format: ImageFormat; - /** Progressive */ - progressive?: boolean; - /** Quality */ - quality: number; - /** Size */ - size: number; -}; -export type SystemConfigImageDto = { - colorspace: Colorspace; - /** Extract embedded */ - extractEmbedded: boolean; - fullsize: SystemConfigGeneratedFullsizeImageDto; - preview: SystemConfigGeneratedImageDto; - thumbnail: SystemConfigGeneratedImageDto; -}; -export type SystemConfigIntegrityChecksumJob = { - /** Cron expression for when the integrity check should run */ - cronExpression: string; - /** Enabled */ - enabled: boolean; - /** Percentage limit of the integrity checksum job */ - percentageLimit: number; - /** How long the integrity checksum job may run for */ - timeLimit: number; -}; -export type SystemConfigIntegrityJob = { - /** Cron expression for when the integrity check should run */ - cronExpression: string; - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigIntegrityChecks = { - checksumFiles: SystemConfigIntegrityChecksumJob; - missingFiles: SystemConfigIntegrityJob; - untrackedFiles: SystemConfigIntegrityJob; -}; -export type JobSettingsDto = { - /** Concurrency */ - concurrency: number; -}; -export type SystemConfigJobDto = { - backgroundTask: JobSettingsDto; - editor: JobSettingsDto; - faceDetection: JobSettingsDto; - integrityCheck: JobSettingsDto; - library: JobSettingsDto; - metadataExtraction: JobSettingsDto; - migration: JobSettingsDto; - notifications: JobSettingsDto; - ocr: JobSettingsDto; - search: JobSettingsDto; - sidecar: JobSettingsDto; - smartSearch: JobSettingsDto; - thumbnailGeneration: JobSettingsDto; - videoConversion: JobSettingsDto; - workflow: JobSettingsDto; -}; -export type SystemConfigLibraryScanDto = { - /** Cron expression */ - cronExpression: string; - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigLibraryWatchDto = { - /** Enabled */ - enabled: boolean; -}; -export type SystemConfigLibraryDto = { - scan: SystemConfigLibraryScanDto; - watch: SystemConfigLibraryWatchDto; -}; -export type SystemConfigLoggingDto = { - /** Enabled */ - enabled: boolean; - level: LogLevel; -}; -export type MachineLearningAvailabilityChecksDto = { - /** Enabled */ - enabled: boolean; - interval: number; - timeout: number; -}; -export type ClipConfig = { - /** Whether the task is enabled */ - enabled: boolean; - /** Name of the model to use */ - modelName: string; -}; -export type DuplicateDetectionConfig = { - /** Whether the task is enabled */ - enabled: boolean; - /** Maximum distance threshold for duplicate detection */ - maxDistance: number; -}; -export type FacialRecognitionConfig = { - /** Whether the task is enabled */ - enabled: boolean; - /** Maximum distance threshold for face recognition */ - maxDistance: number; - /** Minimum number of faces required for recognition */ - minFaces: number; - /** Minimum confidence score for face detection */ - minScore: number; - /** Name of the model to use */ - modelName: string; -}; -export type OcrConfig = { - /** Whether the task is enabled */ - enabled: boolean; - /** Maximum resolution for OCR processing */ - maxResolution: number; - /** Minimum confidence score for text detection */ - minDetectionScore: number; - /** Minimum confidence score for text recognition */ - minRecognitionScore: number; - /** Name of the model to use */ - modelName: string; -}; -export type SystemConfigMachineLearningDto = { - availabilityChecks: MachineLearningAvailabilityChecksDto; - clip: ClipConfig; - duplicateDetection: DuplicateDetectionConfig; - /** Enabled */ - enabled: boolean; - facialRecognition: FacialRecognitionConfig; - ocr: OcrConfig; - /** ML service URLs */ - urls: string[]; -}; -export type SystemConfigMapDto = { - /** Dark map style URL */ - darkStyle: string; - /** Enabled */ - enabled: boolean; - /** Light map style URL */ - lightStyle: string; + /** Last check timestamp */ + checkedAt: string | null; + /** Release version */ + releaseVersion: string | null; }; -export type SystemConfigFacesDto = { - /** Import */ - "import": boolean; +export type ServerVersionHistoryResponseDto = { + /** When this version was first seen */ + createdAt: string; + /** Version history entry ID */ + id: string; + /** Version string */ + version: string; }; -export type SystemConfigMetadataDto = { - faces: SystemConfigFacesDto; +export type SessionCreateDto = { + /** Device OS */ + deviceOS?: string; + /** Device type */ + deviceType?: string; + /** Session duration in seconds */ + duration?: number; }; -export type SystemConfigNewVersionCheckDto = { - channel: ReleaseChannel; - /** Enabled */ - enabled: boolean; +export type SessionCreateResponseDto = { + /** App version */ + appVersion: string | null; + /** Creation date */ + createdAt: string; + /** Is current session */ + current: boolean; + /** Device OS */ + deviceOS: string; + /** Device type */ + deviceType: string; + /** Expiration date */ + expiresAt?: string; + /** Session ID */ + id: string; + /** Is pending sync reset */ + isPendingSyncReset: boolean; + /** Session token */ + token: string; + /** Last update date */ + updatedAt: string; }; -export type SystemConfigNightlyTasksDto = { - /** Cluster new faces */ - clusterNewFaces: boolean; - /** Database cleanup */ - databaseCleanup: boolean; - /** Generate memories */ - generateMemories: boolean; - /** Missing thumbnails */ - missingThumbnails: boolean; - /** Start time (HH:MM) */ - startTime: string; - /** Sync quota usage */ - syncQuotaUsage: boolean; +export type SessionUpdateDto = { + /** Reset pending sync state */ + isPendingSyncReset?: boolean; }; -export type SystemConfigNotificationsDto = { - smtp: SystemConfigSmtpDto; +export type SharedLinkResponseDto = { + album?: AlbumResponseDto; + /** Allow downloads */ + allowDownload: boolean; + /** Allow uploads */ + allowUpload: boolean; + assets: AssetResponseDto[]; + /** Creation date */ + createdAt: string; + /** Link description */ + description: string | null; + /** Expiration date */ + expiresAt: string | null; + /** Shared link ID */ + id: string; + /** Encryption key (base64url) */ + key: string; + /** Has password */ + password: string | null; + /** Show metadata */ + showMetadata: boolean; + /** Custom URL slug */ + slug: string | null; + "type": SharedLinkType; + /** Owner user ID */ + userId: string; }; -export type SystemConfigOAuthDto = { - /** Allow insecure requests */ - allowInsecureRequests: boolean; - /** Auto launch */ - autoLaunch: boolean; - /** Auto register */ - autoRegister: boolean; - /** Button text */ - buttonText: string; - /** Client ID */ - clientId: string; - /** Client secret */ - clientSecret: string; - /** Default storage quota */ - defaultStorageQuota: number | null; - /** Enabled */ - enabled: boolean; - /** End session endpoint */ - endSessionEndpoint: string; - /** Issuer URL */ - issuerUrl: string; - /** Mobile override enabled */ - mobileOverrideEnabled: boolean; - /** Mobile redirect URI (set to empty string to disable) */ - mobileRedirectUri: string; - /** Profile signing algorithm */ - profileSigningAlgorithm: string; - /** OAuth prompt parameter (e.g. select_account, login, consent) */ - prompt: string; - /** Role claim */ - roleClaim: string; - /** Scope */ - scope: string; - /** Signing algorithm */ - signingAlgorithm: string; - /** Storage label claim */ - storageLabelClaim: string; - /** Storage quota claim */ - storageQuotaClaim: string; - /** Timeout */ - timeout: number; - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; +export type SharedLinkCreateDto = { + /** Album ID (for album sharing) */ + albumId?: string; + /** Allow downloads */ + allowDownload?: boolean; + /** Allow uploads */ + allowUpload?: boolean; + /** Asset IDs (for individual assets) */ + assetIds?: string[]; + /** Link description */ + description?: string | null; + /** Expiration date */ + expiresAt?: string | null; + /** Link password */ + password?: string | null; + /** Show metadata */ + showMetadata?: boolean; + /** Custom URL slug */ + slug?: string | null; + "type": SharedLinkType; }; -export type SystemConfigPasswordLoginDto = { - /** Enabled */ - enabled: boolean; +export type SharedLinkLoginDto = { + /** Shared link password */ + password: string; }; -export type SystemConfigReverseGeocodingDto = { - /** Enabled */ - enabled: boolean; +export type SharedLinkEditDto = { + /** Allow downloads */ + allowDownload?: boolean; + /** Allow uploads */ + allowUpload?: boolean; + /** Link description */ + description?: string | null; + /** Expiration date */ + expiresAt?: string | null; + /** Link password */ + password?: string | null; + /** Show metadata */ + showMetadata?: boolean; + /** Custom URL slug */ + slug?: string | null; }; -export type SystemConfigServerDto = { - /** External domain */ - externalDomain: string; - /** Login page message */ - loginPageMessage: string; - /** Public users */ - publicUsers: boolean; +export type AssetIdsDto = { + /** Asset IDs */ + assetIds: string[]; }; -export type SystemConfigStorageTemplateDto = { - /** Enabled */ - enabled: boolean; - /** Hash verification enabled */ - hashVerificationEnabled: boolean; - /** Template */ - template: string; +export type AssetIdsResponseDto = { + /** Asset ID */ + assetId: string; + error?: AssetIdErrorReason; + /** Whether operation succeeded */ + success: boolean; }; -export type SystemConfigTemplateEmailsDto = { - /** Album invite template */ - albumInviteTemplate: string; - /** Album update template */ - albumUpdateTemplate: string; - /** Welcome template */ - welcomeTemplate: string; +export type StackResponseDto = { + assets: AssetResponseDto[]; + /** Stack ID */ + id: string; + /** Primary asset ID */ + primaryAssetId: string; }; -export type SystemConfigTemplatesDto = { - email: SystemConfigTemplateEmailsDto; +export type StackCreateDto = { + /** Asset IDs (first becomes primary, min 2) */ + assetIds: string[]; }; -export type SystemConfigThemeDto = { - /** Custom CSS for theming */ - customCss: string; +export type StackUpdateDto = { + /** Primary asset ID */ + primaryAssetId?: string; }; -export type SystemConfigTrashDto = { - /** Days */ - days: number; - /** Enabled */ - enabled: boolean; +export type SyncAckDeleteDto = { + /** Sync entity types to delete acks for */ + types?: SyncEntityType[]; }; -export type SystemConfigUserDto = { - /** Delete delay */ - deleteDelay: number; +export type SyncAckDto = { + /** Acknowledgment ID */ + ack: string; + "type": SyncEntityType; +}; +export type SyncAckSetDto = { + /** Acknowledgment IDs (max 1000) */ + acks: string[]; }; -export type SystemConfigDto = { - backup: SystemConfigBackupsDto; - ffmpeg: SystemConfigFFmpegDto; - image: SystemConfigImageDto; - integrityChecks: SystemConfigIntegrityChecks; - job: SystemConfigJobDto; - library: SystemConfigLibraryDto; - logging: SystemConfigLoggingDto; - machineLearning: SystemConfigMachineLearningDto; - map: SystemConfigMapDto; - metadata: SystemConfigMetadataDto; - newVersionCheck: SystemConfigNewVersionCheckDto; - nightlyTasks: SystemConfigNightlyTasksDto; - notifications: SystemConfigNotificationsDto; - oauth: SystemConfigOAuthDto; - passwordLogin: SystemConfigPasswordLoginDto; - reverseGeocoding: SystemConfigReverseGeocodingDto; - server: SystemConfigServerDto; - storageTemplate: SystemConfigStorageTemplateDto; - templates: SystemConfigTemplatesDto; - theme: SystemConfigThemeDto; - trash: SystemConfigTrashDto; - user: SystemConfigUserDto; +export type SyncStreamDto = { + /** Reset sync state */ + reset?: boolean; + /** Sync request types */ + types: SyncRequestType[]; }; export type SystemConfigTemplateStorageOptionDto = { /** Available day format options for storage template */ @@ -3423,23 +3584,60 @@ export function getActivityStatistics({ albumId, assetId }: { })); } /** - * Delete an activity + * Delete an activity + */ +export function deleteActivity({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/activities/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +/** + * Unlink all OAuth accounts + */ +export function unlinkAllOAuthAccountsAdmin(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/admin/auth/unlink-all", { + ...opts, + method: "POST" + })); +} +/** + * Get the admin configuration + */ +export function getAdminConfig(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AdminConfigDto; + }>("/admin/config", { + ...opts + })); +} +/** + * Update the system configuration */ -export function deleteActivity({ id }: { - id: string; +export function updateAdminConfig({ adminConfigDto }: { + adminConfigDto: AdminConfigDto; }, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText(`/activities/${encodeURIComponent(id)}`, { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AdminConfigDto; + }>("/admin/config", oazapfts.json({ ...opts, - method: "DELETE" - })); + method: "PUT", + body: adminConfigDto + }))); } /** - * Unlink all OAuth accounts + * Get the system configuration defaults */ -export function unlinkAllOAuthAccountsAdmin(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/admin/auth/unlink-all", { - ...opts, - method: "POST" +export function getAdminConfigDefaults(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AdminConfigDto; + }>("/admin/config/defaults", { + ...opts })); } /** @@ -3649,8 +3847,8 @@ export function getNotificationTemplateAdmin({ name, templateDto }: { /** * Send test email */ -export function sendTestEmailAdmin({ systemConfigSmtpDto }: { - systemConfigSmtpDto: SystemConfigSmtpDto; +export function sendTestEmailAdmin({ adminConfigSmtpDto }: { + adminConfigSmtpDto: AdminConfigSmtpDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -3658,7 +3856,7 @@ export function sendTestEmailAdmin({ systemConfigSmtpDto }: { }>("/admin/notifications/test-email", oazapfts.json({ ...opts, method: "POST", - body: systemConfigSmtpDto + body: adminConfigSmtpDto }))); } /** @@ -4685,6 +4883,114 @@ export function validateAccessToken(opts?: Oazapfts.RequestOpts) { method: "POST" })); } +/** + * Retrieve cluster group requests + */ +export function getClusterGroupRequests(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ClusterGroupRequestResponseDto[]; + }>("/cluster-groups/requests", { + ...opts + })); +} +/** + * Decline a cluster group request + */ +export function deleteClusterGroupRequest({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/cluster-groups/requests/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +/** + * Accept a cluster group request + */ +export function acceptClusterGroupRequest({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/cluster-groups/requests/${encodeURIComponent(id)}/accept`, { + ...opts, + method: "POST" + })); +} +/** + * Leave a cluster group + */ +export function leaveClusterGroup({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/cluster-groups/${encodeURIComponent(id)}/leave`, { + ...opts, + method: "POST" + })); +} +/** + * Retrieve the requests sent by a cluster group + */ +export function getClusterGroupRequestsForGroup({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ClusterGroupRequestResponseDto[]; + }>(`/cluster-groups/${encodeURIComponent(id)}/requests`, { + ...opts + })); +} +/** + * Create a cluster group request + */ +export function createClusterGroupRequest({ id, clusterGroupRequestCreateDto }: { + id: string; + clusterGroupRequestCreateDto: ClusterGroupRequestCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: ClusterGroupRequestResponseDto; + }>(`/cluster-groups/${encodeURIComponent(id)}/requests`, oazapfts.json({ + ...opts, + method: "PUT", + body: clusterGroupRequestCreateDto + }))); +} +/** + * Retrieve the users of a cluster group + */ +export function getClusterGroupUsers({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserResponseDto[]; + }>(`/cluster-groups/${encodeURIComponent(id)}/users`, { + ...opts + })); +} +/** + * Get the configuration with user visibility + */ +export function getUserConfig(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserConfigDto; + }>("/config", { + ...opts + })); +} +/** + * Get the default configuration with user visibility + */ +export function getUserConfigDefaults(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: UserConfigDto; + }>("/config/defaults", { + ...opts + })); +} /** * Download asset archive */ @@ -5630,6 +5936,28 @@ export function getPlugin({ id }: { ...opts })); } +/** + * Get the public configuration + */ +export function getPublicConfig(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PublicConfigDto; + }>("/public/config", { + ...opts + })); +} +/** + * Get the public configuration defaults + */ +export function getPublicConfigDefaults(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PublicConfigDto; + }>("/public/config/defaults", { + ...opts + })); +} /** * List all queues */ @@ -6440,7 +6768,7 @@ export function getSyncStream({ syncStreamDto }: { export function getConfig(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: SystemConfigDto; + data: AdminConfigDto; }>("/system-config", { ...opts })); @@ -6448,16 +6776,16 @@ export function getConfig(opts?: Oazapfts.RequestOpts) { /** * Update system configuration */ -export function updateConfig({ systemConfigDto }: { - systemConfigDto: SystemConfigDto; +export function updateConfig({ adminConfigDto }: { + adminConfigDto: AdminConfigDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: SystemConfigDto; + data: AdminConfigDto; }>("/system-config", oazapfts.json({ ...opts, method: "PUT", - body: systemConfigDto + body: adminConfigDto }))); } /** @@ -6466,7 +6794,7 @@ export function updateConfig({ systemConfigDto }: { export function getConfigDefaults(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: SystemConfigDto; + data: AdminConfigDto; }>("/system-config/defaults", { ...opts })); @@ -7154,6 +7482,80 @@ export enum UserAvatarColor { Gray = "gray", Amber = "amber" } +export enum TranscodeHWAccel { + Nvenc = "nvenc", + Qsv = "qsv", + Vaapi = "vaapi", + Rkmpp = "rkmpp", + Disabled = "disabled" +} +export enum AudioCodec { + Mp3 = "mp3", + Aac = "aac", + Opus = "opus", + PcmS16Le = "pcm_s16le" +} +export enum VideoContainer { + Mov = "mov", + Mp4 = "mp4", + Ogg = "ogg", + Webm = "webm" +} +export enum VideoCodec { + H264 = "h264", + Hevc = "hevc", + Vp9 = "vp9", + Av1 = "av1" +} +export enum CQMode { + Auto = "auto", + Cqp = "cqp", + Icq = "icq" +} +export enum HlsVideoResolution { + $480 = 480, + $720 = 720, + $1080 = 1080, + $1440 = 1440, + $2160 = 2160 +} +export enum ToneMapping { + Hable = "hable", + Mobius = "mobius", + Reinhard = "reinhard", + Disabled = "disabled" +} +export enum TranscodePolicy { + All = "all", + Optimal = "optimal", + Bitrate = "bitrate", + Required = "required", + Disabled = "disabled" +} +export enum Colorspace { + Srgb = "srgb", + P3 = "p3" +} +export enum ImageFormat { + Jpeg = "jpeg", + Webp = "webp" +} +export enum LogLevel { + Verbose = "verbose", + Debug = "debug", + Log = "log", + Warn = "warn", + Error = "error", + Fatal = "fatal" +} +export enum ReleaseChannel { + Stable = "stable", + ReleaseCandidate = "releaseCandidate" +} +export enum OAuthTokenEndpointAuthMethod { + ClientSecretPost = "client_secret_post", + ClientSecretBasic = "client_secret_basic" +} export enum IntegrityReport { UntrackedFile = "untracked_file", MissingFile = "missing_file", @@ -7185,6 +7587,7 @@ export enum NotificationType { SystemMessage = "SystemMessage", AlbumInvite = "AlbumInvite", AlbumUpdate = "AlbumUpdate", + ClusterGroupRequest = "ClusterGroupRequest", Custom = "Custom" } export enum UserStatus { @@ -7262,6 +7665,14 @@ export enum Permission { BackupDownload = "backup.download", BackupUpload = "backup.upload", BackupDelete = "backup.delete", + ClusterGroupRead = "clusterGroup.read", + ClusterGroupLeave = "clusterGroup.leave", + ClusterGroupRequestCreate = "clusterGroupRequest.create", + ClusterGroupRequestRead = "clusterGroupRequest.read", + ClusterGroupRequestDelete = "clusterGroupRequest.delete", + AdminConfigRead = "adminConfig.read", + AdminConfigUpdate = "adminConfig.update", + UserConfigRead = "userConfig.read", DuplicateRead = "duplicate.read", DuplicateDelete = "duplicate.delete", FaceCreate = "face.create", @@ -7669,80 +8080,6 @@ export enum SyncRequestType { AssetFacesV2 = "AssetFacesV2", UserMetadataV1 = "UserMetadataV1" } -export enum TranscodeHWAccel { - Nvenc = "nvenc", - Qsv = "qsv", - Vaapi = "vaapi", - Rkmpp = "rkmpp", - Disabled = "disabled" -} -export enum AudioCodec { - Mp3 = "mp3", - Aac = "aac", - Opus = "opus", - PcmS16Le = "pcm_s16le" -} -export enum VideoContainer { - Mov = "mov", - Mp4 = "mp4", - Ogg = "ogg", - Webm = "webm" -} -export enum VideoCodec { - H264 = "h264", - Hevc = "hevc", - Vp9 = "vp9", - Av1 = "av1" -} -export enum CQMode { - Auto = "auto", - Cqp = "cqp", - Icq = "icq" -} -export enum HlsVideoResolution { - $480 = 480, - $720 = 720, - $1080 = 1080, - $1440 = 1440, - $2160 = 2160 -} -export enum ToneMapping { - Hable = "hable", - Mobius = "mobius", - Reinhard = "reinhard", - Disabled = "disabled" -} -export enum TranscodePolicy { - All = "all", - Optimal = "optimal", - Bitrate = "bitrate", - Required = "required", - Disabled = "disabled" -} -export enum Colorspace { - Srgb = "srgb", - P3 = "p3" -} -export enum ImageFormat { - Jpeg = "jpeg", - Webp = "webp" -} -export enum LogLevel { - Verbose = "verbose", - Debug = "debug", - Log = "log", - Warn = "warn", - Error = "error", - Fatal = "fatal" -} -export enum ReleaseChannel { - Stable = "stable", - ReleaseCandidate = "releaseCandidate" -} -export enum OAuthTokenEndpointAuthMethod { - ClientSecretPost = "client_secret_post", - ClientSecretBasic = "client_secret_basic" -} export enum AssetOrderBy { TakenAt = "takenAt", CreatedAt = "createdAt" diff --git a/server/src/config.ts b/server/src/config.ts deleted file mode 100644 index 55304080a3dea..0000000000000 --- a/server/src/config.ts +++ /dev/null @@ -1,449 +0,0 @@ -import { CronExpression } from '@nestjs/schedule'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; -import { - AudioCodec, - Colorspace, - CQMode, - HlsVideoResolution, - ImageFormat, - LogLevel, - OAuthTokenEndpointAuthMethod, - QueueName, - ToneMapping, - TranscodeHardwareAcceleration, - TranscodePolicy, - VideoCodec, - VideoContainer, -} from 'src/enum'; -import { ConcurrentQueueName, FullsizeImageOptions, ImageOptions } from 'src/types'; - -export type SystemConfig = { - backup: { - database: { - enabled: boolean; - cronExpression: string; - keepLastAmount: number; - }; - }; - ffmpeg: { - crf: number; - threads: number; - preset: string; - targetVideoCodec: VideoCodec; - acceptedVideoCodecs: VideoCodec[]; - targetAudioCodec: AudioCodec; - acceptedAudioCodecs: AudioCodec[]; - acceptedContainers: VideoContainer[]; - targetResolution: string; - maxBitrate: string; - bframes: number; - refs: number; - gopSize: number; - temporalAQ: boolean; - cqMode: CQMode; - twoPass: boolean; - preferredHwDevice: string; - transcode: TranscodePolicy; - accel: TranscodeHardwareAcceleration; - accelDecode: boolean; - tonemap: ToneMapping; - realtime: { - enabled: boolean; - videoCodecs: VideoCodec[]; - resolutions: HlsVideoResolution[]; - }; - }; - integrityChecks: { - missingFiles: { - enabled: boolean; - cronExpression: string; - }; - untrackedFiles: { - enabled: boolean; - cronExpression: string; - }; - checksumFiles: { - enabled: boolean; - cronExpression: string; - timeLimit: number; - percentageLimit: number; - }; - }; - job: Record; - logging: { - enabled: boolean; - level: LogLevel; - }; - machineLearning: { - enabled: boolean; - urls: string[]; - availabilityChecks: { - enabled: boolean; - timeout: number; - interval: number; - }; - clip: { - enabled: boolean; - modelName: string; - }; - duplicateDetection: { - enabled: boolean; - maxDistance: number; - }; - facialRecognition: { - enabled: boolean; - modelName: string; - minScore: number; - minFaces: number; - maxDistance: number; - }; - ocr: { - enabled: boolean; - modelName: string; - minDetectionScore: number; - minRecognitionScore: number; - maxResolution: number; - }; - }; - map: { - enabled: boolean; - lightStyle: string; - darkStyle: string; - }; - reverseGeocoding: { - enabled: boolean; - }; - metadata: { - faces: { - import: boolean; - }; - }; - oauth: { - autoLaunch: boolean; - autoRegister: boolean; - buttonText: string; - clientId: string; - clientSecret: string; - defaultStorageQuota: number | null; - enabled: boolean; - issuerUrl: string; - endSessionEndpoint: string; - mobileOverrideEnabled: boolean; - mobileRedirectUri: string; - prompt: string; - scope: string; - signingAlgorithm: string; - profileSigningAlgorithm: string; - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; - timeout: number; - allowInsecureRequests: boolean; - storageLabelClaim: string; - storageQuotaClaim: string; - roleClaim: string; - }; - passwordLogin: { - enabled: boolean; - }; - storageTemplate: { - enabled: boolean; - hashVerificationEnabled: boolean; - template: string; - }; - image: { - thumbnail: ImageOptions; - preview: ImageOptions; - colorspace: Colorspace; - extractEmbedded: boolean; - fullsize: FullsizeImageOptions; - }; - newVersionCheck: { - enabled: boolean; - channel: ReleaseChannel; - }; - nightlyTasks: { - startTime: string; - databaseCleanup: boolean; - missingThumbnails: boolean; - clusterNewFaces: boolean; - generateMemories: boolean; - syncQuotaUsage: boolean; - }; - trash: { - enabled: boolean; - days: number; - }; - theme: { - customCss: string; - }; - library: { - scan: { - enabled: boolean; - cronExpression: string; - }; - watch: { - enabled: boolean; - }; - }; - notifications: { - smtp: { - enabled: boolean; - from: string; - replyTo: string; - transport: { - ignoreCert: boolean; - host: string; - port: number; - secure: boolean; - username: string; - password: string; - }; - }; - }; - templates: { - email: { - welcomeTemplate: string; - albumInviteTemplate: string; - albumUpdateTemplate: string; - }; - }; - server: { - externalDomain: string; - loginPageMessage: string; - publicUsers: boolean; - }; - user: { - deleteDelay: number; - }; -}; - -export type MachineLearningConfig = SystemConfig['machineLearning']; - -export const defaults = Object.freeze({ - backup: { - database: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_2AM, - keepLastAmount: 14, - }, - }, - ffmpeg: { - crf: 23, - threads: 0, - preset: 'ultrafast', - targetVideoCodec: VideoCodec.H264, - acceptedVideoCodecs: [VideoCodec.H264], - targetAudioCodec: AudioCodec.Aac, - acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.Opus], - acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm], - targetResolution: '720', - maxBitrate: '0', - bframes: -1, - refs: 0, - gopSize: 0, - temporalAQ: false, - cqMode: CQMode.Auto, - twoPass: false, - preferredHwDevice: 'auto', - transcode: TranscodePolicy.Required, - tonemap: ToneMapping.Hable, - accel: TranscodeHardwareAcceleration.Disabled, - accelDecode: true, - realtime: { - enabled: false, - videoCodecs: [VideoCodec.H264, VideoCodec.Hevc], - resolutions: [HlsVideoResolution.p480, HlsVideoResolution.p720, HlsVideoResolution.p1080], - }, - }, - integrityChecks: { - missingFiles: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_3AM, - }, - untrackedFiles: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_3AM, - }, - checksumFiles: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_3AM, - timeLimit: 60 * 60 * 1000, // 1 hour - percentageLimit: 1, // 100% of assets - }, - }, - job: { - [QueueName.BackgroundTask]: { concurrency: 5 }, - [QueueName.SmartSearch]: { concurrency: 2 }, - [QueueName.MetadataExtraction]: { concurrency: 5 }, - [QueueName.FaceDetection]: { concurrency: 2 }, - [QueueName.Search]: { concurrency: 5 }, - [QueueName.Sidecar]: { concurrency: 5 }, - [QueueName.Library]: { concurrency: 5 }, - [QueueName.Migration]: { concurrency: 5 }, - [QueueName.ThumbnailGeneration]: { concurrency: 3 }, - [QueueName.VideoConversion]: { concurrency: 1 }, - [QueueName.Notification]: { concurrency: 5 }, - [QueueName.Ocr]: { concurrency: 1 }, - [QueueName.Workflow]: { concurrency: 5 }, - [QueueName.IntegrityCheck]: { concurrency: 1 }, - [QueueName.Editor]: { concurrency: 2 }, - }, - logging: { - enabled: true, - level: LogLevel.Log, - }, - machineLearning: { - enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', - urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'], - availabilityChecks: { - enabled: true, - timeout: 2000, - interval: 30_000, - }, - clip: { - enabled: true, - modelName: 'ViT-B-32__openai', - }, - duplicateDetection: { - enabled: true, - maxDistance: 0.01, - }, - facialRecognition: { - enabled: true, - modelName: 'buffalo_l', - minScore: 0.7, - maxDistance: 0.5, - minFaces: 3, - }, - ocr: { - enabled: true, - modelName: 'PP-OCRv5_mobile', - minDetectionScore: 0.5, - minRecognitionScore: 0.8, - maxResolution: 736, - }, - }, - map: { - enabled: true, - lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', - darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', - }, - reverseGeocoding: { - enabled: true, - }, - metadata: { - faces: { - import: false, - }, - }, - oauth: { - autoLaunch: false, - autoRegister: true, - buttonText: 'Login with OAuth', - clientId: '', - clientSecret: '', - defaultStorageQuota: null, - enabled: false, - issuerUrl: '', - endSessionEndpoint: '', - mobileOverrideEnabled: false, - mobileRedirectUri: '', - prompt: '', - scope: 'openid email profile', - signingAlgorithm: 'RS256', - profileSigningAlgorithm: 'none', - storageLabelClaim: 'preferred_username', - storageQuotaClaim: 'immich_quota', - roleClaim: 'immich_role', - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost, - timeout: 30_000, - allowInsecureRequests: false, - }, - passwordLogin: { - enabled: true, - }, - storageTemplate: { - enabled: false, - hashVerificationEnabled: true, - template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', - }, - image: { - thumbnail: { - format: ImageFormat.Webp, - size: 250, - quality: 80, - progressive: false, - }, - preview: { - format: ImageFormat.Jpeg, - size: 1440, - quality: 80, - progressive: false, - }, - colorspace: Colorspace.P3, - extractEmbedded: false, - fullsize: { - enabled: false, - format: ImageFormat.Jpeg, - quality: 80, - progressive: false, - }, - }, - newVersionCheck: { - enabled: true, - channel: ReleaseChannel.Stable, - }, - nightlyTasks: { - startTime: '00:00', - databaseCleanup: true, - generateMemories: true, - syncQuotaUsage: true, - missingThumbnails: true, - clusterNewFaces: true, - }, - trash: { - enabled: true, - days: 30, - }, - theme: { - customCss: '', - }, - library: { - scan: { - enabled: true, - cronExpression: CronExpression.EVERY_DAY_AT_MIDNIGHT, - }, - watch: { - enabled: false, - }, - }, - server: { - externalDomain: '', - loginPageMessage: '', - publicUsers: true, - }, - notifications: { - smtp: { - enabled: false, - from: '', - replyTo: '', - transport: { - ignoreCert: false, - host: '', - port: 587, - secure: false, - username: '', - password: '', - }, - }, - }, - templates: { - email: { - welcomeTemplate: '', - albumInviteTemplate: '', - albumUpdateTemplate: '', - }, - }, - user: { - deleteDelay: 7, - }, -}); diff --git a/server/src/constants.ts b/server/src/constants.ts index 815a400ed80ae..0ab0838547d61 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -150,6 +150,11 @@ export const endpointTags: Record = { [ApiTag.Assets]: 'An asset is an image or video that has been uploaded to Immich.', [ApiTag.Authentication]: 'Endpoints related to user authentication, including OAuth.', [ApiTag.AuthenticationAdmin]: 'Administrative endpoints related to authentication.', + [ApiTag.ClusterGroups]: + 'A cluster group is a set of users whose faces are clustered together, so that a person can be shared between them.', + [ApiTag.ConfigUser]: 'The system configuration properties that are visible to logged in users.', + [ApiTag.ConfigAdmin]: 'Endpoints to view and modify the full system configuration.', + [ApiTag.ConfigPublic]: 'The system configuration properties that are visible to everyone.', [ApiTag.DatabaseBackups]: 'Manage backups of the Immich database.', [ApiTag.Deprecated]: 'Deprecated endpoints that are planned for removal in the next major release.', [ApiTag.Download]: 'Endpoints for downloading assets or collections of assets.', diff --git a/server/src/controllers/cluster-group.controller.ts b/server/src/controllers/cluster-group.controller.ts new file mode 100644 index 0000000000000..58d7fa5d08676 --- /dev/null +++ b/server/src/controllers/cluster-group.controller.ts @@ -0,0 +1,107 @@ +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Res } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Response } from 'express'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { ClusterGroupRequestCreateDto, ClusterGroupRequestResponseDto } from 'src/dtos/cluster-group.dto'; +import { UserResponseDto } from 'src/dtos/user.dto'; +import { ApiTag, Permission } from 'src/enum'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { ClusterGroupService } from 'src/services/cluster-group.service'; +import { UUIDParamDto } from 'src/validation'; + +@ApiTags(ApiTag.ClusterGroups) +@Controller('cluster-groups') +export class ClusterGroupController { + constructor(private service: ClusterGroupService) {} + + @Get('requests') + @Authenticated({ permission: Permission.ClusterGroupRequestRead }) + @Endpoint({ + summary: 'Retrieve cluster group requests', + description: 'Retrieve the pending requests for the current user to join a cluster group.', + history: new HistoryBuilder().added('v3.2.0'), + }) + getClusterGroupRequests(@Auth() auth: AuthDto): Promise { + return this.service.getRequests(auth); + } + + @Post('requests/:id/accept') + @Authenticated({ permission: Permission.ClusterGroupRequestCreate }) + @HttpCode(HttpStatus.NO_CONTENT) + @Endpoint({ + summary: 'Accept a cluster group request', + description: 'Join the cluster group the request was created for.', + history: new HistoryBuilder().added('v3.2.0'), + }) + acceptClusterGroupRequest(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.acceptRequest(auth, id); + } + + @Delete('requests/:id') + @Authenticated({ permission: Permission.ClusterGroupRequestDelete }) + @HttpCode(HttpStatus.NO_CONTENT) + @Endpoint({ + summary: 'Decline a cluster group request', + description: 'Delete a pending request to join a cluster group.', + history: new HistoryBuilder().added('v3.2.0'), + }) + deleteClusterGroupRequest(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.deleteRequest(auth, id); + } + + @Get(':id/requests') + @Authenticated({ permission: Permission.ClusterGroupRequestRead }) + @Endpoint({ + summary: 'Retrieve the requests sent by a cluster group', + description: 'Retrieve the pending requests for other users to join the cluster group.', + history: new HistoryBuilder().added('v3.2.0'), + }) + getClusterGroupRequestsForGroup( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + ): Promise { + return this.service.getRequestsForGroup(auth, id); + } + + @Get(':id/users') + @Authenticated({ permission: Permission.ClusterGroupRead }) + @Endpoint({ + summary: 'Retrieve the users of a cluster group', + description: 'Retrieve the users that are a member of the cluster group.', + history: new HistoryBuilder().added('v3.2.0'), + }) + getClusterGroupUsers(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.getUsers(auth, id); + } + + @Put(':id/requests') + @Authenticated({ permission: Permission.ClusterGroupRequestCreate }) + @Endpoint({ + summary: 'Create a cluster group request', + description: 'Ask another user to join the cluster group of the current user.', + history: new HistoryBuilder().added('v3.2.0'), + }) + async createClusterGroupRequest( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: ClusterGroupRequestCreateDto, + @Res({ passthrough: true }) res: Response, + ): Promise { + const { duplicate, value } = await this.service.createRequest(auth, id, dto); + res.status(duplicate ? HttpStatus.OK : HttpStatus.CREATED); + return value; + } + + @Post(':id/leave') + @Authenticated({ permission: Permission.ClusterGroupLeave }) + @HttpCode(HttpStatus.NO_CONTENT) + @Endpoint({ + summary: 'Leave a cluster group', + description: 'Move the current user into a new cluster group of their own.', + history: new HistoryBuilder().added('v3.2.0'), + }) + leaveClusterGroup(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.leave(auth, id); + } +} diff --git a/server/src/controllers/config-admin.controller.ts b/server/src/controllers/config-admin.controller.ts new file mode 100644 index 0000000000000..b135b0e62e109 --- /dev/null +++ b/server/src/controllers/config-admin.controller.ts @@ -0,0 +1,46 @@ +import { Body, Controller, Get, Put } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { AdminConfigDto } from 'src/dtos/config.dto'; +import { ApiTag, Permission } from 'src/enum'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; + +@ApiTags(ApiTag.ConfigAdmin) +@Controller('admin/config') +export class ConfigAdminController { + constructor(private service: SystemConfigService) {} + + @Get() + @Authenticated({ permission: Permission.AdminConfigRead, admin: true }) + @Endpoint({ + summary: 'Get the admin configuration', + description: 'Retrieve admin configuration.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getAdminConfig(): Promise { + return this.service.getAdminConfig(); + } + + @Get('defaults') + @Authenticated({ permission: Permission.AdminConfigRead, admin: true }) + @Endpoint({ + summary: 'Get the system configuration defaults', + description: 'Retrieve the default value of every system configuration property.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getAdminConfigDefaults(): AdminConfigDto { + return this.service.getAdminConfigDefaults(); + } + + @Put() + @Authenticated({ permission: Permission.AdminConfigUpdate, admin: true }) + @Endpoint({ + summary: 'Update the system configuration', + description: 'Update the system configuration with a new system configuration.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + updateAdminConfig(@Body() dto: AdminConfigDto): Promise { + return this.service.updateAdminConfig(dto); + } +} diff --git a/server/src/controllers/config-public.controller.ts b/server/src/controllers/config-public.controller.ts new file mode 100644 index 0000000000000..87f7bb14c817d --- /dev/null +++ b/server/src/controllers/config-public.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { PublicConfigDto } from 'src/dtos/config.dto'; +import { ApiTag } from 'src/enum'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; + +@ApiTags(ApiTag.ConfigPublic) +@Controller('public/config') +export class ConfigPublicController { + constructor(private service: SystemConfigService) {} + + @Get() + @Authenticated({ public: true }) + @Endpoint({ + summary: 'Get the public configuration', + description: 'Retrieve the system configuration properties that are visible to everyone.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getPublicConfig(): Promise { + return this.service.getPublicConfig(); + } + + @Get('defaults') + @Authenticated({ public: true }) + @Endpoint({ + summary: 'Get the public configuration defaults', + description: 'Retrieve the default value of the configuration properties that are visible to everyone.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getPublicConfigDefaults(): PublicConfigDto { + return this.service.getPublicConfigDefaults(); + } +} diff --git a/server/src/controllers/config-user.controller.ts b/server/src/controllers/config-user.controller.ts new file mode 100644 index 0000000000000..8f49d119fd68d --- /dev/null +++ b/server/src/controllers/config-user.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { Endpoint, HistoryBuilder } from 'src/decorators'; +import { UserConfigDto } from 'src/dtos/config.dto'; +import { ApiTag, Permission } from 'src/enum'; +import { Authenticated } from 'src/middleware/auth.guard'; +import { SystemConfigService } from 'src/services/system-config.service'; + +@ApiTags(ApiTag.ConfigUser) +@Controller('config') +export class ConfigUserController { + constructor(private service: SystemConfigService) {} + + @Get() + @Authenticated({ permission: Permission.UserConfigRead }) + @Endpoint({ + summary: 'Get the configuration with user visibility', + description: 'Retrieve the system configuration properties that are visible to logged in users.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getUserConfig(): Promise { + return this.service.getUserConfig(); + } + + @Get('defaults') + @Authenticated({ permission: Permission.UserConfigRead }) + @Endpoint({ + summary: 'Get the default configuration with user visibility', + description: 'Retrieve the default value of the configuration properties that are visible to logged in users.', + history: new HistoryBuilder().added('v3.2.0').alpha('v3.2.0'), + }) + getUserConfigDefaults(): UserConfigDto { + return this.service.getUserConfigDefaults(); + } +} diff --git a/server/src/controllers/config.controller.spec.ts b/server/src/controllers/config.controller.spec.ts new file mode 100644 index 0000000000000..fdd6a76e744db --- /dev/null +++ b/server/src/controllers/config.controller.spec.ts @@ -0,0 +1,116 @@ +import _ from 'lodash'; +import { ConfigAdminController } from 'src/controllers/config-admin.controller'; +import { ConfigPublicController } from 'src/controllers/config-public.controller'; +import { ConfigUserController } from 'src/controllers/config-user.controller'; +import { defaults, mapPublicConfig, mapUserConfig } from 'src/dtos/config.dto'; +import { SystemConfigService } from 'src/services/system-config.service'; +import request from 'supertest'; +import { errorDto } from 'test/medium/responses'; +import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; + +/** Returns a full config that passes Zod validation (required URLs and min lengths). */ +function validConfig() { + const config = _.cloneDeep(defaults) as typeof defaults & { + oauth: { mobileRedirectUri: string }; + notifications: { smtp: { from: string; transport: { host: string } } }; + server: { externalDomain: string }; + }; + config.oauth.mobileRedirectUri ||= 'https://example.com'; + config.server.externalDomain ||= 'https://example.com'; + config.notifications.smtp.from ||= 'noreply@example.com'; + config.notifications.smtp.transport.host ||= 'localhost'; + return config; +} + +describe('config controllers', () => { + let ctx: ControllerContext; + const service = mockBaseService(SystemConfigService); + + beforeAll(async () => { + ctx = await controllerSetup( + [ConfigAdminController, ConfigUserController, ConfigPublicController], + [{ provide: SystemConfigService, useValue: service }], + ); + return () => ctx.close(); + }); + + beforeEach(() => { + service.resetAllMocks(); + ctx.reset(); + }); + + describe('GET /admin/config', () => { + it('should return the full config', async () => { + service.getAdminConfig.mockResolvedValue(validConfig()); + + const { status, body } = await request(ctx.getHttpServer()).get('/admin/config'); + + expect(status).toBe(200); + expect(body.oauth.clientSecret).toBeDefined(); + expect(body.job.thumbnailGeneration).toBeDefined(); + }); + }); + + describe('PUT /admin/config', () => { + it('should accept a valid config', async () => { + service.updateAdminConfig.mockImplementation((dto) => Promise.resolve(dto)); + + const { status } = await request(ctx.getHttpServer()).put('/admin/config').send(validConfig()); + + expect(status).toBe(200); + }); + + it('should reject an invalid config', async () => { + const config = validConfig(); + config.nightlyTasks.startTime = 'invalid'; + + const { status, body } = await request(ctx.getHttpServer()).put('/admin/config').send(config); + + expect(status).toBe(400); + expect(body).toEqual( + errorDto.validationError([ + { + path: ['nightlyTasks', 'startTime'], + message: 'Invalid input: expected string in HH:MM format, received string', + }, + ]), + ); + expect(service.updateAdminConfig).not.toHaveBeenCalled(); + }); + }); + + describe('GET /config', () => { + it('should return the properties visible to logged in users', async () => { + service.getUserConfig.mockResolvedValue(mapUserConfig(validConfig())); + + const { status, body } = await request(ctx.getHttpServer()).get('/config'); + + expect(status).toBe(200); + expect(body.image).toEqual({ + thumbnail: { size: defaults.image.thumbnail.size }, + preview: { size: defaults.image.preview.size }, + fullsize: { enabled: defaults.image.fullsize.enabled }, + }); + expect(body.oauth.clientSecret).toBeUndefined(); + expect(body.job).toBeUndefined(); + }); + }); + + describe('GET /public/config', () => { + it('should return the properties visible to everyone', async () => { + service.getPublicConfig.mockResolvedValue(mapPublicConfig(validConfig())); + + const { status, body } = await request(ctx.getHttpServer()).get('/public/config'); + + expect(status).toBe(200); + expect(body.server).toEqual({ loginPageMessage: defaults.server.loginPageMessage }); + expect(body.oauth).toEqual({ + autoLaunch: defaults.oauth.autoLaunch, + buttonText: defaults.oauth.buttonText, + enabled: defaults.oauth.enabled, + }); + expect(body.image).toBeUndefined(); + expect(body.trash).toBeUndefined(); + }); + }); +}); diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index e7a01643abdc3..e840654152a68 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -6,6 +6,10 @@ import { AssetMediaController } from 'src/controllers/asset-media.controller'; import { AssetController } from 'src/controllers/asset.controller'; import { AuthAdminController } from 'src/controllers/auth-admin.controller'; import { AuthController } from 'src/controllers/auth.controller'; +import { ClusterGroupController } from 'src/controllers/cluster-group.controller'; +import { ConfigAdminController } from 'src/controllers/config-admin.controller'; +import { ConfigPublicController } from 'src/controllers/config-public.controller'; +import { ConfigUserController } from 'src/controllers/config-user.controller'; import { DatabaseBackupController } from 'src/controllers/database-backup.controller'; import { DownloadController } from 'src/controllers/download.controller'; import { DuplicateController } from 'src/controllers/duplicate.controller'; @@ -49,6 +53,10 @@ export const controllers = [ AssetMediaController, AuthController, AuthAdminController, + ClusterGroupController, + ConfigUserController, + ConfigAdminController, + ConfigPublicController, DatabaseBackupController, DownloadController, DuplicateController, diff --git a/server/src/controllers/notification-admin.controller.ts b/server/src/controllers/notification-admin.controller.ts index c322c5a2b609e..7a5585fad4ffc 100644 --- a/server/src/controllers/notification-admin.controller.ts +++ b/server/src/controllers/notification-admin.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/com import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfigSmtpDto } from 'src/dtos/config.dto'; import { NotificationCreateDto, NotificationDto, @@ -9,7 +10,6 @@ import { TemplateResponseDto, TestEmailResponseDto, } from 'src/dtos/notification.dto'; -import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { ApiTag } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { EmailTemplate } from 'src/repositories/email.repository'; diff --git a/server/src/controllers/server.controller.ts b/server/src/controllers/server.controller.ts index 6407155492ee4..ef9f408e8c3ea 100644 --- a/server/src/controllers/server.controller.ts +++ b/server/src/controllers/server.controller.ts @@ -101,7 +101,11 @@ export class ServerController { @Endpoint({ summary: 'Get features', description: 'Retrieve available features supported by this server.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'getPublicConfig' }), }) getServerFeatures(): Promise { return this.service.getFeatures(); @@ -112,7 +116,11 @@ export class ServerController { @Endpoint({ summary: 'Get config', description: 'Retrieve the current server configuration.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'getPublicConfig' }), }) getServerConfig(): Promise { return this.service.getSystemConfig(); diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts index 7d40f125836af..0ae8741c61e67 100644 --- a/server/src/controllers/system-config.controller.spec.ts +++ b/server/src/controllers/system-config.controller.spec.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; -import { defaults } from 'src/config'; import { SystemConfigController } from 'src/controllers/system-config.controller'; +import { defaults } from 'src/dtos/config.dto'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { SystemConfigService } from 'src/services/system-config.service'; import request from 'supertest'; diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index 6b79b38d98ba5..b7c5f0099669f 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -1,7 +1,7 @@ import { Body, Controller, Get, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Endpoint, HistoryBuilder } from 'src/decorators'; -import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; +import { AdminConfigDto, ConfigTemplateStorageOptionDto } from 'src/dtos/config.dto'; import { ApiTag, Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { StorageTemplateService } from 'src/services/storage-template.service'; @@ -20,10 +20,14 @@ export class SystemConfigController { @Endpoint({ summary: 'Get system configuration', description: 'Retrieve the current system configuration.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'getAdminConfig' }), }) - getConfig(): Promise { - return this.service.getSystemConfig(); + getConfig(): Promise { + return this.service.getAdminConfig(); } @Get('defaults') @@ -31,10 +35,14 @@ export class SystemConfigController { @Endpoint({ summary: 'Get system configuration defaults', description: 'Retrieve the default values for the system configuration.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'getAdminConfigDefaults' }), }) - getConfigDefaults(): SystemConfigDto { - return this.service.getDefaults(); + getConfigDefaults(): AdminConfigDto { + return this.service.getAdminConfigDefaults(); } @Put() @@ -42,10 +50,14 @@ export class SystemConfigController { @Endpoint({ summary: 'Update system configuration', description: 'Update the system configuration with a new system configuration.', - history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), + history: new HistoryBuilder() + .added('v1') + .beta('v1') + .stable('v2') + .deprecated('v3.2.0', { replacementId: 'updateAdminConfig' }), }) - updateConfig(@Body() dto: SystemConfigDto): Promise { - return this.service.updateSystemConfig(dto); + updateConfig(@Body() dto: AdminConfigDto): Promise { + return this.service.updateAdminConfig(dto); } @Get('storage-template-options') @@ -55,7 +67,7 @@ export class SystemConfigController { description: 'Retrieve exemplary storage template options.', history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), }) - getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { + getStorageTemplateOptions(): ConfigTemplateStorageOptionDto { return this.storageTemplateService.getStorageTemplateOptions(); } } diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 07b753f82080f..46124d96aa169 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -24,6 +24,8 @@ import { getConfig } from 'src/utils/config'; export interface MoveRequest { entityId: string; + /** a person is owned, so the owner is needed to save the new path */ + ownerId?: string; pathType: PathType; oldPath: string | null; newPath: string; @@ -35,6 +37,8 @@ export interface MoveRequest { export type ThumbnailPathEntity = { id: string; ownerId: string }; +export type PersonThumbnailPathEntity = { personGroupId: string; ownerId: string }; + export type HlsSessionFolder = { ownerId: string; sessionId: string }; export type HlsVariantFolder = { ownerId: string; sessionId: string; variantIndex: number }; @@ -113,8 +117,8 @@ export class StorageCore { return join(StorageCore.getMediaLocation(), folder); } - static getPersonThumbnailPath(person: ThumbnailPathEntity) { - return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`); + static getPersonThumbnailPath(person: PersonThumbnailPathEntity) { + return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.personGroupId}.jpeg`); } static getImagePath(asset: ThumbnailPathEntity, { fileType, format, isEdited }: ImagePathOptions) { @@ -176,12 +180,13 @@ export class StorageCore { }); } - async movePersonFile(person: { id: string; ownerId: string; thumbnailPath: string }, pathType: PersonPathType) { - const { id: entityId, thumbnailPath } = person; + async movePersonFile(person: PersonThumbnailPathEntity & { thumbnailPath: string }, pathType: PersonPathType) { + const { ownerId, personGroupId, thumbnailPath } = person; switch (pathType) { case PersonPathType.Face: { await this.moveFile({ - entityId, + entityId: personGroupId, + ownerId, pathType, oldPath: thumbnailPath, newPath: StorageCore.getPersonThumbnailPath(person), @@ -191,7 +196,7 @@ export class StorageCore { } async moveFile(request: MoveRequest) { - const { entityId, pathType, oldPath, newPath, assetInfo } = request; + const { entityId, ownerId, pathType, oldPath, newPath, assetInfo } = request; if (!oldPath || oldPath === newPath) { return; } @@ -264,7 +269,7 @@ export class StorageCore { } } - await this.savePath(pathType, entityId, newPath); + await this.savePath(pathType, entityId, newPath, ownerId); await this.moveRepository.delete(move.id); } @@ -317,7 +322,7 @@ export class StorageCore { return { dri, mali }; } - private savePath(pathType: PathType, id: string, newPath: string) { + private savePath(pathType: PathType, id: string, newPath: string, ownerId?: string) { switch (pathType) { case AssetPathType.Original: { return this.assetRepository.update({ id, originalPath: newPath }); @@ -333,7 +338,12 @@ export class StorageCore { } case PersonPathType.Face: { - return this.personRepository.update({ id, thumbnailPath: newPath }); + if (!ownerId) { + this.logger.warn('Unable to save person path without an owner'); + return; + } + + return this.personRepository.update({ ownerId, personGroupId: id, thumbnailPath: newPath }); } case UserPathType.Profile: { diff --git a/server/src/database.ts b/server/src/database.ts index 100ba451e768a..2d04b138258dd 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -133,6 +133,7 @@ export type User = { }; export type UserAdmin = User & { + clusterGroupId: string; storageLabel: string | null; shouldChangePassword: boolean; isAdmin: boolean; @@ -241,7 +242,7 @@ export type Exif = Omit, 'updatedAt' | 'updateId' | ' export type Person = { createdAt: Date; - id: string; + personGroupId: string; ownerId: string; updatedAt: Date; updateId: string; @@ -264,7 +265,7 @@ export type AssetFace = { boundingBoxY2: number; imageHeight: number; imageWidth: number; - personId: string | null; + personGroupId: string | null; sourceType: SourceType; person?: ShallowDehydrateObject | null; updatedAt: Date; @@ -376,6 +377,7 @@ export const columns = { userWithPrefix: userWithPrefixColumns, userAdmin: [ ...userColumns, + 'clusterGroupId', 'createdAt', 'updatedAt', 'deletedAt', diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index 470808e430cfe..8c933e06bbe6e 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,4 +1,5 @@ import { createZodDto } from 'nestjs-zod'; +import { HistoryBuilder } from 'src/decorators'; import { Permission } from 'src/enum'; import { isoDatetimeToDate } from 'src/validation'; import z from 'zod'; @@ -31,8 +32,9 @@ const ApiKeyResponseSchema = z const ApiKeyCreateResponseSchema = z .object({ + ...ApiKeyResponseSchema.shape, secret: z.string().describe('API key secret (only shown once)'), - apiKey: ApiKeyResponseSchema, + apiKey: ApiKeyResponseSchema.meta({ ...new HistoryBuilder().added('v1').deprecated('v3.2.0').getExtensions() }), }) .meta({ id: 'ApiKeyCreateResponseDto' }); diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index c54ce1cbab315..eaa05b40dc248 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -170,8 +170,8 @@ const peopleFromFaces = (faces?: MaybeDehydrated[]): PersonResponseDt const peopleMap: Map = new Map(); for (const face of faces) { - if (face.person && !peopleMap.has(face.person.id)) { - peopleMap.set(face.person.id, mapPerson(face.person)); + if (face.person && !peopleMap.has(face.person.personGroupId)) { + peopleMap.set(face.person.personGroupId, mapPerson(face.person)); } } diff --git a/server/src/dtos/cluster-group.dto.ts b/server/src/dtos/cluster-group.dto.ts new file mode 100644 index 0000000000000..d7de9d08ee5c2 --- /dev/null +++ b/server/src/dtos/cluster-group.dto.ts @@ -0,0 +1,32 @@ +import { Selectable } from 'kysely'; +import { createZodDto } from 'nestjs-zod'; +import { ClusterGroupRequestTable } from 'src/schema/tables/cluster-group-request.table'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; + +const ClusterGroupRequestCreateSchema = z + .object({ + userId: z.uuidv4().describe('User to invite into the cluster group'), + }) + .meta({ id: 'ClusterGroupRequestCreateDto' }); + +const ClusterGroupRequestResponseSchema = z + .object({ + id: z.uuidv4().describe('Request ID'), + clusterGroupId: z.uuidv4().describe('Cluster group the user is invited to join'), + userId: z.uuidv4().describe('User the request was created for'), + createdAt: isoDatetimeToDate.describe('Creation date'), + }) + .meta({ id: 'ClusterGroupRequestResponseDto' }); + +export class ClusterGroupRequestCreateDto extends createZodDto(ClusterGroupRequestCreateSchema) {} +export class ClusterGroupRequestResponseDto extends createZodDto(ClusterGroupRequestResponseSchema) {} + +export function mapClusterGroupRequest(request: Selectable): ClusterGroupRequestResponseDto { + return { + id: request.id, + clusterGroupId: request.clusterGroupId, + userId: request.userId, + createdAt: request.createdAt, + }; +} diff --git a/server/src/dtos/config.dto.spec.ts b/server/src/dtos/config.dto.spec.ts new file mode 100644 index 0000000000000..ce4f5a5d06827 --- /dev/null +++ b/server/src/dtos/config.dto.spec.ts @@ -0,0 +1,68 @@ +import { + AdminConfigDto, + defaults, + mapPublicConfig, + mapUserConfig, + PublicConfigDto, + UserConfigDto, +} from 'src/dtos/config.dto'; +import { getKeysDeep } from 'src/utils/misc'; +import z from 'zod'; + +const PUBLIC_PROPERTIES = [ + 'oauth.autoLaunch', + 'oauth.buttonText', + 'oauth.enabled', + 'passwordLogin.enabled', + 'server.loginPageMessage', + 'theme.customCss', +]; + +describe('config visibility', () => { + it('should expose every property to admins', () => { + const paths = getKeysDeep(defaults); + + expect(paths).toEqual(expect.arrayContaining(PUBLIC_PROPERTIES)); + expect(paths).toContain('oauth.clientSecret'); + expect(paths.length).toBeGreaterThan(100); + }); + + it('should expose the public properties to everyone', () => { + expect(getKeysDeep(mapPublicConfig(defaults)).sort()).toEqual(PUBLIC_PROPERTIES); + }); + + it('should expose everything public to logged in users as well', () => { + expect(getKeysDeep(mapUserConfig(defaults))).toEqual(expect.arrayContaining(PUBLIC_PROPERTIES)); + }); + + it('should accept the defaults with the admin schema', () => { + expect(AdminConfigDto.schema.safeParse(defaults)).toEqual(expect.objectContaining({ success: true })); + }); + + it('should map the defaults onto the user and public schemas', () => { + expect(UserConfigDto.schema.safeParse(mapUserConfig(defaults))).toEqual(expect.objectContaining({ success: true })); + expect(PublicConfigDto.schema.safeParse(mapPublicConfig(defaults))).toEqual( + expect.objectContaining({ success: true }), + ); + }); + + it('should not leak admin properties into the public config', () => { + const config = mapPublicConfig(defaults) as Record; + + expect(config.oauth).toEqual({ + autoLaunch: defaults.oauth.autoLaunch, + buttonText: defaults.oauth.buttonText, + enabled: defaults.oauth.enabled, + }); + expect(config.job).toBeUndefined(); + expect(config.image).toBeUndefined(); + expect(config.notifications).toBeUndefined(); + }); + + it('should keep the visibility metadata out of the schemas', () => { + for (const schema of [AdminConfigDto.schema, UserConfigDto.schema, PublicConfigDto.schema]) { + const json = JSON.stringify(z.toJSONSchema(schema, { io: 'input', unrepresentable: 'any' })); + expect(json).not.toContain('visibility'); + } + }); +}); diff --git a/server/src/dtos/config.dto.ts b/server/src/dtos/config.dto.ts new file mode 100644 index 0000000000000..a7dbaf0cc6480 --- /dev/null +++ b/server/src/dtos/config.dto.ts @@ -0,0 +1,773 @@ +import { CronExpression } from '@nestjs/schedule'; +import { validateCronExpression } from 'cron'; +import { createZodDto } from 'nestjs-zod'; +import { + AudioCodec, + AudioCodecSchema, + Colorspace, + ColorspaceSchema, + ConfigVisibility, + CQMode, + CQModeSchema, + HlsVideoResolution, + HlsVideoResolutionSchema, + ImageFormat, + ImageFormatSchema, + LogLevel, + LogLevelSchema, + OAuthTokenEndpointAuthMethod, + OAuthTokenEndpointAuthMethodSchema, + ReleaseChannel, + ReleaseChannelSchema, + ToneMapping, + ToneMappingSchema, + TranscodeHardwareAcceleration, + TranscodeHardwareAccelerationSchema, + TranscodePolicy, + TranscodePolicySchema, + VideoCodec, + VideoCodecSchema, + VideoContainer, + VideoContainerSchema, +} from 'src/enum'; +import { DeepPartial } from 'src/types'; +import z from 'zod'; + +const { Admin, User, Public } = ConfigVisibility; + +const configBool = z + .preprocess((val) => { + if (val === 'true') { + return true; + } + if (val === 'false') { + return false; + } + return val; + }, z.boolean()) + .meta({ type: 'boolean' }); + +const cronExpressionSchema = z + .string() + .superRefine((value, ctx) => { + const validated = validateCronExpression(value); + if (!validated.valid) { + ctx.addIssue({ + code: 'custom', + message: `Invalid cron expression. ${validated.error?.message ?? ''}`, + input: value, + }); + } + }) + .describe('Cron expression'); + +const emptyOrUrl = (error: string) => + z.string().refine((url) => url.length === 0 || z.url().safeParse(url).success, { error }); + +const AdminConfigIntegrityJobSchema = z + .object({ + enabled: z.boolean().describe('Enabled'), + cronExpression: cronExpressionSchema.describe('Cron expression for when the integrity check should run'), + }) + .describe('Integrity job config') + .meta({ id: 'AdminConfigIntegrityJobDto' }); + +const AdminConfigJobSettingsSchema = z + .object({ concurrency: z.int().min(1).describe('Concurrency') }) + .meta({ id: 'AdminConfigJobSettingsDto' }); + +const AdminConfigMachineLearningTaskSchema = z.object({ + enabled: z.boolean().describe('Whether the task is enabled').meta({ visibility: User }), +}); + +const AdminConfigMachineLearningModelSchema = AdminConfigMachineLearningTaskSchema.extend({ + modelName: z.string().describe('Name of the model to use'), +}); + +const AdminConfigGeneratedImageSchema = z + .object({ + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + size: z.int().min(1).describe('Size').meta({ visibility: User }), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'AdminConfigGeneratedImageDto' }); + +const AdminConfigFFmpegSchema = z + .object({ + crf: z.coerce.number().int().min(0).max(51).describe('CRF'), + threads: z.coerce.number().int().min(0).describe('Threads'), + preset: z.string().describe('Preset'), + targetVideoCodec: VideoCodecSchema, + acceptedVideoCodecs: z.array(VideoCodecSchema).describe('Accepted video codecs'), + targetAudioCodec: AudioCodecSchema, + acceptedAudioCodecs: z.array(AudioCodecSchema).describe('Accepted audio codecs'), + acceptedContainers: z.array(VideoContainerSchema).describe('Accepted containers'), + targetResolution: z.string().describe('Target resolution'), + maxBitrate: z.string().describe('Max bitrate'), + bframes: z.coerce.number().int().min(-1).max(16).describe('B-frames'), + refs: z.coerce.number().int().min(0).max(6).describe('References'), + gopSize: z.coerce.number().int().min(0).describe('GOP size'), + temporalAQ: configBool.describe('Temporal AQ'), + cqMode: CQModeSchema, + twoPass: configBool.describe('Two pass'), + preferredHwDevice: z.string().describe('Preferred hardware device'), + transcode: TranscodePolicySchema, + accel: TranscodeHardwareAccelerationSchema, + accelDecode: configBool.describe('Accelerated decode'), + tonemap: ToneMappingSchema, + realtime: z + .object({ + enabled: configBool.describe('Enable real-time HLS transcoding (alpha)').meta({ visibility: User }), + videoCodecs: z + .array(VideoCodecSchema) + .describe('Video codecs to use for real-time HLS transcoding') + .meta({ visibility: User }), + resolutions: z + .array(HlsVideoResolutionSchema) + .describe('Resolutions to use for real-time HLS transcoding') + .meta({ visibility: User }), + }) + .meta({ id: 'AdminConfigFFmpegRealtimeDto' }), + }) + .meta({ id: 'AdminConfigFFmpegDto' }); + +const AdminConfigSmtpSchema = z + .object({ + enabled: configBool.describe('Whether SMTP email notifications are enabled'), + from: z.string().describe('Email address to send from'), + replyTo: z.string().describe('Email address for replies'), + transport: z + .object({ + ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'), + host: z.string().describe('SMTP server hostname'), + port: z.int().min(0).max(65_535).describe('SMTP server port'), + secure: configBool.describe('Whether to use secure connection (TLS/SSL)'), + username: z.string().describe('SMTP username'), + password: z.string().describe('SMTP password'), + }) + .meta({ id: 'AdminConfigSmtpTransportDto' }), + }) + .meta({ id: 'AdminConfigSmtpDto' }); + +const AdminConfigSchemaWithVisibility = z + .object({ + backup: z + .object({ + database: z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + keepLastAmount: z.int().min(1).describe('Keep last amount'), + }) + .meta({ id: 'AdminConfigDatabaseBackupDto' }), + }) + .meta({ id: 'AdminConfigBackupsDto' }), + ffmpeg: AdminConfigFFmpegSchema, + integrityChecks: z + .object({ + missingFiles: AdminConfigIntegrityJobSchema, + untrackedFiles: AdminConfigIntegrityJobSchema, + checksumFiles: AdminConfigIntegrityJobSchema.extend({ + timeLimit: z.int().nonnegative().describe('How long the integrity checksum job may run for'), + percentageLimit: z + .float32() + .nonnegative() + .max(1) + .describe('Percentage limit of the integrity checksum job') + .meta({ format: 'double' }), + }) + .describe('Integrity checksum job config') + .meta({ id: 'AdminConfigIntegrityChecksumJobDto' }), + }) + .describe('Integrity checks config') + .meta({ id: 'AdminConfigIntegrityChecksDto' }), + job: z + .object({ + thumbnailGeneration: AdminConfigJobSettingsSchema, + metadataExtraction: AdminConfigJobSettingsSchema, + videoConversion: AdminConfigJobSettingsSchema, + faceDetection: AdminConfigJobSettingsSchema, + smartSearch: AdminConfigJobSettingsSchema, + backgroundTask: AdminConfigJobSettingsSchema, + migration: AdminConfigJobSettingsSchema, + search: AdminConfigJobSettingsSchema, + sidecar: AdminConfigJobSettingsSchema, + library: AdminConfigJobSettingsSchema, + notifications: AdminConfigJobSettingsSchema, + ocr: AdminConfigJobSettingsSchema, + workflow: AdminConfigJobSettingsSchema, + editor: AdminConfigJobSettingsSchema, + integrityCheck: AdminConfigJobSettingsSchema, + }) + .meta({ id: 'AdminConfigJobDto' }), + logging: z + .object({ + enabled: configBool.describe('Enabled'), + level: LogLevelSchema, + }) + .meta({ id: 'AdminConfigLoggingDto' }), + machineLearning: z + .object({ + enabled: configBool.describe('Enabled').meta({ visibility: User }), + urls: z.array(z.string()).min(1).describe('ML service URLs'), + availabilityChecks: z + .object({ + enabled: configBool.describe('Enabled'), + timeout: z.int(), + interval: z.int(), + }) + .meta({ id: 'AdminConfigMachineLearningAvailabilityChecksDto' }), + clip: AdminConfigMachineLearningModelSchema.meta({ id: 'AdminConfigClipDto' }), + duplicateDetection: AdminConfigMachineLearningTaskSchema.extend({ + maxDistance: z + .number() + .min(0.001) + .max(0.1) + .describe('Maximum distance threshold for duplicate detection') + .meta({ format: 'double' }), + }).meta({ id: 'AdminConfigDuplicateDetectionDto' }), + facialRecognition: AdminConfigMachineLearningModelSchema.extend({ + minScore: z + .number() + .min(0.1) + .max(1) + .describe('Minimum confidence score for face detection') + .meta({ format: 'double' }), + maxDistance: z + .number() + .min(0.1) + .max(2) + .describe('Maximum distance threshold for face recognition') + .meta({ format: 'double' }), + minFaces: z + .int() + .min(1) + .describe('Minimum number of faces required for recognition') + .meta({ visibility: User }), + }).meta({ id: 'AdminConfigFacialRecognitionDto' }), + ocr: AdminConfigMachineLearningModelSchema.extend({ + maxResolution: z.int().min(1).describe('Maximum resolution for OCR processing'), + minDetectionScore: z + .number() + .min(0.1) + .max(1) + .describe('Minimum confidence score for text detection') + .meta({ format: 'double' }), + minRecognitionScore: z + .number() + .min(0.1) + .max(1) + .describe('Minimum confidence score for text recognition') + .meta({ format: 'double' }), + }).meta({ id: 'AdminConfigOcrDto' }), + }) + .meta({ id: 'AdminConfigMachineLearningDto' }), + map: z + .object({ + enabled: configBool.describe('Enabled').meta({ visibility: User }), + lightStyle: z.url().describe('Light map style URL').meta({ visibility: User }), + darkStyle: z.url().describe('Dark map style URL').meta({ visibility: User }), + }) + .meta({ id: 'AdminConfigMapDto' }), + reverseGeocoding: z + .object({ enabled: configBool.describe('Enabled').meta({ visibility: User }) }) + .meta({ id: 'AdminConfigReverseGeocodingDto' }), + metadata: z + .object({ + faces: z.object({ import: configBool.describe('Import') }).meta({ id: 'AdminConfigFacesDto' }), + }) + .meta({ id: 'AdminConfigMetadataDto' }), + oauth: z + .object({ + autoLaunch: configBool.describe('Auto launch').meta({ visibility: Public }), + autoRegister: configBool.describe('Auto register'), + buttonText: z.string().describe('Button text').meta({ visibility: Public }), + clientId: z.string().describe('Client ID'), + clientSecret: z.string().describe('Client secret'), + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema, + timeout: z.int().min(1).describe('Timeout'), + allowInsecureRequests: configBool.describe('Allow insecure requests'), + defaultStorageQuota: z.int().min(0).nullable().describe('Default storage quota'), + enabled: configBool.describe('Enabled').meta({ visibility: Public }), + issuerUrl: emptyOrUrl('Issuer URL must be an empty string or a valid URL').describe('Issuer URL'), + scope: z.string().describe('Scope'), + prompt: z.string().describe('OAuth prompt parameter (e.g. select_account, login, consent)'), + endSessionEndpoint: emptyOrUrl('endSessionEndpoint must be an empty string or a valid URL').describe( + 'End session endpoint', + ), + signingAlgorithm: z.string().describe('Signing algorithm'), + profileSigningAlgorithm: z.string().describe('Profile signing algorithm'), + storageLabelClaim: z.string().describe('Storage label claim'), + storageQuotaClaim: z.string().describe('Storage quota claim'), + roleClaim: z.string().describe('Role claim'), + mobileOverrideEnabled: configBool.describe('Mobile override enabled'), + mobileRedirectUri: z.string().describe('Mobile redirect URI (set to empty string to disable)'), + }) + .transform((value, ctx) => { + if (!value.mobileOverrideEnabled || value.mobileRedirectUri === '') { + return value; + } + + if (!z.url().safeParse(value.mobileRedirectUri).success) { + ctx.issues.push({ + code: 'custom', + message: 'Mobile redirect URI must be an empty string or a valid URL', + input: value.mobileRedirectUri, + }); + return z.NEVER; + } + + return value; + }) + .meta({ id: 'AdminConfigOAuthDto' }), + passwordLogin: z + .object({ enabled: configBool.describe('Enabled').meta({ visibility: Public }) }) + .meta({ id: 'AdminConfigPasswordLoginDto' }), + storageTemplate: z + .object({ + enabled: configBool.describe('Enabled'), + hashVerificationEnabled: configBool.describe('Hash verification enabled'), + template: z.string().describe('Template'), + }) + .meta({ id: 'AdminConfigStorageTemplateDto' }), + image: z + .object({ + thumbnail: AdminConfigGeneratedImageSchema, + preview: AdminConfigGeneratedImageSchema, + fullsize: z + .object({ + enabled: configBool.describe('Enabled').meta({ visibility: User }), + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'AdminConfigGeneratedFullsizeImageDto' }), + colorspace: ColorspaceSchema, + extractEmbedded: configBool.describe('Extract embedded'), + }) + .meta({ id: 'AdminConfigImageDto' }), + newVersionCheck: z + .object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema }) + .meta({ id: 'AdminConfigNewVersionCheckDto' }), + nightlyTasks: z + .object({ + startTime: z.iso + .time({ + precision: -1, + error: (iss) => `Invalid input: expected string in HH:MM format, received ${typeof iss.input}`, + }) + .describe('Start time (HH:MM)'), + databaseCleanup: configBool.describe('Database cleanup'), + missingThumbnails: configBool.describe('Missing thumbnails'), + clusterNewFaces: configBool.describe('Cluster new faces'), + generateMemories: configBool.describe('Generate memories'), + syncQuotaUsage: configBool.describe('Sync quota usage'), + }) + .meta({ id: 'AdminConfigNightlyTasksDto' }), + trash: z + .object({ + enabled: configBool.describe('Enabled').meta({ visibility: User }), + days: z.int().min(0).describe('Days').meta({ visibility: User }), + }) + .meta({ id: 'AdminConfigTrashDto' }), + theme: z + .object({ customCss: z.string().describe('Custom CSS for theming').meta({ visibility: Public }) }) + .meta({ id: 'AdminConfigThemeDto' }), + library: z + .object({ + scan: z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + }) + .meta({ id: 'AdminConfigLibraryScanDto' }), + watch: z.object({ enabled: configBool.describe('Enabled') }).meta({ id: 'AdminConfigLibraryWatchDto' }), + }) + .meta({ id: 'AdminConfigLibraryDto' }), + notifications: z.object({ smtp: AdminConfigSmtpSchema }).meta({ id: 'AdminConfigNotificationsDto' }), + templates: z + .object({ + email: z + .object({ + welcomeTemplate: z.string().describe('Welcome template'), + albumInviteTemplate: z.string().describe('Album invite template'), + albumUpdateTemplate: z.string().describe('Album update template'), + }) + .meta({ id: 'AdminConfigTemplateEmailsDto' }), + }) + .meta({ id: 'AdminConfigTemplatesDto' }), + server: z + .object({ + externalDomain: emptyOrUrl('External domain must be an empty string or a valid URL') + .describe('External domain') + .meta({ visibility: User }), + loginPageMessage: z.string().describe('Login page message').meta({ visibility: Public }), + publicUsers: configBool.describe('Public users').meta({ visibility: User }), + }) + .meta({ id: 'AdminConfigServerDto' }), + user: z + .object({ deleteDelay: z.int().min(1).describe('Delete delay').meta({ visibility: User }) }) + .meta({ id: 'AdminConfigUserDto' }), + }) + .describe('Configuration properties that are visible to the admin') + .meta({ id: 'AdminConfigDto' }); + +export type SystemConfig = z.infer; +export type MachineLearningConfig = SystemConfig['machineLearning']; + +const visibilities = [Public, User, Admin]; + +const isVisible = (property: ConfigVisibility, visibility: ConfigVisibility) => + visibilities.indexOf(property) <= visibilities.indexOf(visibility); + +const getMeta = (schema: z.ZodType) => + (z.globalRegistry.get(schema) ?? {}) as { id?: string; description?: string; visibility?: ConfigVisibility }; + +const unwrap = (schema: z.ZodType) => (schema instanceof z.ZodPipe ? (schema.def.in as z.ZodType) : schema); + +const visibleSchemas = new Map>(); + +const applyVisibility = (visibility: ConfigVisibility): z.ZodType | undefined => { + const map: Record = { + [Admin]: 'Configuration properties that are visible to the admin', + [User]: 'Configuration properties that are visible to a logged user', + [Public]: 'Configuration properties that are visible to everyone', + }; + + return applyVisibilityRecursive(AdminConfigSchemaWithVisibility, visibility, map[visibility]); +}; + +const applyVisibilityRecursive = ( + schema: z.ZodType, + visibility: ConfigVisibility, + override?: string, +): z.ZodType | undefined => { + const object = unwrap(schema); + const { id, description, visibility: property } = getMeta(schema); + + if (!(object instanceof z.ZodObject)) { + return isVisible(property ?? Admin, visibility) ? schema : undefined; + } + + let cache = visibleSchemas.get(schema); + if (!cache) { + cache = new Map(); + visibleSchemas.set(schema, cache); + } + + if (cache.has(visibility)) { + return cache.get(visibility); + } + + const shape: Record = {}; + for (const [key, value] of Object.entries(object.shape as Record)) { + const visible = applyVisibilityRecursive(value, visibility); + if (visible) { + shape[key] = visible; + } + } + + let visible: z.ZodType | undefined; + if (Object.keys(shape).length > 0) { + visible = z.object(shape).meta({ + ...(id && { id: `${visibility}${id.slice(Admin.length)}` }), + ...((override ?? description) && { description: override ?? description }), + }); + } + + cache.set(visibility, visible); + + return visible; +}; + +const stripVisibilityMetadata = (schema: T): T => { + const object = unwrap(schema); + if (object instanceof z.ZodObject) { + for (const value of Object.values(object.shape as Record)) { + stripVisibilityMetadata(value); + } + + return schema; + } + + const { visibility, ...meta } = getMeta(schema); + if (visibility) { + z.globalRegistry.add(schema, meta); + } + + return schema; +}; + +const AdminConfigSchema = applyVisibility(Admin)! as z.ZodType; +const UserConfigSchema = applyVisibility(User)! as z.ZodType>; +const PublicConfigSchema = applyVisibility(Public)! as z.ZodType>; + +// prevent visibility metadata from leaking to openapi spec +// eslint-disable-next-line unicorn/no-top-level-side-effects +stripVisibilityMetadata(AdminConfigSchemaWithVisibility); + +const ConfigTemplateStorageOptionSchema = z + .object({ + yearOptions: z.array(z.string()).describe('Available year format options for storage template'), + monthOptions: z.array(z.string()).describe('Available month format options for storage template'), + weekOptions: z.array(z.string()).describe('Available week format options for storage template'), + dayOptions: z.array(z.string()).describe('Available day format options for storage template'), + hourOptions: z.array(z.string()).describe('Available hour format options for storage template'), + minuteOptions: z.array(z.string()).describe('Available minute format options for storage template'), + secondOptions: z.array(z.string()).describe('Available second format options for storage template'), + presetOptions: z.array(z.string()).describe('Available preset template options'), + }) + .meta({ id: 'SystemConfigTemplateStorageOptionDto' }); + +export class AdminConfigDto extends createZodDto(AdminConfigSchema) {} +export class UserConfigDto extends createZodDto(UserConfigSchema) {} +export class PublicConfigDto extends createZodDto(PublicConfigSchema) {} +export class ConfigFFmpegDto extends createZodDto(AdminConfigFFmpegSchema) {} +export class ConfigSmtpDto extends createZodDto(AdminConfigSmtpSchema) {} +export class ConfigTemplateStorageOptionDto extends createZodDto(ConfigTemplateStorageOptionSchema) {} + +/** @deprecated the `/system-config` endpoints these are named after are on their way out */ +export { AdminConfigDto as SystemConfigDto, ConfigSmtpDto as SystemConfigSmtpDto }; + +export function mapAdminConfig(config: SystemConfig): AdminConfigDto { + return config; +} + +export function mapUserConfig(config: SystemConfig): UserConfigDto { + return UserConfigSchema.parse(config); +} + +export function mapPublicConfig(config: SystemConfig): PublicConfigDto { + return PublicConfigSchema.parse(config); +} + +export const defaults = Object.freeze({ + backup: { + database: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_2AM, + keepLastAmount: 14, + }, + }, + ffmpeg: { + crf: 23, + threads: 0, + preset: 'ultrafast', + targetVideoCodec: VideoCodec.H264, + acceptedVideoCodecs: [VideoCodec.H264], + targetAudioCodec: AudioCodec.Aac, + acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.Opus], + acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm], + targetResolution: '720', + maxBitrate: '0', + bframes: -1, + refs: 0, + gopSize: 0, + temporalAQ: false, + cqMode: CQMode.Auto, + twoPass: false, + preferredHwDevice: 'auto', + transcode: TranscodePolicy.Required, + tonemap: ToneMapping.Hable, + accel: TranscodeHardwareAcceleration.Disabled, + accelDecode: true, + realtime: { + enabled: false, + videoCodecs: [VideoCodec.H264, VideoCodec.Hevc], + resolutions: [HlsVideoResolution.p480, HlsVideoResolution.p720, HlsVideoResolution.p1080], + }, + }, + integrityChecks: { + missingFiles: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_3AM, + }, + untrackedFiles: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_3AM, + }, + checksumFiles: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_3AM, + timeLimit: 60 * 60 * 1000, // 1 hour + percentageLimit: 1, // 100% of assets + }, + }, + job: { + thumbnailGeneration: { concurrency: 3 }, + metadataExtraction: { concurrency: 5 }, + videoConversion: { concurrency: 1 }, + faceDetection: { concurrency: 2 }, + smartSearch: { concurrency: 2 }, + backgroundTask: { concurrency: 5 }, + migration: { concurrency: 5 }, + search: { concurrency: 5 }, + sidecar: { concurrency: 5 }, + library: { concurrency: 5 }, + notifications: { concurrency: 5 }, + ocr: { concurrency: 1 }, + workflow: { concurrency: 5 }, + editor: { concurrency: 2 }, + integrityCheck: { concurrency: 1 }, + }, + logging: { + enabled: true, + level: LogLevel.Log, + }, + machineLearning: { + enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', + urls: [process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003'], + availabilityChecks: { + enabled: true, + timeout: 2000, + interval: 30_000, + }, + clip: { + enabled: true, + modelName: 'ViT-B-32__openai', + }, + duplicateDetection: { + enabled: true, + maxDistance: 0.01, + }, + facialRecognition: { + enabled: true, + modelName: 'buffalo_l', + minScore: 0.7, + maxDistance: 0.5, + minFaces: 3, + }, + ocr: { + enabled: true, + modelName: 'PP-OCRv5_mobile', + minDetectionScore: 0.5, + minRecognitionScore: 0.8, + maxResolution: 736, + }, + }, + map: { + enabled: true, + lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', + darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', + }, + reverseGeocoding: { + enabled: true, + }, + metadata: { + faces: { + import: false, + }, + }, + oauth: { + autoLaunch: false, + autoRegister: true, + buttonText: 'Login with OAuth', + clientId: '', + clientSecret: '', + defaultStorageQuota: null, + enabled: false, + issuerUrl: '', + endSessionEndpoint: '', + mobileOverrideEnabled: false, + mobileRedirectUri: '', + prompt: '', + scope: 'openid email profile', + signingAlgorithm: 'RS256', + profileSigningAlgorithm: 'none', + storageLabelClaim: 'preferred_username', + storageQuotaClaim: 'immich_quota', + roleClaim: 'immich_role', + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost, + timeout: 30_000, + allowInsecureRequests: false, + }, + passwordLogin: { + enabled: true, + }, + storageTemplate: { + enabled: false, + hashVerificationEnabled: true, + template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', + }, + image: { + thumbnail: { + format: ImageFormat.Webp, + size: 250, + quality: 80, + progressive: false, + }, + preview: { + format: ImageFormat.Jpeg, + size: 1440, + quality: 80, + progressive: false, + }, + colorspace: Colorspace.P3, + extractEmbedded: false, + fullsize: { + enabled: false, + format: ImageFormat.Jpeg, + quality: 80, + progressive: false, + }, + }, + newVersionCheck: { + enabled: true, + channel: ReleaseChannel.Stable, + }, + nightlyTasks: { + startTime: '00:00', + databaseCleanup: true, + generateMemories: true, + syncQuotaUsage: true, + missingThumbnails: true, + clusterNewFaces: true, + }, + trash: { + enabled: true, + days: 30, + }, + theme: { + customCss: '', + }, + library: { + scan: { + enabled: true, + cronExpression: CronExpression.EVERY_DAY_AT_MIDNIGHT, + }, + watch: { + enabled: false, + }, + }, + server: { + externalDomain: '', + loginPageMessage: '', + publicUsers: true, + }, + notifications: { + smtp: { + enabled: false, + from: '', + replyTo: '', + transport: { + ignoreCert: false, + host: '', + port: 587, + secure: false, + username: '', + password: '', + }, + }, + }, + templates: { + email: { + welcomeTemplate: '', + albumInviteTemplate: '', + albumUpdateTemplate: '', + }, + }, + user: { + deleteDelay: 7, + }, +}); diff --git a/server/src/dtos/model-config.dto.ts b/server/src/dtos/model-config.dto.ts deleted file mode 100644 index 2ba6f0c365abc..0000000000000 --- a/server/src/dtos/model-config.dto.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { createZodDto } from 'nestjs-zod'; -import z from 'zod'; - -const TaskConfigSchema = z - .object({ - enabled: z.boolean().describe('Whether the task is enabled'), - }) - .meta({ id: 'TaskConfig' }); - -const ModelConfigSchema = TaskConfigSchema.extend({ - modelName: z.string().describe('Name of the model to use'), -}); - -export const CLIPConfigSchema = ModelConfigSchema.meta({ id: 'CLIPConfig' }); - -export const DuplicateDetectionConfigSchema = TaskConfigSchema.extend({ - maxDistance: z - .number() - .meta({ format: 'double' }) - .min(0.001) - .max(0.1) - .describe('Maximum distance threshold for duplicate detection'), -}).meta({ id: 'DuplicateDetectionConfig' }); - -export const FacialRecognitionConfigSchema = ModelConfigSchema.extend({ - minScore: z - .number() - .meta({ format: 'double' }) - .min(0.1) - .max(1) - .describe('Minimum confidence score for face detection'), - maxDistance: z - .number() - .meta({ format: 'double' }) - .min(0.1) - .max(2) - .describe('Maximum distance threshold for face recognition'), - minFaces: z.int().min(1).describe('Minimum number of faces required for recognition'), -}).meta({ id: 'FacialRecognitionConfig' }); - -export const OcrConfigSchema = ModelConfigSchema.extend({ - maxResolution: z.int().min(1).describe('Maximum resolution for OCR processing'), - minDetectionScore: z - .number() - .meta({ format: 'double' }) - .min(0.1) - .max(1) - .describe('Minimum confidence score for text detection'), - minRecognitionScore: z - .number() - .meta({ format: 'double' }) - .min(0.1) - .max(1) - .describe('Minimum confidence score for text recognition'), -}).meta({ id: 'OcrConfig' }); - -export class CLIPConfig extends createZodDto(CLIPConfigSchema) {} diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index fa56ded1765de..0379b6f3b0f6d 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -173,7 +173,7 @@ export class PeopleResponseDto extends createZodDto(PeopleResponseSchema) {} export function mapPerson(person: MaybeDehydrated): PersonResponseDto { return { - id: person.id, + id: person.personGroupId, name: person.name, birthDate: asDateString(person.birthDate), thumbnailPath: person.thumbnailPath, @@ -215,6 +215,6 @@ export function mapFaces( ): AssetFaceResponseDto { return { ...mapFacesWithoutPerson(face, edits, assetDimensions), - person: face.person?.ownerId === auth.user.id ? mapPerson(face.person) : null, + person: face.person ? mapPerson(face.person) : null, }; } diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts deleted file mode 100644 index a50b7abe87b45..0000000000000 --- a/server/src/dtos/system-config.dto.ts +++ /dev/null @@ -1,444 +0,0 @@ -import { validateCronExpression } from 'cron'; -import { createZodDto } from 'nestjs-zod'; -import { SystemConfig } from 'src/config'; -import { - CLIPConfigSchema, - DuplicateDetectionConfigSchema, - FacialRecognitionConfigSchema, - OcrConfigSchema, -} from 'src/dtos/model-config.dto'; -import { - AudioCodecSchema, - ColorspaceSchema, - CQModeSchema, - HlsVideoResolutionSchema, - ImageFormatSchema, - LogLevelSchema, - OAuthTokenEndpointAuthMethodSchema, - ToneMappingSchema, - TranscodeHardwareAccelerationSchema, - TranscodePolicySchema, - VideoCodecSchema, - VideoContainerSchema, -} from 'src/enum'; -import z from 'zod'; - -/** Coerces 'true'/'false' strings to boolean, but also allows booleans. */ -const configBool = z - .preprocess((val) => { - if (val === 'true') { - return true; - } - if (val === 'false') { - return false; - } - return val; - }, z.boolean()) - .meta({ type: 'boolean' }); - -const JobSettingsSchema = z - .object({ - concurrency: z.int().min(1).describe('Concurrency'), - }) - .meta({ id: 'JobSettingsDto' }); - -const cronExpressionSchema = z - .string() - .superRefine((value, ctx) => { - const validated = validateCronExpression(value); - if (!validated.valid) { - ctx.addIssue({ - code: 'custom', - message: `Invalid cron expression. ${validated.error?.message ?? ''}`, - input: value, - }); - } - }) - .describe('Cron expression'); - -const DatabaseBackupSchema = z - .object({ - enabled: configBool.describe('Enabled'), - cronExpression: cronExpressionSchema, - keepLastAmount: z.int().min(1).describe('Keep last amount'), - }) - .meta({ id: 'DatabaseBackupConfig' }); - -const SystemConfigIntegrityJobSchema = z - .object({ - enabled: z.boolean().describe('Enabled'), - cronExpression: cronExpressionSchema.describe('Cron expression for when the integrity check should run'), - }) - .describe('Integrity job config') - .meta({ id: 'SystemConfigIntegrityJob' }); - -const SystemConfigIntegrityChecksumJobSchema = SystemConfigIntegrityJobSchema.extend({ - timeLimit: z.int().nonnegative().describe('How long the integrity checksum job may run for'), - percentageLimit: z - .float32() - .nonnegative() - .max(1) - .describe('Percentage limit of the integrity checksum job') - .meta({ format: 'double' }), -}) - .describe('Integrity checksum job config') - .meta({ id: 'SystemConfigIntegrityChecksumJob' }); - -const SystemConfigIntegrityChecksSchema = z - .object({ - missingFiles: SystemConfigIntegrityJobSchema, - untrackedFiles: SystemConfigIntegrityJobSchema, - checksumFiles: SystemConfigIntegrityChecksumJobSchema, - }) - .describe('Integrity checks config') - .meta({ id: 'SystemConfigIntegrityChecks' }); - -const SystemConfigBackupsSchema = z.object({ database: DatabaseBackupSchema }).meta({ id: 'SystemConfigBackupsDto' }); - -const SystemConfigFFmpegSchema = z - .object({ - crf: z.coerce.number().int().min(0).max(51).describe('CRF'), - threads: z.coerce.number().int().min(0).describe('Threads'), - preset: z.string().describe('Preset'), - targetVideoCodec: VideoCodecSchema, - acceptedVideoCodecs: z.array(VideoCodecSchema).describe('Accepted video codecs'), - targetAudioCodec: AudioCodecSchema, - acceptedAudioCodecs: z.array(AudioCodecSchema).describe('Accepted audio codecs'), - acceptedContainers: z.array(VideoContainerSchema).describe('Accepted containers'), - targetResolution: z.string().describe('Target resolution'), - maxBitrate: z.string().describe('Max bitrate'), - bframes: z.coerce.number().int().min(-1).max(16).describe('B-frames'), - refs: z.coerce.number().int().min(0).max(6).describe('References'), - gopSize: z.coerce.number().int().min(0).describe('GOP size'), - temporalAQ: configBool.describe('Temporal AQ'), - cqMode: CQModeSchema, - twoPass: configBool.describe('Two pass'), - preferredHwDevice: z.string().describe('Preferred hardware device'), - transcode: TranscodePolicySchema, - accel: TranscodeHardwareAccelerationSchema, - accelDecode: configBool.describe('Accelerated decode'), - tonemap: ToneMappingSchema, - realtime: z - .object({ - enabled: configBool.describe('Enable real-time HLS transcoding (alpha)'), - videoCodecs: z.array(VideoCodecSchema).describe('Video codecs to use for real-time HLS transcoding'), - resolutions: z.array(HlsVideoResolutionSchema).describe('Resolutions to use for real-time HLS transcoding'), - }) - .meta({ id: 'SystemConfigFFmpegRealtimeDto' }), - }) - .meta({ id: 'SystemConfigFFmpegDto' }); - -const SystemConfigJobSchema = z - .object({ - thumbnailGeneration: JobSettingsSchema, - metadataExtraction: JobSettingsSchema, - videoConversion: JobSettingsSchema, - faceDetection: JobSettingsSchema, - smartSearch: JobSettingsSchema, - backgroundTask: JobSettingsSchema, - migration: JobSettingsSchema, - search: JobSettingsSchema, - sidecar: JobSettingsSchema, - library: JobSettingsSchema, - notifications: JobSettingsSchema, - ocr: JobSettingsSchema, - workflow: JobSettingsSchema, - editor: JobSettingsSchema, - integrityCheck: JobSettingsSchema, - }) - .meta({ id: 'SystemConfigJobDto' }); - -const SystemConfigLibraryScanSchema = z - .object({ - enabled: configBool.describe('Enabled'), - cronExpression: cronExpressionSchema, - }) - .meta({ id: 'SystemConfigLibraryScanDto' }); - -const SystemConfigLibraryWatchSchema = z - .object({ enabled: configBool.describe('Enabled') }) - .meta({ id: 'SystemConfigLibraryWatchDto' }); - -const SystemConfigLibrarySchema = z - .object({ scan: SystemConfigLibraryScanSchema, watch: SystemConfigLibraryWatchSchema }) - .meta({ id: 'SystemConfigLibraryDto' }); - -const SystemConfigLoggingSchema = z - .object({ - enabled: configBool.describe('Enabled'), - level: LogLevelSchema, - }) - .meta({ id: 'SystemConfigLoggingDto' }); - -const MachineLearningAvailabilityChecksSchema = z - .object({ - enabled: configBool.describe('Enabled'), - timeout: z.int(), - interval: z.int(), - }) - .meta({ id: 'MachineLearningAvailabilityChecksDto' }); - -const SystemConfigMachineLearningSchema = z - .object({ - enabled: configBool.describe('Enabled'), - urls: z.array(z.string()).min(1).describe('ML service URLs'), - availabilityChecks: MachineLearningAvailabilityChecksSchema, - clip: CLIPConfigSchema, - duplicateDetection: DuplicateDetectionConfigSchema, - facialRecognition: FacialRecognitionConfigSchema, - ocr: OcrConfigSchema, - }) - .meta({ id: 'SystemConfigMachineLearningDto' }); - -const SystemConfigMapSchema = z - .object({ - enabled: configBool.describe('Enabled'), - lightStyle: z.url().describe('Light map style URL'), - darkStyle: z.url().describe('Dark map style URL'), - }) - .meta({ id: 'SystemConfigMapDto' }); - -export enum ReleaseChannel { - Stable = 'stable', - ReleaseCandidate = 'releaseCandidate', -} - -const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' }); - -const SystemConfigNewVersionCheckSchema = z - .object({ enabled: configBool.describe('Enabled'), channel: ReleaseChannelSchema }) - .meta({ id: 'SystemConfigNewVersionCheckDto' }); - -const SystemConfigNightlyTasksSchema = z - .object({ - startTime: z.iso - .time({ - precision: -1, - error: (iss) => `Invalid input: expected string in HH:MM format, received ${typeof iss.input}`, - }) - .describe('Start time (HH:MM)'), - databaseCleanup: configBool.describe('Database cleanup'), - missingThumbnails: configBool.describe('Missing thumbnails'), - clusterNewFaces: configBool.describe('Cluster new faces'), - generateMemories: configBool.describe('Generate memories'), - syncQuotaUsage: configBool.describe('Sync quota usage'), - }) - .meta({ id: 'SystemConfigNightlyTasksDto' }); - -const SystemConfigOAuthSchema = z - .object({ - autoLaunch: configBool.describe('Auto launch'), - autoRegister: configBool.describe('Auto register'), - buttonText: z.string().describe('Button text'), - clientId: z.string().describe('Client ID'), - clientSecret: z.string().describe('Client secret'), - tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema, - timeout: z.int().min(1).describe('Timeout'), - allowInsecureRequests: configBool.describe('Allow insecure requests'), - defaultStorageQuota: z.int().min(0).nullable().describe('Default storage quota'), - enabled: configBool.describe('Enabled'), - issuerUrl: z - .string() - .refine((url) => url.length === 0 || z.url().safeParse(url).success, { - error: 'Issuer URL must be an empty string or a valid URL', - }) - .describe('Issuer URL'), - scope: z.string().describe('Scope'), - prompt: z.string().describe('OAuth prompt parameter (e.g. select_account, login, consent)'), - endSessionEndpoint: z - .string() - .refine((url) => url.length === 0 || z.url().safeParse(url).success, { - error: 'endSessionEndpoint must be an empty string or a valid URL', - }) - .describe('End session endpoint'), - signingAlgorithm: z.string().describe('Signing algorithm'), - profileSigningAlgorithm: z.string().describe('Profile signing algorithm'), - storageLabelClaim: z.string().describe('Storage label claim'), - storageQuotaClaim: z.string().describe('Storage quota claim'), - roleClaim: z.string().describe('Role claim'), - mobileOverrideEnabled: configBool.describe('Mobile override enabled'), - mobileRedirectUri: z.string().describe('Mobile redirect URI (set to empty string to disable)'), - }) - .transform((value, ctx) => { - if (!value.mobileOverrideEnabled || value.mobileRedirectUri === '') { - return value; - } - - if (!z.url().safeParse(value.mobileRedirectUri).success) { - ctx.issues.push({ - code: 'custom', - message: 'Mobile redirect URI must be an empty string or a valid URL', - input: value.mobileRedirectUri, - }); - return z.NEVER; - } - - return value; - }) - .meta({ - id: 'SystemConfigOAuthDto', - }); - -const SystemConfigPasswordLoginSchema = z - .object({ enabled: configBool.describe('Enabled') }) - .meta({ id: 'SystemConfigPasswordLoginDto' }); - -const SystemConfigReverseGeocodingSchema = z - .object({ enabled: configBool.describe('Enabled') }) - .meta({ id: 'SystemConfigReverseGeocodingDto' }); - -const SystemConfigFacesSchema = z - .object({ import: configBool.describe('Import') }) - .meta({ id: 'SystemConfigFacesDto' }); -const SystemConfigMetadataSchema = z.object({ faces: SystemConfigFacesSchema }).meta({ id: 'SystemConfigMetadataDto' }); - -const SystemConfigServerSchema = z - .object({ - externalDomain: z - .string() - .refine((url) => url.length === 0 || z.url().safeParse(url).success, { - error: 'External domain must be an empty string or a valid URL', - }) - .describe('External domain'), - loginPageMessage: z.string().describe('Login page message'), - publicUsers: configBool.describe('Public users'), - }) - .meta({ id: 'SystemConfigServerDto' }); - -const SystemConfigSmtpTransportSchema = z - .object({ - ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'), - host: z.string().describe('SMTP server hostname'), - port: z.int().min(0).max(65_535).describe('SMTP server port'), - secure: configBool.describe('Whether to use secure connection (TLS/SSL)'), - username: z.string().describe('SMTP username'), - password: z.string().describe('SMTP password'), - }) - .meta({ id: 'SystemConfigSmtpTransportDto' }); - -const SystemConfigSmtpSchema = z - .object({ - enabled: configBool.describe('Whether SMTP email notifications are enabled'), - from: z.string().describe('Email address to send from'), - replyTo: z.string().describe('Email address for replies'), - transport: SystemConfigSmtpTransportSchema, - }) - .meta({ id: 'SystemConfigSmtpDto' }); - -const SystemConfigNotificationsSchema = z - .object({ smtp: SystemConfigSmtpSchema }) - .meta({ id: 'SystemConfigNotificationsDto' }); - -const SystemConfigTemplateEmailsSchema = z - .object({ - albumInviteTemplate: z.string().describe('Album invite template'), - welcomeTemplate: z.string().describe('Welcome template'), - albumUpdateTemplate: z.string().describe('Album update template'), - }) - .meta({ id: 'SystemConfigTemplateEmailsDto' }); -const SystemConfigTemplatesSchema = z - .object({ email: SystemConfigTemplateEmailsSchema }) - .meta({ id: 'SystemConfigTemplatesDto' }); - -const SystemConfigStorageTemplateSchema = z - .object({ - enabled: configBool.describe('Enabled'), - hashVerificationEnabled: configBool.describe('Hash verification enabled'), - template: z.string().describe('Template'), - }) - .meta({ id: 'SystemConfigStorageTemplateDto' }); - -const SystemConfigTemplateStorageOptionSchema = z - .object({ - yearOptions: z.array(z.string()).describe('Available year format options for storage template'), - monthOptions: z.array(z.string()).describe('Available month format options for storage template'), - weekOptions: z.array(z.string()).describe('Available week format options for storage template'), - dayOptions: z.array(z.string()).describe('Available day format options for storage template'), - hourOptions: z.array(z.string()).describe('Available hour format options for storage template'), - minuteOptions: z.array(z.string()).describe('Available minute format options for storage template'), - secondOptions: z.array(z.string()).describe('Available second format options for storage template'), - presetOptions: z.array(z.string()).describe('Available preset template options'), - }) - .meta({ id: 'SystemConfigTemplateStorageOptionDto' }); - -const SystemConfigThemeSchema = z - .object({ customCss: z.string().describe('Custom CSS for theming') }) - .meta({ id: 'SystemConfigThemeDto' }); - -const SystemConfigGeneratedImageSchema = z - .object({ - format: ImageFormatSchema, - quality: z.int().min(1).max(100).describe('Quality'), - size: z.int().min(1).describe('Size'), - progressive: configBool.default(false).optional().describe('Progressive'), - }) - .meta({ id: 'SystemConfigGeneratedImageDto' }); - -const SystemConfigGeneratedFullsizeImageSchema = z - .object({ - enabled: configBool.describe('Enabled'), - format: ImageFormatSchema, - quality: z.int().min(1).max(100).describe('Quality'), - progressive: configBool.default(false).optional().describe('Progressive'), - }) - .meta({ id: 'SystemConfigGeneratedFullsizeImageDto' }); - -const SystemConfigImageSchema = z - .object({ - thumbnail: SystemConfigGeneratedImageSchema, - preview: SystemConfigGeneratedImageSchema, - fullsize: SystemConfigGeneratedFullsizeImageSchema, - colorspace: ColorspaceSchema, - extractEmbedded: configBool.describe('Extract embedded'), - }) - .meta({ id: 'SystemConfigImageDto' }); - -const SystemConfigTrashSchema = z - .object({ - enabled: configBool.describe('Enabled'), - days: z.int().min(0).describe('Days'), - }) - .meta({ id: 'SystemConfigTrashDto' }); - -const SystemConfigUserSchema = z - .object({ - deleteDelay: z.int().min(1).describe('Delete delay'), - }) - .meta({ id: 'SystemConfigUserDto' }); - -export const SystemConfigSchema = z - .object({ - backup: SystemConfigBackupsSchema, - ffmpeg: SystemConfigFFmpegSchema, - logging: SystemConfigLoggingSchema, - machineLearning: SystemConfigMachineLearningSchema, - map: SystemConfigMapSchema, - newVersionCheck: SystemConfigNewVersionCheckSchema, - nightlyTasks: SystemConfigNightlyTasksSchema, - oauth: SystemConfigOAuthSchema, - passwordLogin: SystemConfigPasswordLoginSchema, - reverseGeocoding: SystemConfigReverseGeocodingSchema, - metadata: SystemConfigMetadataSchema, - storageTemplate: SystemConfigStorageTemplateSchema, - job: SystemConfigJobSchema, - image: SystemConfigImageSchema, - trash: SystemConfigTrashSchema, - theme: SystemConfigThemeSchema, - library: SystemConfigLibrarySchema, - notifications: SystemConfigNotificationsSchema, - templates: SystemConfigTemplatesSchema, - server: SystemConfigServerSchema, - user: SystemConfigUserSchema, - integrityChecks: SystemConfigIntegrityChecksSchema, - }) - .describe('System configuration') - .meta({ id: 'SystemConfigDto' }); - -export class SystemConfigFFmpegDto extends createZodDto(SystemConfigFFmpegSchema) {} -export class SystemConfigSmtpDto extends createZodDto(SystemConfigSmtpSchema) {} -export class SystemConfigTemplateStorageOptionDto extends createZodDto(SystemConfigTemplateStorageOptionSchema) {} -export class SystemConfigDto extends createZodDto(SystemConfigSchema) {} - -export function mapConfig(config: SystemConfig): SystemConfigDto { - return config; -} diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 528163e57ca78..2a15e43976ad7 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,5 +1,6 @@ import { createZodDto } from 'nestjs-zod'; import { User, UserAdmin } from 'src/database'; +import { HistoryBuilder } from 'src/decorators'; import { pinCodeRegex } from 'src/dtos/auth.dto'; import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum'; import { MaybeDehydrated, UserMetadataItem } from 'src/types'; @@ -116,6 +117,10 @@ const UserAdminDeleteSchema = z export class UserAdminDeleteDto extends createZodDto(UserAdminDeleteSchema) {} const UserAdminResponseSchema = UserResponseSchema.extend({ + clusterGroupId: z + .uuidv4() + .describe('Cluster group the user is a member of') + .meta(new HistoryBuilder().added('v3.2.0').getExtensions()), storageLabel: z.string().nullable().describe('Storage label'), shouldChangePassword: z.boolean().describe('Require password change on next login'), isAdmin: z.boolean().describe('Is admin user'), @@ -139,6 +144,7 @@ export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { return { ...mapUser(entity), + clusterGroupId: entity.clusterGroupId, storageLabel: entity.storageLabel, shouldChangePassword: entity.shouldChangePassword, isAdmin: entity.isAdmin, diff --git a/server/src/enum.ts b/server/src/enum.ts index b6e2f6e4686ce..2b61999a4ffd3 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -161,6 +161,17 @@ export enum Permission { BackupUpload = 'backup.upload', BackupDelete = 'backup.delete', + ClusterGroupRead = 'clusterGroup.read', + ClusterGroupLeave = 'clusterGroup.leave', + ClusterGroupRequestCreate = 'clusterGroupRequest.create', + ClusterGroupRequestRead = 'clusterGroupRequest.read', + ClusterGroupRequestDelete = 'clusterGroupRequest.delete', + + AdminConfigRead = 'adminConfig.read', + AdminConfigUpdate = 'adminConfig.update', + + UserConfigRead = 'userConfig.read', + DuplicateRead = 'duplicate.read', DuplicateDelete = 'duplicate.delete', @@ -1131,6 +1142,7 @@ export enum NotificationType { SystemMessage = 'SystemMessage', AlbumInvite = 'AlbumInvite', AlbumUpdate = 'AlbumUpdate', + ClusterGroupRequest = 'ClusterGroupRequest', Custom = 'Custom', } @@ -1165,12 +1177,25 @@ export const AssetVisibilitySchema = z .describe('Asset visibility') .meta({ id: 'AssetVisibility' }); +export enum ReleaseChannel { + Stable = 'stable', + ReleaseCandidate = 'releaseCandidate', +} + +export const ReleaseChannelSchema = z.enum(ReleaseChannel).describe('Release channel').meta({ id: 'ReleaseChannel' }); + export enum CronJob { LibraryScan = 'LibraryScan', NightlyJobs = 'NightlyJobs', VersionCheck = 'VersionCheck', } +export enum ConfigVisibility { + Public = 'Public', + User = 'User', + Admin = 'Admin', +} + export enum ApiTag { Activities = 'Activities', Albums = 'Albums', @@ -1178,6 +1203,9 @@ export enum ApiTag { Authentication = 'Authentication', AuthenticationAdmin = 'Authentication (admin)', Assets = 'Assets', + ConfigUser = 'Config (user)', + ConfigAdmin = 'Config (admin)', + ConfigPublic = 'Config (public)', DatabaseBackups = 'Database Backups (admin)', Deprecated = 'Deprecated', Download = 'Download', @@ -1191,6 +1219,7 @@ export enum ApiTag { Memories = 'Memories', Notifications = 'Notifications', NotificationsAdmin = 'Notifications (admin)', + ClusterGroups = 'Cluster groups', Partners = 'Partners', People = 'People', Plugins = 'Plugins', diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index 94d3b4d003cc1..270bc915e4cdc 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -187,13 +187,56 @@ where "notification"."id" in ($1) and "notification"."userId" = $2 +-- AccessRepository.clusterGroup.checkInviteAccess +select + "cluster_group_request"."clusterGroupId" +from + "cluster_group_request" +where + "cluster_group_request"."clusterGroupId" in ($1) + and "cluster_group_request"."userId" = $2 + +-- AccessRepository.clusterGroup.checkOwnerAccess +select + "user"."clusterGroupId" +from + "user" +where + "user"."clusterGroupId" in ($1) + and "user"."id" = $2 + +-- AccessRepository.clusterGroupRequest.checkOwnerAccess +select + "cluster_group_request"."id" +from + "cluster_group_request" +where + "cluster_group_request"."id" in ($1) + and "cluster_group_request"."userId" = $2 + +-- AccessRepository.clusterGroupRequest.checkGroupAccess +select + "cluster_group_request"."id" +from + "cluster_group_request" +where + "cluster_group_request"."id" in ($1) + and "cluster_group_request"."clusterGroupId" = ( + select + "user"."clusterGroupId" + from + "user" + where + "user"."id" = $2 + ) + -- AccessRepository.person.checkOwnerAccess select - "person"."id" + "person"."personGroupId" from "person" where - "person"."id" in ($1) + "person"."personGroupId" in ($1) and "person"."ownerId" = $2 -- AccessRepository.person.checkFaceOwnerAccess diff --git a/server/src/queries/asset.job.repository.sql b/server/src/queries/asset.job.repository.sql index aa046039130a1..96af89edb56a4 100644 --- a/server/src/queries/asset.job.repository.sql +++ b/server/src/queries/asset.job.repository.sql @@ -354,9 +354,11 @@ select "asset_file"."assetId" = "asset"."id" and "asset_file"."type" = $2 ) as agg - ) as "files" + ) as "files", + "user"."clusterGroupId" from "asset" + inner join "user" on "user"."id" = "asset"."ownerId" where "asset"."id" = $3 diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index cc694dd63a91e..b790705cc8798 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -192,7 +192,8 @@ select from "person" where - "asset_face"."personId" = "person"."id" + "person"."personGroupId" = "asset_face"."personGroupId" + and "person"."ownerId" = "asset"."ownerId" ) as "person" on true where "asset_face"."assetId" = "asset"."id" diff --git a/server/src/queries/cluster.group.repository.sql b/server/src/queries/cluster.group.repository.sql new file mode 100644 index 0000000000000..f81e87a6a5284 --- /dev/null +++ b/server/src/queries/cluster.group.repository.sql @@ -0,0 +1,71 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- ClusterGroupRepository.create +insert into + "cluster_group" +default values +returning + * + +-- ClusterGroupRepository.hasOtherMembers +select + "user"."id" +from + "user" +where + "user"."clusterGroupId" = $1 + and "user"."id" != $2 + and "user"."deletedAt" is null + +-- ClusterGroupRepository.createRequest +insert into + "cluster_group_request" ("clusterGroupId", "userId") +values + ($1, $2) +on conflict ("clusterGroupId", "userId") do update +set + "clusterGroupId" = $3 +returning + *, + (xmax = 0) as "isInserted" + +-- ClusterGroupRepository.getRequest +select + "cluster_group_request".* +from + "cluster_group_request" +where + "cluster_group_request"."id" = $1 + +-- ClusterGroupRepository.searchRequests +select + "cluster_group_request".* +from + "cluster_group_request" +where + "cluster_group_request"."userId" = $1 + and "cluster_group_request"."clusterGroupId" = $2 +order by + "cluster_group_request"."createdAt" asc + +-- ClusterGroupRepository.getUsers +select + "id", + "name", + "email", + "avatarColor", + "profileImagePath", + "profileChangedAt" +from + "user" +where + "user"."clusterGroupId" = $1 + and "user"."deletedAt" is null +order by + "user"."id" = $2 desc, + "user"."name" asc + +-- ClusterGroupRepository.deleteRequest +delete from "cluster_group_request" +where + "cluster_group_request"."id" = $1 diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql index 44339cbcd9851..7730813a5fb6e 100644 --- a/server/src/queries/memory.repository.sql +++ b/server/src/queries/memory.repository.sql @@ -47,7 +47,8 @@ select $1 as "one" from "asset_face" - inner join "person" on "person"."id" = "asset_face"."personId" + inner join "person" on "person"."personGroupId" = "asset_face"."personGroupId" + and "person"."ownerId" = "asset"."ownerId" where "asset_face"."assetId" = "asset"."id" and "person"."isHidden" = $2 @@ -86,7 +87,8 @@ select $1 as "one" from "asset_face" - inner join "person" on "person"."id" = "asset_face"."personId" + inner join "person" on "person"."personGroupId" = "asset_face"."personGroupId" + and "person"."ownerId" = "asset"."ownerId" where "asset_face"."assetId" = "asset"."id" and "person"."isHidden" = $2 diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index a2f3f6444290d..aa17fd05fa252 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -3,18 +3,53 @@ -- PersonRepository.reassignFaces update "asset_face" set - "personId" = $1 + "personGroupId" = $1 where - "asset_face"."personId" = $2 + "asset_face"."personGroupId" = $2 -- PersonRepository.delete delete from "person" where - "person"."id" in ($1) + "ownerId" = $1 + and "person"."personGroupId" in ($2) +returning + "personGroupId", + "ownerId", + "thumbnailPath" + +-- PersonRepository.deleteGroups +delete from "person_group" +where + "person_group"."id" in ($1) + +-- PersonRepository.deleteEmptyGroups +delete from "person_group" +where + not exists ( + select + "person"."personGroupId" + from + "person" + where + "person"."personGroupId" = "person_group"."id" + ) + +-- PersonRepository.deleteOrphanedClusterGroups +delete from "cluster_group" +where + not exists ( + select + "user"."id" + from + "user" + where + "user"."clusterGroupId" = "cluster_group"."id" + ) -- PersonRepository.getFileSamples select - "id", + "ownerId", + "personGroupId", "thumbnailPath" from "person" @@ -28,8 +63,9 @@ select "person".* from "person" - inner join "asset_face" on "asset_face"."personId" = "person"."id" + inner join "asset_face" on "asset_face"."personGroupId" = "person"."personGroupId" inner join "asset" on "asset_face"."assetId" = "asset"."id" + and "asset"."ownerId" = "person"."ownerId" and "asset"."visibility" = 'timeline' and "asset"."deletedAt" is null where @@ -38,7 +74,8 @@ where and "asset_face"."isVisible" is true and "person"."isHidden" = $2 group by - "person"."id" + "person"."ownerId", + "person"."personGroupId" having ( "person"."name" != $3 @@ -72,12 +109,13 @@ select "person".* from "person" - left join "asset_face" on "asset_face"."personId" = "person"."id" + left join "asset_face" on "asset_face"."personGroupId" = "person"."personGroupId" where "asset_face"."deletedAt" is null and "asset_face"."isVisible" is true group by - "person"."id" + "person"."ownerId", + "person"."personGroupId" having count("asset_face"."assetId") = $1 @@ -94,15 +132,16 @@ select from "person" where - "person"."id" = "asset_face"."personId" + "person"."personGroupId" = "asset_face"."personGroupId" + and "person"."ownerId" = $1 ) as obj ) as "person" from "asset_face" where - "asset_face"."assetId" = $1 + "asset_face"."assetId" = $2 and "asset_face"."deletedAt" is null - and "asset_face"."isVisible" = $2 + and "asset_face"."isVisible" = $3 order by "asset_face"."boundingBoxX1" asc @@ -119,19 +158,20 @@ select from "person" where - "person"."id" = "asset_face"."personId" + "person"."personGroupId" = "asset_face"."personGroupId" + and "person"."ownerId" = $1 ) as obj ) as "person" from "asset_face" where - "asset_face"."id" = $1 + "asset_face"."id" = $2 and "asset_face"."deletedAt" is null -- PersonRepository.getFaceForFacialRecognitionJob select "asset_face"."id", - "asset_face"."personId", + "asset_face"."personGroupId", "asset_face"."sourceType", ( select @@ -141,9 +181,11 @@ select select "asset"."ownerId", "asset"."visibility", - "asset"."fileCreatedAt" + "asset"."fileCreatedAt", + "user"."clusterGroupId" from "asset" + inner join "user" on "user"."id" = "asset"."ownerId" where "asset"."id" = "asset_face"."assetId" ) as obj @@ -195,16 +237,26 @@ from inner join "asset" on "asset_face"."assetId" = "asset"."id" left join "asset_exif" on "asset_exif"."assetId" = "asset"."id" where - "person"."id" = $1 + "person"."ownerId" = $1 + and "person"."personGroupId" = $2 and "asset_face"."deletedAt" is null -- PersonRepository.reassignFace update "asset_face" set - "personId" = $1 + "personGroupId" = $1 where "asset_face"."id" = $2 +-- PersonRepository.getByGroupId +select + "person".* +from + "person" +where + "person"."personGroupId" = $1 + and "person"."ownerId" = $2 + -- PersonRepository.getByName with "similarity_threshold" as ( @@ -226,7 +278,7 @@ limit -- PersonRepository.getDistinctNames select distinct - on (lower("person"."name")) "person"."id", + on (lower("person"."name")) "person"."personGroupId", "person"."name" from "person" @@ -244,10 +296,25 @@ from left join "asset" on "asset"."id" = "asset_face"."assetId" and "asset"."visibility" = 'timeline' and "asset"."deletedAt" is null + and ( + "asset"."ownerId" = $1::uuid + or exists ( + select + 1 as "exists" + from + "album_asset" + inner join "album" on "album"."id" = "album_asset"."albumId" + and "album"."deletedAt" is null + inner join "album_user" on "album_user"."albumId" = "album"."id" + and "album_user"."userId" = $2::uuid + where + "album_asset"."assetId" = "asset"."id" + ) + ) where "asset_face"."deletedAt" is null and "asset_face"."isVisible" is true - and "asset_face"."personId" = $1 + and "asset_face"."personGroupId" = $3 -- PersonRepository.getNumberOfPeople select @@ -267,7 +334,7 @@ where from "asset_face" where - "asset_face"."personId" = "person"."id" + "asset_face"."personGroupId" = "person"."personGroupId" and "asset_face"."deletedAt" is null and "asset_face"."isVisible" = $2 and exists ( @@ -282,6 +349,164 @@ where ) and "person"."ownerId" = $3 +-- PersonRepository.createGroup +insert into + "person_group" ("clusterGroupId") +select + "user"."clusterGroupId" +from + "user" +where + "user"."id" = $1 +returning + * + +-- PersonRepository.reassignCluster +begin +update "person_group" +set + "clusterGroupId" = $1 +where + "person_group"."id" in ( + select + "person"."personGroupId" + from + "person" + where + "person"."ownerId" = $2 + ) + and not exists ( + select + "person"."personGroupId" + from + "person" + where + "person"."personGroupId" = "person_group"."id" + and "person"."ownerId" != $3 + ) +with + "shared" as ( + select distinct + "person"."personGroupId" as "oldId" + from + "person" + where + "person"."ownerId" = $1 + and exists ( + select + "other"."personGroupId" + from + "person" as "other" + where + "other"."personGroupId" = "person"."personGroupId" + and "other"."ownerId" != $2 + ) + ), + "mapping" as materialized ( + select + "shared"."oldId", + uuid_generate_v4 () as "newId" + from + "shared" + ), + "created" as ( + insert into + "person_group" ("id", "clusterGroupId") + select + "mapping"."newId", + $3 as "clusterGroupId" + from + "mapping" + ) +select + "mapping"."oldId", + "mapping"."newId" +from + "mapping" +commit + +-- PersonRepository.createGroups +insert into + "person_group" ( + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31", + "32", + "33", + "34", + "35" + ) +values + ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18, + $19, + $20, + $21, + $22, + $23, + $24, + $25, + $26, + $27, + $28, + $29, + $30, + $31, + $32, + $33, + $34, + $35, + $36 + ) +returning + * + -- PersonRepository.refreshFaces with "added_embeddings" as ( @@ -310,14 +535,15 @@ select from "person" where - "person"."id" = "asset_face"."personId" + "person"."personGroupId" = "asset_face"."personGroupId" + and "person"."ownerId" = $1 ) as obj ) as "person" from "asset_face" where - "asset_face"."assetId" in ($1) - and "asset_face"."personId" in ($2) + "asset_face"."assetId" in ($2) + and "asset_face"."personGroupId" in ($3) and "asset_face"."deletedAt" is null -- PersonRepository.getRandomFace @@ -326,7 +552,7 @@ select from "asset_face" where - "asset_face"."personId" = $1 + "asset_face"."personGroupId" = $1 and "asset_face"."deletedAt" is null and "asset_face"."isVisible" is true @@ -348,15 +574,6 @@ set where "asset_face"."id" = $2 --- PersonRepository.getForPeopleDelete -select - "id", - "thumbnailPath" -from - "person" -where - "id" in ($1) - -- PersonRepository.getForFeatureFaceUpdate select "asset_face"."id" @@ -366,4 +583,14 @@ from and "asset"."isOffline" = $1 where "asset_face"."assetId" = $2 - and "asset_face"."personId" = $3 + and "asset_face"."personGroupId" = $3 + +-- PersonRepository.getForMergePerson +select + "person".* +from + "person" +where + "person"."personGroupId" in ($1) +order by + "person"."ownerId" diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index efd7236bb5cba..abd10e6221bc2 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -218,15 +218,21 @@ with "cte" as ( select "asset_face"."id", - "asset_face"."personId", + "asset_face"."personGroupId", face_search.embedding <=> $1 as "distance" from "asset_face" inner join "asset" on "asset"."id" = "asset_face"."assetId" inner join "face_search" on "face_search"."faceId" = "asset_face"."id" - left join "person" on "person"."id" = "asset_face"."personId" where - "asset"."ownerId" = any ($2::uuid[]) + "asset"."ownerId" in ( + select + "user"."id" + from + "user" + where + "user"."clusterGroupId" = $2 + ) and "asset"."deletedAt" is null order by "distance" @@ -239,7 +245,7 @@ from "cte" where "cte"."distance" <= $4 -commit +rollback -- SearchRepository.searchPlaces select @@ -849,11 +855,11 @@ where "asset_face"."assetId" = "asset"."id" and "asset_face"."deletedAt" is null and "asset_face"."isVisible" = $2 - and "asset_face"."personId" = any ($3::uuid[]) + and "asset_face"."personGroupId" = any ($3::uuid[]) group by "asset_face"."assetId" having - count(distinct "asset_face"."personId") = $4 + count(distinct "asset_face"."personGroupId") = $4 ) order by "asset"."fileCreatedAt" desc, @@ -1374,7 +1380,7 @@ where "asset_face"."assetId" = "asset"."id" and "asset_face"."deletedAt" is null and "asset_face"."isVisible" = $3 - and "asset_face"."personId" = any ($4::uuid[]) + and "asset_face"."personGroupId" = any ($4::uuid[]) ) ) order by diff --git a/server/src/queries/sync.repository.sql b/server/src/queries/sync.repository.sql index 69f7981bf3b31..15c5a73f2ca3c 100644 --- a/server/src/queries/sync.repository.sql +++ b/server/src/queries/sync.repository.sql @@ -536,7 +536,7 @@ order by select "asset_face"."id", "assetId", - "personId", + "personGroupId" as "personId", "imageWidth", "imageHeight", "boundingBoxX1", @@ -1029,7 +1029,7 @@ order by -- SyncRepository.person.getDeletes select "id", - "personId" + "personGroupId" as "personId" from "person_audit" as "person_audit" where @@ -1041,7 +1041,7 @@ order by -- SyncRepository.person.getUpserts select - "id", + "personGroupId" as "id", "createdAt", "updatedAt", "ownerId", diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index c5a4f139a7041..de5cfea138637 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -8,6 +8,7 @@ select "avatarColor", "profileImagePath", "profileChangedAt", + "clusterGroupId", "createdAt", "updatedAt", "deletedAt", @@ -47,6 +48,7 @@ select "avatarColor", "profileImagePath", "profileChangedAt", + "clusterGroupId", "createdAt", "updatedAt", "deletedAt", @@ -126,6 +128,7 @@ select "avatarColor", "profileImagePath", "profileChangedAt", + "clusterGroupId", "createdAt", "updatedAt", "deletedAt", @@ -165,6 +168,7 @@ select "avatarColor", "profileImagePath", "profileChangedAt", + "clusterGroupId", "createdAt", "updatedAt", "deletedAt", @@ -190,6 +194,7 @@ select "avatarColor", "profileImagePath", "profileChangedAt", + "clusterGroupId", "createdAt", "updatedAt", "deletedAt", @@ -237,6 +242,7 @@ select "avatarColor", "profileImagePath", "profileChangedAt", + "clusterGroupId", "createdAt", "updatedAt", "deletedAt", @@ -275,6 +281,7 @@ select "avatarColor", "profileImagePath", "profileChangedAt", + "clusterGroupId", "createdAt", "updatedAt", "deletedAt", diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index b315538bf9b80..62f2fcf6e99b3 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -420,23 +420,97 @@ class MemoryAccess { } } +class ClusterGroupAccess { + constructor(private db: Kysely) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + checkInviteAccess(userId: string, clusterGroupIds: Set) { + if (clusterGroupIds.size === 0) { + return new Set(); + } + + return this.db + .selectFrom('cluster_group_request') + .select('cluster_group_request.clusterGroupId') + .where('cluster_group_request.clusterGroupId', 'in', [...clusterGroupIds]) + .where('cluster_group_request.userId', '=', userId) + .execute() + .then((requests) => new Set(requests.map((request) => request.clusterGroupId))); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkOwnerAccess(userId: string, clusterGroupIds: Set) { + if (clusterGroupIds.size === 0) { + return new Set(); + } + + return this.db + .selectFrom('user') + .select('user.clusterGroupId') + .where('user.clusterGroupId', 'in', [...clusterGroupIds]) + .where('user.id', '=', userId) + .execute() + .then((users) => new Set(users.map((user) => user.clusterGroupId))); + } +} + +class ClusterGroupRequestAccess { + constructor(private db: Kysely) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + checkOwnerAccess(userId: string, clusterGroupRequestIds: Set) { + if (clusterGroupRequestIds.size === 0) { + return new Set(); + } + + return this.db + .selectFrom('cluster_group_request') + .select('cluster_group_request.id') + .where('cluster_group_request.id', 'in', [...clusterGroupRequestIds]) + .where('cluster_group_request.userId', '=', userId) + .execute() + .then((requests) => new Set(requests.map(({ id }) => id))); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + checkGroupAccess(userId: string, clusterGroupRequestIds: Set) { + if (clusterGroupRequestIds.size === 0) { + return new Set(); + } + + return this.db + .selectFrom('cluster_group_request') + .select('cluster_group_request.id') + .where('cluster_group_request.id', 'in', [...clusterGroupRequestIds]) + .where('cluster_group_request.clusterGroupId', '=', (eb) => + eb.selectFrom('user').select('user.clusterGroupId').where('user.id', '=', userId), + ) + .execute() + .then((requests) => new Set(requests.map(({ id }) => id))); + } +} + class PersonAccess { constructor(private db: Kysely) {} @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @ChunkedSet({ paramIndex: 1 }) - async checkOwnerAccess(userId: string, personIds: Set) { - if (personIds.size === 0) { + async checkOwnerAccess(userId: string, personGroupIds: Set) { + if (personGroupIds.size === 0) { return new Set(); } return this.db .selectFrom('person') - .select('person.id') - .where('person.id', 'in', [...personIds]) + .select('person.personGroupId') + .where('person.personGroupId', 'in', [...personGroupIds]) .where('person.ownerId', '=', userId) .execute() - .then((persons) => new Set(persons.map((person) => person.id))); + .then((persons) => new Set(persons.map((person) => person.personGroupId))); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @@ -526,6 +600,8 @@ export class AccessRepository { duplicate: DuplicateAccess; memory: MemoryAccess; notification: NotificationAccess; + clusterGroup: ClusterGroupAccess; + clusterGroupRequest: ClusterGroupRequestAccess; person: PersonAccess; partner: PartnerAccess; session: SessionAccess; @@ -542,6 +618,8 @@ export class AccessRepository { this.duplicate = new DuplicateAccess(db); this.memory = new MemoryAccess(db); this.notification = new NotificationAccess(db); + this.clusterGroup = new ClusterGroupAccess(db); + this.clusterGroupRequest = new ClusterGroupRequestAccess(db); this.person = new PersonAccess(db); this.partner = new PartnerAccess(db); this.session = new SessionAccess(db); diff --git a/server/src/repositories/asset-job.repository.ts b/server/src/repositories/asset-job.repository.ts index bab0c44a41c87..5c7924a2086c5 100644 --- a/server/src/repositories/asset-job.repository.ts +++ b/server/src/repositories/asset-job.repository.ts @@ -151,6 +151,8 @@ export class AssetJobRepository { .select(columns.asset) .select(withFaces) .select((eb) => withFiles(eb, AssetFileType.Sidecar)) + .innerJoin('user', 'user.id', 'asset.ownerId') + .select(['user.clusterGroupId']) .where('asset.id', '=', id) .executeTakeFirst(); } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 5c3d4addce071..9427bfca6a49e 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -37,6 +37,7 @@ import { anyUuid, asUuid, hasPeople, + inSharedAlbum, removeUndefinedKeys, truncatedDate, unnest, @@ -124,7 +125,7 @@ interface AssetGetByChecksumOptions { interface GetByIdsRelations { exifInfo?: boolean; - faces?: { person?: boolean; withDeleted?: boolean }; + faces?: { person?: boolean; withDeleted?: boolean; viewingUserId?: string }; files?: boolean; library?: boolean; owner?: boolean; @@ -513,12 +514,12 @@ export class AssetRepository { } @GenerateSql({ params: [[DummyValue.UUID]] }) - @ChunkedArray() - getByIdsWithAllRelationsButStacks(ids: string[]) { + @ChunkedArray({ paramIndex: 0 }) + getByIdsWithAllRelationsButStacks(ids: string[], viewingUserId?: string) { return this.db .selectFrom('asset') .selectAll('asset') - .select(withFacesAndPeople) + .select(withFacesAndPeople({ viewingUserId })) .select(withTags) .$call(withExif) .where('asset.id', '=', anyUuid(ids)) @@ -577,7 +578,11 @@ export class AssetRepository { .selectAll('asset') .where('asset.id', '=', asUuid(id)) .$if(!!exifInfo, withExif) - .$if(!!faces, (qb) => qb.select(faces?.person ? withFacesAndPeople : withFaces).$narrowType<{ faces: NotNull }>()) + .$if(!!faces, (qb) => + qb + .select(faces?.person ? withFacesAndPeople({ viewingUserId: faces.viewingUserId! }) : withFaces) + .$narrowType<{ faces: NotNull }>(), + ) .$if(!!library, (qb) => qb.select(withLibrary)) .$if(!!owner, (qb) => qb.select(withOwner)) .$if(!!smartSearch, withSmartSearch) @@ -639,12 +644,12 @@ export class AssetRepository { .selectFrom('asset') .selectAll('asset') .$call(withExif) - .$call((qb) => qb.select(withFacesAndPeople)) + .$call((qb) => qb.select(withFaces)) .$call((qb) => qb.select(withEdits)) .executeTakeFirst(); } - return this.getById(asset.id, { exifInfo: true, faces: { person: true }, edits: true }); + return this.getById(asset.id, { exifInfo: true, faces: {}, edits: true }); } async remove(asset: { id: string }): Promise { @@ -743,8 +748,8 @@ export class AssetRepository { .execute(); } - @GenerateSql({ params: [{}] }) - async getTimeBuckets(options: TimeBucketOptions): Promise { + @GenerateSql({ params: [{}, { user: { id: DummyValue.UUID } }] }) + async getTimeBuckets(options: TimeBucketOptions, auth: AuthDto): Promise { return this.db .with('asset', (qb) => qb @@ -781,7 +786,13 @@ export class AssetRepository { ) .where((eb) => eb.or([eb('asset.stackId', 'is', null), eb(eb.table('stack'), 'is not', null)])), ) - .$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!))) + .$if(!!options.userIds, (qb) => + qb.where((eb) => { + // TODO this should become a shared `hasAccess` style helper once implement sharing in more places + const isOwner = eb('asset.ownerId', '=', anyUuid(options.userIds!)); + return options.personId ? eb.or([isOwner, inSharedAlbum(eb, auth.user.id)]) : isOwner; + }), + ) .$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!)) .$if(!!options.assetType, (qb) => qb.where('asset.type', '=', options.assetType!)) .$if(options.isDuplicate !== undefined, (qb) => @@ -867,7 +878,12 @@ export class AssetRepository { ), ) .$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!])) - .$if(!!options.userIds, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!))) + .$if(!!options.userIds, (qb) => + qb.where((eb) => { + const isOwner = eb('asset.ownerId', '=', anyUuid(options.userIds!)); + return options.personId ? eb.or([isOwner, inSharedAlbum(eb, auth.user.id)]) : isOwner; + }), + ) .$if(options.isFavorite !== undefined, (qb) => qb.where('asset.isFavorite', '=', options.isFavorite!)) .$if(!!options.withStacked, (qb) => qb diff --git a/server/src/repositories/cluster-group.repository.ts b/server/src/repositories/cluster-group.repository.ts new file mode 100644 index 0000000000000..821870b342799 --- /dev/null +++ b/server/src/repositories/cluster-group.repository.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@nestjs/common'; +import { Insertable, Kysely, sql } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { columns } from 'src/database'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { DB } from 'src/schema'; +import { ClusterGroupRequestTable } from 'src/schema/tables/cluster-group-request.table'; + +@Injectable() +export class ClusterGroupRepository { + constructor(@InjectKysely() private db: Kysely) {} + + @GenerateSql() + create() { + return this.db.insertInto('cluster_group').defaultValues().returningAll().executeTakeFirstOrThrow(); + } + + @GenerateSql({ params: [{ clusterGroupId: DummyValue.UUID, userId: DummyValue.UUID }] }) + async hasOtherMembers({ clusterGroupId, userId }: { clusterGroupId: string; userId: string }): Promise { + const member = await this.db + .selectFrom('user') + .select('user.id') + .where('user.clusterGroupId', '=', clusterGroupId) + .where('user.id', '!=', userId) + .where('user.deletedAt', 'is', null) + .executeTakeFirst(); + + return !!member; + } + + @GenerateSql({ params: [{ clusterGroupId: DummyValue.UUID, userId: DummyValue.UUID }] }) + createRequest(request: Insertable) { + return this.db + .insertInto('cluster_group_request') + .values(request) + .onConflict((oc) => + // the update is pointless, but required for the query to return the conflicting row + oc.columns(['clusterGroupId', 'userId']).doUpdateSet({ clusterGroupId: request.clusterGroupId }), + ) + .returningAll() + .returning(sql`(xmax = 0)`.as('isInserted')) + .executeTakeFirst(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + getRequest(id: string) { + return this.db + .selectFrom('cluster_group_request') + .selectAll('cluster_group_request') + .where('cluster_group_request.id', '=', id) + .executeTakeFirst(); + } + + @GenerateSql({ params: [{ userId: DummyValue.UUID, clusterGroupId: DummyValue.UUID }] }) + searchRequests({ userId, clusterGroupId }: { userId?: string; clusterGroupId?: string } = {}) { + return this.db + .selectFrom('cluster_group_request') + .selectAll('cluster_group_request') + .$if(!!userId, (qb) => qb.where('cluster_group_request.userId', '=', userId!)) + .$if(!!clusterGroupId, (qb) => qb.where('cluster_group_request.clusterGroupId', '=', clusterGroupId!)) + .orderBy('cluster_group_request.createdAt', 'asc') + .execute(); + } + + @GenerateSql({ params: [{ clusterGroupId: DummyValue.UUID, userId: DummyValue.UUID }] }) + getUsers({ clusterGroupId, userId }: { clusterGroupId: string; userId: string }) { + return this.db + .selectFrom('user') + .select(columns.user) + .where('user.clusterGroupId', '=', clusterGroupId) + .where('user.deletedAt', 'is', null) + .orderBy((eb) => eb('user.id', '=', userId), 'desc') + .orderBy('user.name', 'asc') + .execute(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + async deleteRequest(id: string): Promise { + await this.db.deleteFrom('cluster_group_request').where('cluster_group_request.id', '=', id).execute(); + } +} diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 7fedc4eb3aed0..4ce5f7fbdcceb 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -2,10 +2,10 @@ import { Injectable } from '@nestjs/common'; import { ModuleRef, Reflector } from '@nestjs/core'; import _ from 'lodash'; import { Socket } from 'socket.io'; -import { SystemConfig } from 'src/config'; import { Asset } from 'src/database'; import { EventConfig } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfig } from 'src/dtos/config.dto'; import { ImmichWorker, JobStatus, MetadataKey, QueueName, UserAvatarColor, UserStatus } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -41,6 +41,9 @@ type EventMap = { AlbumUpdate: [{ id: string; userIds: string[]; recipientIds: string[] }]; AlbumInvite: [{ id: string; userId: string; senderName: string }]; + // cluster group events + ClusterGroupRequest: [{ clusterGroupId: string; userId: string; senderName: string }]; + // asset events AssetCreate: [{ asset: Pick; file?: UploadFile }]; AssetTag: [{ assetId: string; userId: string }]; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 534a71b6d6e5b..15ec7c353a980 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -7,6 +7,7 @@ import { AppRepository } from 'src/repositories/app.repository'; import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; +import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -83,6 +84,7 @@ export const repositories = [ NotificationRepository, OAuthRepository, OcrRepository, + ClusterGroupRepository, PartnerRepository, PersonRepository, PluginRepository, diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts index a05ddbc866baa..c1b14fae0ab98 100644 --- a/server/src/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -1,8 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Duration } from 'luxon'; import { readFile } from 'node:fs/promises'; -import { MachineLearningConfig } from 'src/config'; -import { CLIPConfig } from 'src/dtos/model-config.dto'; +import { MachineLearningConfig } from 'src/dtos/config.dto'; import { LoggingRepository } from 'src/repositories/logging.repository'; export interface BoundingBox { @@ -205,7 +204,7 @@ export class MachineLearningRepository { }; } - async encodeImage(imagePath: string, { modelName }: CLIPConfig) { + async encodeImage(imagePath: string, { modelName }: MachineLearningConfig['clip']) { const request = { [ModelTask.SEARCH]: { [ModelType.VISUAL]: { modelName } } }; const response = await this.predict({ imagePath }, request); return response[ModelTask.SEARCH]; diff --git a/server/src/repositories/media.repository.spec.ts b/server/src/repositories/media.repository.spec.ts index e8106c0ff97df..08475b4404b50 100644 --- a/server/src/repositories/media.repository.spec.ts +++ b/server/src/repositories/media.repository.spec.ts @@ -329,7 +329,7 @@ describe(MediaRepository.name, () => { const baseFace: AssetFace = { id: 'face-1', assetId: 'asset-1', - personId: 'person-1', + personGroupId: 'person-1', boundingBoxX1: 100, boundingBoxY1: 100, boundingBoxX2: 200, diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts index 09aa5ad880d4b..9e34efdf3f999 100644 --- a/server/src/repositories/memory.repository.ts +++ b/server/src/repositories/memory.repository.ts @@ -73,7 +73,11 @@ export class MemoryRepository implements IBulkAsset { eb.exists( eb .selectFrom('asset_face') - .innerJoin('person', 'person.id', 'asset_face.personId') + .innerJoin('person', (join) => + join + .onRef('person.personGroupId', '=', 'asset_face.personGroupId') + .onRef('person.ownerId', '=', 'asset.ownerId'), + ) .select((eb) => eb.val(1).as('one')) .whereRef('asset_face.assetId', '=', 'asset.id') .where('person.isHidden', '=', true), diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index 8ea6ed50fb62b..28aea850418bf 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -8,8 +8,9 @@ import { AssetFileType, AssetVisibility, SourceType, UserMetadataKey } from 'src import { DB } from 'src/schema'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; +import { PersonGroupTable } from 'src/schema/tables/person-group.table'; import { PersonTable } from 'src/schema/tables/person.table'; -import { dummy, removeUndefinedKeys, withFilePath } from 'src/utils/database'; +import { asUuid, dummy, inSharedAlbum, removeUndefinedKeys, withFilePath } from 'src/utils/database'; import { paginationHelper, PaginationOptions } from 'src/utils/pagination'; export interface PersonSearchOptions { @@ -22,19 +23,20 @@ export interface PersonNameSearchOptions { } export interface PersonNameResponse { - id: string; + personGroupId: string; name: string; } export interface AssetFaceId { assetId: string; - personId: string; + personGroupId: string; } export interface UpdateFacesData { - oldPersonId?: string; + oldPersonGroupId?: string; faceIds?: string[]; - newPersonId: string; + ownerId?: string; + newPersonGroupId: string; } export interface PersonStatistics { @@ -53,17 +55,34 @@ export interface GetAllPeopleOptions { } export interface GetAllFacesOptions { - personId?: string | null; + personGroupId?: string | null; assetId?: string; sourceType?: SourceType; } export type UnassignFacesOptions = DeleteFacesOptions; -const withPerson = (eb: ExpressionBuilder) => { - return jsonObjectFrom( - eb.selectFrom('person').selectAll('person').whereRef('person.id', '=', 'asset_face.personId'), - ).as('person'); +export type GetFacesOptions = WithPersonOptions & { isVisible?: boolean }; + +/** a person is identified by its owner and the group it belongs to */ +export type PersonId = { ownerId: string; personGroupId: string }; + +export type ReassignCluster = { userId: string; newClusterId: string }; + +export type WithPersonOptions = { + /** whose version of the person to select */ + viewingUserId: string; +}; + +const withPerson = ({ viewingUserId }: WithPersonOptions) => { + return (eb: ExpressionBuilder) => + jsonObjectFrom( + eb + .selectFrom('person') + .selectAll('person') + .whereRef('person.personGroupId', '=', 'asset_face.personGroupId') + .where('person.ownerId', '=', viewingUserId), + ).as('person'); }; const withFaceSearch = (eb: ExpressionBuilder) => { @@ -76,13 +95,18 @@ const withFaceSearch = (eb: ExpressionBuilder) => { export class PersonRepository { constructor(@InjectKysely() private db: Kysely) {} - @GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] }) - async reassignFaces({ oldPersonId, faceIds, newPersonId }: UpdateFacesData): Promise { + @GenerateSql({ params: [{ oldPersonGroupId: DummyValue.UUID, newPersonGroupId: DummyValue.UUID }] }) + async reassignFaces({ oldPersonGroupId, faceIds, ownerId, newPersonGroupId }: UpdateFacesData): Promise { const result = await this.db .updateTable('asset_face') - .set({ personId: newPersonId }) - .$if(!!oldPersonId, (qb) => qb.where('asset_face.personId', '=', oldPersonId!)) + .set({ personGroupId: newPersonGroupId }) + .$if(!!oldPersonGroupId, (qb) => qb.where('asset_face.personGroupId', '=', oldPersonGroupId!)) .$if(!!faceIds, (qb) => qb.where('asset_face.id', 'in', faceIds!)) + .$if(!!ownerId, (qb) => + qb.where('asset_face.personGroupId', 'in', (eb) => + eb.selectFrom('person').select('person.personGroupId').where('person.ownerId', '=', ownerId!), + ), + ) .executeTakeFirst(); return Number(result.numChangedRows ?? 0); @@ -91,19 +115,64 @@ export class PersonRepository { async unassignFaces({ sourceType }: UnassignFacesOptions): Promise { await this.db .updateTable('asset_face') - .set({ personId: null }) + .set({ personGroupId: null }) .where('asset_face.sourceType', '=', sourceType) .execute(); } + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.UUID] }) + @Chunked() + async delete(personGroupIds: string[], ownerId?: string) { + if (personGroupIds.length === 0) { + return []; + } + + return this.db + .deleteFrom('person') + .$if(!!ownerId, (qb) => qb.where('ownerId', '=', ownerId!)) + .where('person.personGroupId', 'in', personGroupIds) + .returning(['personGroupId', 'ownerId', 'thumbnailPath']) + .execute(); + } + @GenerateSql({ params: [[DummyValue.UUID]] }) @Chunked() - async delete(ids: string[]): Promise { + async deleteGroups(ids: string[]): Promise { if (ids.length === 0) { return; } - await this.db.deleteFrom('person').where('person.id', 'in', ids).execute(); + await this.db.deleteFrom('person_group').where('person_group.id', 'in', ids).execute(); + } + + @GenerateSql() + async deleteEmptyGroups(): Promise { + const result = await this.db + .deleteFrom('person_group') + .where(({ not, exists, selectFrom }) => + not( + exists( + selectFrom('person') + .whereRef('person.personGroupId', '=', 'person_group.id') + .select('person.personGroupId'), + ), + ), + ) + .executeTakeFirst(); + + return Number(result.numDeletedRows); + } + + @GenerateSql() + async deleteOrphanedClusterGroups(): Promise { + const result = await this.db + .deleteFrom('cluster_group') + .where(({ not, exists, selectFrom }) => + not(exists(selectFrom('user').whereRef('user.clusterGroupId', '=', 'cluster_group.id').select('user.id'))), + ) + .executeTakeFirst(); + + return Number(result.numDeletedRows); } async deleteFaces({ sourceType }: DeleteFacesOptions): Promise { @@ -114,8 +183,8 @@ export class PersonRepository { return this.db .selectFrom('asset_face') .selectAll('asset_face') - .$if(options.personId === null, (qb) => qb.where('asset_face.personId', 'is', null)) - .$if(!!options.personId, (qb) => qb.where('asset_face.personId', '=', options.personId!)) + .$if(options.personGroupId === null, (qb) => qb.where('asset_face.personGroupId', 'is', null)) + .$if(!!options.personGroupId, (qb) => qb.where('asset_face.personGroupId', '=', options.personGroupId!)) .$if(!!options.sourceType, (qb) => qb.where('asset_face.sourceType', '=', options.sourceType!)) .$if(!!options.assetId, (qb) => qb.where('asset_face.assetId', '=', options.assetId!)) .where('asset_face.deletedAt', 'is', null) @@ -139,7 +208,7 @@ export class PersonRepository { getFileSamples() { return this.db .selectFrom('person') - .select(['id', 'thumbnailPath']) + .select(['ownerId', 'personGroupId', 'thumbnailPath']) .where('thumbnailPath', '!=', sql.lit('')) .limit(sql.lit(3)) .execute(); @@ -150,10 +219,11 @@ export class PersonRepository { const items = await this.db .selectFrom('person') .selectAll('person') - .innerJoin('asset_face', 'asset_face.personId', 'person.id') + .innerJoin('asset_face', 'asset_face.personGroupId', 'person.personGroupId') .innerJoin('asset', (join) => join .onRef('asset_face.assetId', '=', 'asset.id') + .onRef('asset.ownerId', '=', 'person.ownerId') .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) .on('asset.deletedAt', 'is', null), ) @@ -178,7 +248,7 @@ export class PersonRepository { ), ]), ) - .groupBy('person.id') + .groupBy(['person.ownerId', 'person.personGroupId']) .$if(!!options?.closestFaceAssetId, (qb) => qb.orderBy((eb) => eb( @@ -216,22 +286,22 @@ export class PersonRepository { return this.db .selectFrom('person') .selectAll('person') - .leftJoin('asset_face', 'asset_face.personId', 'person.id') + .leftJoin('asset_face', 'asset_face.personGroupId', 'person.personGroupId') .where('asset_face.deletedAt', 'is', null) .where('asset_face.isVisible', 'is', true) .having((eb) => eb.fn.count('asset_face.assetId'), '=', 0) - .groupBy('person.id') + .groupBy(['person.ownerId', 'person.personGroupId']) .execute(); } - @GenerateSql({ params: [DummyValue.UUID] }) - getFaces(assetId: string, options?: { isVisible?: boolean }) { - const isVisible = options === undefined ? true : options.isVisible; + @GenerateSql({ params: [DummyValue.UUID, { viewingUserId: DummyValue.UUID, isVisible: true }] }) + getFaces(assetId: string, options: GetFacesOptions) { + const { viewingUserId, isVisible } = options; return this.db .selectFrom('asset_face') .selectAll('asset_face') - .select(withPerson) + .select(withPerson({ viewingUserId })) .where('asset_face.assetId', '=', assetId) .where('asset_face.deletedAt', 'is', null) .$if(isVisible !== undefined, (qb) => qb.where('asset_face.isVisible', '=', isVisible!)) @@ -239,13 +309,13 @@ export class PersonRepository { .execute(); } - @GenerateSql({ params: [DummyValue.UUID] }) - getFaceById(id: string) { + @GenerateSql({ params: [DummyValue.UUID, { viewingUserId: DummyValue.UUID }] }) + getFaceById(id: string, { viewingUserId }: WithPersonOptions) { // TODO return null instead of find or fail return this.db .selectFrom('asset_face') .selectAll('asset_face') - .select(withPerson) + .select(withPerson({ viewingUserId })) .where('asset_face.id', '=', id) .where('asset_face.deletedAt', 'is', null) .executeTakeFirstOrThrow(); @@ -255,12 +325,13 @@ export class PersonRepository { getFaceForFacialRecognitionJob(id: string) { return this.db .selectFrom('asset_face') - .select(['asset_face.id', 'asset_face.personId', 'asset_face.sourceType']) + .select(['asset_face.id', 'asset_face.personGroupId', 'asset_face.sourceType']) .select((eb) => jsonObjectFrom( eb .selectFrom('asset') - .select(['asset.ownerId', 'asset.visibility', 'asset.fileCreatedAt']) + .innerJoin('user', 'user.id', 'asset.ownerId') + .select(['asset.ownerId', 'asset.visibility', 'asset.fileCreatedAt', 'user.clusterGroupId']) .whereRef('asset.id', '=', 'asset_face.assetId'), ).as('asset'), ) @@ -270,8 +341,8 @@ export class PersonRepository { .executeTakeFirst(); } - @GenerateSql({ params: [DummyValue.UUID] }) - getDataForThumbnailGenerationJob(id: string) { + @GenerateSql({ params: [{ ownerId: DummyValue.UUID, personGroupId: DummyValue.UUID }] }) + getDataForThumbnailGenerationJob({ ownerId, personGroupId }: PersonId) { return this.db .selectFrom('person') .innerJoin('asset_face', 'asset_face.id', 'person.faceAssetId') @@ -290,27 +361,30 @@ export class PersonRepository { 'asset_exif.orientation as exifOrientation', ]) .select((eb) => withFilePath(eb, AssetFileType.Preview).as('previewPath')) - .where('person.id', '=', id) + .where('person.ownerId', '=', ownerId) + .where('person.personGroupId', '=', personGroupId) .where('asset_face.deletedAt', 'is', null) .executeTakeFirst(); } @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) - async reassignFace(assetFaceId: string, newPersonId: string): Promise { + async reassignFace(assetFaceId: string, newPersonGroupId: string): Promise { const result = await this.db .updateTable('asset_face') - .set({ personId: newPersonId }) + .set({ personGroupId: newPersonGroupId }) .where('asset_face.id', '=', assetFaceId) .executeTakeFirst(); return Number(result.numChangedRows ?? 0); } - getById(personId: string) { + @GenerateSql({ params: [{ ownerId: DummyValue.UUID, personGroupId: DummyValue.UUID }] }) + getByGroupId({ ownerId, personGroupId }: PersonId) { return this.db // .selectFrom('person') .selectAll('person') - .where('person.id', '=', personId) + .where('person.personGroupId', '=', personGroupId) + .where('person.ownerId', '=', ownerId) .executeTakeFirst(); } @@ -334,27 +408,28 @@ export class PersonRepository { getDistinctNames(userId: string, { withHidden }: PersonNameSearchOptions): Promise { return this.db .selectFrom('person') - .select(['person.id', 'person.name']) + .select(['person.personGroupId', 'person.name']) .distinctOn((eb) => eb.fn('lower', ['person.name'])) .where((eb) => eb.and([eb('person.ownerId', '=', userId), eb('person.name', '!=', '')])) .$if(!withHidden, (qb) => qb.where('person.isHidden', '=', false)) .execute(); } - @GenerateSql({ params: [DummyValue.UUID] }) - async getStatistics(personId: string): Promise { + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) + async getStatistics(personGroupId: string, userId: string): Promise { const result = await this.db .selectFrom('asset_face') .leftJoin('asset', (join) => join .onRef('asset.id', '=', 'asset_face.assetId') .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)) - .on('asset.deletedAt', 'is', null), + .on('asset.deletedAt', 'is', null) + .on((eb) => eb.or([eb('asset.ownerId', '=', asUuid(userId)), inSharedAlbum(eb, userId)])), ) .select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count')) .where('asset_face.deletedAt', 'is', null) .where('asset_face.isVisible', 'is', true) - .where('asset_face.personId', '=', personId) + .where('asset_face.personGroupId', '=', personGroupId) .executeTakeFirst(); return { @@ -371,7 +446,7 @@ export class PersonRepository { eb.exists((eb) => eb .selectFrom('asset_face') - .whereRef('asset_face.personId', '=', 'person.id') + .whereRef('asset_face.personGroupId', '=', 'person.personGroupId') .where('asset_face.deletedAt', 'is', null) .where('asset_face.isVisible', '=', true) .where((eb) => @@ -395,13 +470,124 @@ export class PersonRepository { return this.db.insertInto('person').values(person).returningAll().executeTakeFirstOrThrow(); } - async createAll(people: Insertable[]): Promise { + async createAll(people: Insertable[]) { if (people.length === 0) { return []; } - const results = await this.db.insertInto('person').values(people).returningAll().execute(); - return results.map(({ id }) => id); + return this.db.insertInto('person').values(people).returningAll().execute(); + } + + @GenerateSql({ params: [DummyValue.UUID] }) + createGroup(ownerId: string) { + return this.db + .insertInto('person_group') + .columns(['clusterGroupId']) + .expression((eb) => eb.selectFrom('user').select('user.clusterGroupId').where('user.id', '=', ownerId)) + .returningAll() + .executeTakeFirstOrThrow(); + } + + @GenerateSql({ params: [{ userId: DummyValue.UUID, newClusterId: DummyValue.UUID }] }) + async reassignCluster({ userId, newClusterId }: ReassignCluster): Promise { + await this.db.transaction().execute(async (trx) => { + // a group nobody else has people in moves across as it is + await trx + .updateTable('person_group') + .set({ clusterGroupId: newClusterId }) + .where('person_group.id', 'in', (eb) => + eb.selectFrom('person').select('person.personGroupId').where('person.ownerId', '=', userId), + ) + .where(({ not, exists, selectFrom }) => + not( + exists( + selectFrom('person') + .select('person.personGroupId') + .whereRef('person.personGroupId', '=', 'person_group.id') + .where('person.ownerId', '!=', userId), + ), + ), + ) + .execute(); + + // the rest is shared with someone else, so this user gets a group of their own for each + const mapping = await trx + .with('shared', (db) => + db + .selectFrom('person') + .select('person.personGroupId as oldId') + .distinct() + .where('person.ownerId', '=', userId) + .where(({ exists, selectFrom }) => + exists( + selectFrom('person as other') + .select('other.personGroupId') + .whereRef('other.personGroupId', '=', 'person.personGroupId') + .where('other.ownerId', '!=', userId), + ), + ), + ) + .with( + (cte) => cte('mapping').materialized(), + (db) => db.selectFrom('shared').select(['shared.oldId', sql`uuid_generate_v4()`.as('newId')]), + ) + .with('created', (db) => + db + .insertInto('person_group') + .columns(['id', 'clusterGroupId']) + .expression((eb) => + eb.selectFrom('mapping').select(['mapping.newId', sql.val(newClusterId).as('clusterGroupId')]), + ), + ) + .selectFrom('mapping') + .select(['mapping.oldId', 'mapping.newId']) + .execute(); + + if (mapping.length === 0) { + return; + } + + const oldIds = mapping.map(({ oldId }) => oldId); + const newIds = mapping.map(({ newId }) => newId); + const remapped = sql<{ + oldId: string; + newId: string; + }>`(select unnest(${`{${oldIds}}`}::uuid[]) as "oldId", unnest(${`{${newIds}}`}::uuid[]) as "newId")`.as( + 'mapping', + ); + + await trx + .updateTable('person') + .from(remapped) + .set((eb) => ({ personGroupId: eb.ref('mapping.newId') })) + .whereRef('person.personGroupId', '=', 'mapping.oldId') + .where('person.ownerId', '=', userId) + .execute(); + + await trx + .updateTable('asset_face') + .from(remapped) + .set((eb) => ({ personGroupId: eb.ref('mapping.newId') })) + .whereRef('asset_face.personGroupId', '=', 'mapping.oldId') + .where(({ exists, selectFrom }) => + exists( + selectFrom('asset') + .select('asset.id') + .whereRef('asset.id', '=', 'asset_face.assetId') + .where('asset.ownerId', '=', userId), + ), + ) + .execute(); + }); + } + + @GenerateSql({ params: [DummyValue.UUID, 2] }) + async createGroups(personGroups: Insertable[]) { + if (personGroups.length === 0) { + return []; + } + + return this.db.insertInto('person_group').values(personGroups).returningAll().execute(); } @GenerateSql({ params: [[], [], [{ faceId: DummyValue.UUID, embedding: DummyValue.VECTOR }]] }) @@ -428,11 +614,12 @@ export class PersonRepository { await query.selectFrom(dummy).execute(); } - async update(person: Updateable & { id: string }) { + async update(person: Updateable & PersonId) { return this.db .updateTable('person') .set(person) - .where('person.id', '=', person.id) + .where('person.ownerId', '=', person.ownerId) + .where('person.personGroupId', '=', person.personGroupId) .returningAll() .executeTakeFirstOrThrow(); } @@ -446,7 +633,7 @@ export class PersonRepository { .insertInto('person') .values(people) .onConflict((oc) => - oc.column('id').doUpdateSet((eb) => + oc.columns(['ownerId', 'personGroupId']).doUpdateSet((eb) => removeUndefinedKeys( { name: eb.ref('excluded.name'), @@ -464,36 +651,38 @@ export class PersonRepository { .execute(); } - @GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] }) + @GenerateSql({ + params: [[{ assetId: DummyValue.UUID, personGroupId: DummyValue.UUID }], { viewingUserId: DummyValue.UUID }], + }) @ChunkedArray() - getFacesByIds(ids: AssetFaceId[]) { + getFacesByIds(ids: AssetFaceId[], { viewingUserId }: WithPersonOptions) { if (ids.length === 0) { return Promise.resolve([]); } const assetIds: string[] = []; - const personIds: string[] = []; - for (const { assetId, personId } of ids) { + const personGroupIds: string[] = []; + for (const { assetId, personGroupId } of ids) { assetIds.push(assetId); - personIds.push(personId); + personGroupIds.push(personGroupId); } return this.db .selectFrom('asset_face') .selectAll('asset_face') - .select(withPerson) + .select(withPerson({ viewingUserId })) .where('asset_face.assetId', 'in', assetIds) - .where('asset_face.personId', 'in', personIds) + .where('asset_face.personGroupId', 'in', personGroupIds) .where('asset_face.deletedAt', 'is', null) .execute(); } @GenerateSql({ params: [DummyValue.UUID] }) - getRandomFace(personId: string) { + getRandomFace(personGroupId: string) { return this.db .selectFrom('asset_face') .selectAll('asset_face') - .where('asset_face.personId', '=', personId) + .where('asset_face.personGroupId', '=', personGroupId) .where('asset_face.deletedAt', 'is', null) .where('asset_face.isVisible', 'is', true) .executeTakeFirst(); @@ -532,15 +721,6 @@ export class PersonRepository { } } - @GenerateSql({ params: [[DummyValue.UUID]] }) - @Chunked() - getForPeopleDelete(ids: string[]) { - if (ids.length === 0) { - return Promise.resolve([]); - } - return this.db.selectFrom('person').select(['id', 'thumbnailPath']).where('id', 'in', ids).execute(); - } - @GenerateSql({ params: [[], []] }) async updateVisibility(visible: AssetFace[], hidden: AssetFace[]): Promise { if (visible.length === 0 && hidden.length === 0) { @@ -574,14 +754,24 @@ export class PersonRepository { }); } - @GenerateSql({ params: [{ personId: DummyValue.UUID, assetId: DummyValue.UUID }] }) - getForFeatureFaceUpdate({ personId, assetId }: { personId: string; assetId: string }) { + @GenerateSql({ params: [{ personGroupId: DummyValue.UUID, assetId: DummyValue.UUID }] }) + getForFeatureFaceUpdate({ personGroupId, assetId }: { personGroupId: string; assetId: string }) { return this.db .selectFrom('asset_face') .select('asset_face.id') .where('asset_face.assetId', '=', assetId) - .where('asset_face.personId', '=', personId) + .where('asset_face.personGroupId', '=', personGroupId) .innerJoin('asset', (join) => join.onRef('asset.id', '=', 'asset_face.assetId').on('asset.isOffline', '=', false)) .executeTakeFirst(); } + + @GenerateSql({ params: [[DummyValue.UUID]] }) + getForMergePerson(personGroupIds: string[]) { + return this.db + .selectFrom('person') + .selectAll('person') + .where('person.personGroupId', 'in', personGroupIds) + .orderBy('person.ownerId') + .execute(); + } } diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 4bde10f165174..ce055a0044aa6 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -54,6 +54,8 @@ export interface SearchOneToOneRelationOptions { export interface SearchRelationOptions extends SearchOneToOneRelationOptions { withFaces?: boolean; withPeople?: boolean; + /** whose version of the people to select, required when selecting faces or people */ + viewingUserId?: string; } export interface SearchDateOptions { @@ -137,6 +139,8 @@ export interface AssetSearchBuilderV3Options { filter?: SearchFilter; /** Server-derived ownership scope. Never client-controlled. */ userIds?: string[]; + /** whose version of the people to select, required when selecting faces or people */ + viewingUserId?: string; withExif?: boolean; withFaces?: boolean; withPeople?: boolean; @@ -156,11 +160,12 @@ export type SmartSearchOptions = SearchDateOptions & SearchUserIdOptions & SearchPeopleOptions & SearchTagOptions & - SearchOcrOptions & { visibility?: AssetVisibility | 'not-locked' }; + SearchOcrOptions & { visibility?: AssetVisibility | 'not-locked'; viewingUserId?: string }; export type LargeAssetSearchOptions = AssetSearchOptions & { minFileSize?: number }; -export interface FaceEmbeddingSearch extends SearchEmbeddingOptions { +export interface FaceEmbeddingSearch extends Omit { + clusterGroupId: string; hasPerson?: boolean; numResults: number; maxDistance: number; @@ -170,7 +175,7 @@ export interface FaceEmbeddingSearch extends SearchEmbeddingOptions { export interface FaceSearchResult { distance: number; id: string; - personId: string | null; + personGroupId: string | null; } export interface AssetDuplicateResult { @@ -339,7 +344,7 @@ export class SearchRepository { }, ], }) - searchFaces({ userIds, embedding, numResults, maxDistance, hasPerson, minBirthDate }: FaceEmbeddingSearch) { + searchFaces({ clusterGroupId, embedding, numResults, maxDistance, hasPerson, minBirthDate }: FaceEmbeddingSearch) { if (!z.int().min(1).max(1000).safeParse(numResults).success) { throw new Error(`Invalid value for 'numResults': ${numResults}`); } @@ -350,20 +355,29 @@ export class SearchRepository { .with('cte', (qb) => qb .selectFrom('asset_face') + .innerJoin('asset', 'asset.id', 'asset_face.assetId') + .innerJoin('face_search', 'face_search.faceId', 'asset_face.id') .select([ 'asset_face.id', - 'asset_face.personId', + 'asset_face.personGroupId', sql`face_search.embedding <=> ${embedding}`.as('distance'), ]) - .innerJoin('asset', 'asset.id', 'asset_face.assetId') - .innerJoin('face_search', 'face_search.faceId', 'asset_face.id') - .leftJoin('person', 'person.id', 'asset_face.personId') - .where('asset.ownerId', '=', anyUuid(userIds)) + .where('asset.ownerId', 'in', (eb) => + eb.selectFrom('user').select('user.id').where('user.clusterGroupId', '=', clusterGroupId), + ) .where('asset.deletedAt', 'is', null) - .$if(!!hasPerson, (qb) => qb.where('asset_face.personId', 'is not', null)) + .$if(!!hasPerson, (qb) => qb.where('asset_face.personGroupId', 'is not', null)) .$if(!!minBirthDate, (qb) => qb.where((eb) => - eb.or([eb('person.birthDate', 'is', null), eb('person.birthDate', '<=', minBirthDate!)]), + eb.not( + eb.exists( + eb + .selectFrom('person') + .select('person.personGroupId') + .whereRef('person.personGroupId', '=', 'asset_face.personGroupId') + .where('person.birthDate', '>', minBirthDate!), + ), + ), ), ) .orderBy('distance') diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index 5cfded148d2ad..4b0e13356d5f8 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -4,7 +4,7 @@ import { exec as execCallback } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import sharp from 'sharp'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; +import { ReleaseChannel } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index eca8d18e66152..a8359cac5a031 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -65,6 +65,7 @@ export class SyncRepository { partnerAssetExif: PartnerAssetExifsSync; partnerStack: PartnerStackSync; person: PersonSync; + personGroup: PersonGroupSync; stack: StackSync; user: UserSync; userMetadata: UserMetadataSync; @@ -89,6 +90,7 @@ export class SyncRepository { this.partnerAssetExif = new PartnerAssetExifsSync(this.db); this.partnerStack = new PartnerStackSync(this.db); this.person = new PersonSync(this.db); + this.personGroup = new PersonGroupSync(this.db); this.stack = new StackSync(this.db); this.user = new UserSync(this.db); this.userMetadata = new UserMetadataSync(this.db); @@ -422,7 +424,7 @@ class PersonSync extends BaseSync { @GenerateSql({ params: [dummyQueryOptions], stream: true }) getDeletes(options: SyncQueryOptions) { return this.auditQuery('person_audit', options) - .select(['id', 'personId']) + .select(['id', 'personGroupId as personId']) .where('ownerId', '=', options.userId) .stream(); } @@ -435,7 +437,7 @@ class PersonSync extends BaseSync { getUpserts(options: SyncQueryOptions) { return this.upsertQuery('person', options) .select([ - 'id', + 'personGroupId as id', 'createdAt', 'updatedAt', 'ownerId', @@ -452,6 +454,12 @@ class PersonSync extends BaseSync { } } +class PersonGroupSync extends BaseSync { + cleanupAuditTable(daysAgo: number) { + return this.auditCleanup('person_group_audit', daysAgo); + } +} + class AssetFaceSync extends BaseSync { @GenerateSql({ params: [dummyQueryOptions], stream: true }) getDeletes(options: SyncQueryOptions) { @@ -472,7 +480,7 @@ class AssetFaceSync extends BaseSync { .select([ 'asset_face.id', 'assetId', - 'personId', + 'personGroupId as personId', 'imageWidth', 'imageHeight', 'boundingBoxX1', diff --git a/server/src/schema/functions.ts b/server/src/schema/functions.ts index 2ee61d82dd57a..531c9b81bf44f 100644 --- a/server/src/schema/functions.ts +++ b/server/src/schema/functions.ts @@ -212,8 +212,21 @@ export const person_delete_audit = registerFunction({ language: 'PLPGSQL', body: ` BEGIN - INSERT INTO person_audit ("personId", "ownerId") - SELECT "id", "ownerId" + INSERT INTO person_audit ("personGroupId", "ownerId") + SELECT "personGroupId", "ownerId" + FROM OLD; + RETURN NULL; + END`, +}); + +export const person_group_delete_audit = registerFunction({ + name: 'person_group_delete_audit', + returnType: 'TRIGGER', + language: 'PLPGSQL', + body: ` + BEGIN + INSERT INTO person_group_audit ("personGroupId", "clusterGroupId") + SELECT "id", "clusterGroupId" FROM OLD; RETURN NULL; END`, diff --git a/server/src/schema/index.ts b/server/src/schema/index.ts index 45423f7c40167..61dc5d8760622 100644 --- a/server/src/schema/index.ts +++ b/server/src/schema/index.ts @@ -21,6 +21,7 @@ import { memory_delete_audit, partner_delete_audit, person_delete_audit, + person_group_delete_audit, stack_delete_audit, updated_at, user_delete_audit, @@ -48,6 +49,8 @@ import { AssetMetadataTable } from 'src/schema/tables/asset-metadata.table'; import { AssetOcrAuditTable } from 'src/schema/tables/asset-ocr-audit.table'; import { AssetOcrTable } from 'src/schema/tables/asset-ocr.table'; import { AssetTable } from 'src/schema/tables/asset.table'; +import { ClusterGroupRequestTable } from 'src/schema/tables/cluster-group-request.table'; +import { ClusterGroupTable } from 'src/schema/tables/cluster-group.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; import { GeodataPlacesTable } from 'src/schema/tables/geodata-places.table'; import { IntegrityReportTable } from 'src/schema/tables/integrity-report.table'; @@ -63,6 +66,8 @@ import { OcrSearchTable } from 'src/schema/tables/ocr-search.table'; import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table'; import { PartnerTable } from 'src/schema/tables/partner.table'; import { PersonAuditTable } from 'src/schema/tables/person-audit.table'; +import { PersonGroupAuditTable } from 'src/schema/tables/person-group-audit.table'; +import { PersonGroupTable } from 'src/schema/tables/person-group.table'; import { PersonTable } from 'src/schema/tables/person.table'; import { PluginMethodTable } from 'src/schema/tables/plugin-method.table'; import { PluginTable } from 'src/schema/tables/plugin.table'; @@ -116,6 +121,8 @@ export class ImmichDatabase { AssetTable, AssetFileTable, AssetExifTable, + ClusterGroupTable, + ClusterGroupRequestTable, FaceSearchTable, GeodataPlacesTable, IntegrityReportTable, @@ -132,6 +139,8 @@ export class ImmichDatabase { PartnerTable, PersonTable, PersonAuditTable, + PersonGroupTable, + PersonGroupAuditTable, SessionTable, SharedLinkAssetTable, SharedLinkTable, @@ -172,6 +181,7 @@ export class ImmichDatabase { memory_asset_delete_audit, stack_delete_audit, person_delete_audit, + person_group_delete_audit, user_metadata_audit, asset_metadata_audit, asset_face_audit, @@ -246,6 +256,11 @@ export interface DB { person: PersonTable; person_audit: PersonAuditTable; + person_group: PersonGroupTable; + person_group_audit: PersonGroupAuditTable; + + cluster_group: ClusterGroupTable; + cluster_group_request: ClusterGroupRequestTable; session: SessionTable; session_sync_checkpoint: SessionSyncCheckpointTable; diff --git a/server/src/schema/migrations/1787148183729-ClusterGroups.ts b/server/src/schema/migrations/1787148183729-ClusterGroups.ts new file mode 100644 index 0000000000000..13b95762d8221 --- /dev/null +++ b/server/src/schema/migrations/1787148183729-ClusterGroups.ts @@ -0,0 +1,232 @@ +import { Kysely, sql } from 'kysely'; + +export async function up(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION person_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO person_audit ("personGroupId", "ownerId") + SELECT "personGroupId", "ownerId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "person_delete_audit" + AFTER DELETE ON "person" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() <= 1) + EXECUTE FUNCTION person_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE FUNCTION person_group_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO person_group_audit ("personGroupId", "clusterGroupId") + SELECT "id", "clusterGroupId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE TABLE "cluster_group" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "name" character varying, + "createdAt" timestamp with time zone NOT NULL DEFAULT now(), + "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), + "updateId" uuid NOT NULL DEFAULT immich_uuid_v7(), + CONSTRAINT "cluster_group_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "cluster_group_updateId_idx" ON "cluster_group" ("updateId");`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "cluster_group_updatedAt" + BEFORE UPDATE ON "cluster_group" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + + await sql`ALTER TABLE "user" ADD "clusterGroupId" uuid;`.execute(db); + await sql`UPDATE "user" SET "clusterGroupId" = uuid_generate_v4();`.execute(db); + await sql`INSERT INTO "cluster_group" ("id") SELECT "clusterGroupId" FROM "user";`.execute(db); + await sql`ALTER TABLE "user" ALTER COLUMN "clusterGroupId" SET NOT NULL;`.execute(db); + await sql`CREATE INDEX "user_clusterGroupId_idx" ON "user" ("clusterGroupId");`.execute(db); + await sql`ALTER TABLE "user" ADD CONSTRAINT "user_clusterGroupId_fkey" FOREIGN KEY ("clusterGroupId") REFERENCES "cluster_group" ("id") ON UPDATE CASCADE ON DELETE NO ACTION;`.execute(db); + + await sql`CREATE TABLE "cluster_group_request" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "clusterGroupId" uuid NOT NULL, + "userId" uuid NOT NULL, + "createdAt" timestamp with time zone NOT NULL DEFAULT now(), + CONSTRAINT "cluster_group_request_clusterGroupId_fkey" FOREIGN KEY ("clusterGroupId") REFERENCES "cluster_group" ("id") ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT "cluster_group_request_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT "cluster_group_request_clusterGroupId_userId_uq" UNIQUE ("clusterGroupId", "userId"), + CONSTRAINT "cluster_group_request_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "cluster_group_request_clusterGroupId_idx" ON "cluster_group_request" ("clusterGroupId");`.execute( + db, + ); + await sql`CREATE INDEX "cluster_group_request_userId_idx" ON "cluster_group_request" ("userId");`.execute(db); + + await sql`CREATE TABLE "person_group" ( + "id" uuid NOT NULL DEFAULT uuid_generate_v4(), + "clusterGroupId" uuid NOT NULL, + "createdAt" timestamp with time zone NOT NULL DEFAULT now(), + "createId" uuid NOT NULL DEFAULT immich_uuid_v7(), + "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), + "updateId" uuid NOT NULL DEFAULT immich_uuid_v7(), + CONSTRAINT "person_group_clusterGroupId_fkey" FOREIGN KEY ("clusterGroupId") REFERENCES "cluster_group" ("id") ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT "person_group_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "person_group_clusterGroupId_idx" ON "person_group" ("clusterGroupId");`.execute(db); + await sql`CREATE INDEX "person_group_createId_idx" ON "person_group" ("createId");`.execute(db); + await sql`CREATE INDEX "person_group_updateId_idx" ON "person_group" ("updateId");`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "person_group_delete_audit" + AFTER DELETE ON "person_group" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION person_group_delete_audit();`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "person_group_updatedAt" + BEFORE UPDATE ON "person_group" + FOR EACH ROW + EXECUTE FUNCTION updated_at();`.execute(db); + await sql`CREATE TABLE "person_group_audit" ( + "id" uuid NOT NULL DEFAULT immich_uuid_v7(), + "personGroupId" uuid NOT NULL, + "clusterGroupId" uuid NOT NULL, + "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), + CONSTRAINT "person_group_audit_pkey" PRIMARY KEY ("id") +);`.execute(db); + await sql`CREATE INDEX "person_group_audit_personGroupId_idx" ON "person_group_audit" ("personGroupId");`.execute(db); + await sql`CREATE INDEX "person_group_audit_clusterGroupId_idx" ON "person_group_audit" ("clusterGroupId");`.execute(db); + await sql`CREATE INDEX "person_group_audit_deletedAt_idx" ON "person_group_audit" ("deletedAt");`.execute(db); + + await sql`ALTER TABLE "person" ADD "personGroupId" uuid;`.execute(db); + await sql`INSERT INTO "person_group" ("id", "clusterGroupId", "createdAt") + SELECT "person"."id", "user"."clusterGroupId", "person"."createdAt" + FROM "person" + INNER JOIN "user" ON "user"."id" = "person"."ownerId";`.execute(db); + await sql`UPDATE "person" SET "personGroupId" = "id";`.execute(db); + await sql`ALTER TABLE "person" ALTER COLUMN "personGroupId" SET NOT NULL;`.execute(db); + await sql`CREATE INDEX "person_personGroupId_idx" ON "person" ("personGroupId");`.execute(db); + await sql`ALTER TABLE "person" ADD CONSTRAINT "person_personGroupId_fkey" FOREIGN KEY ("personGroupId") REFERENCES "person_group" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute( + db, + ); + + await sql`ALTER TABLE "person_audit" ADD "personGroupId" uuid;`.execute(db); + await sql`UPDATE "person_audit" SET "personGroupId" = "personId";`.execute(db); + await sql`ALTER TABLE "person_audit" ALTER COLUMN "personGroupId" SET NOT NULL;`.execute(db); + await sql`ALTER TABLE "person_audit" DROP COLUMN "personId";`.execute(db); + await sql`CREATE INDEX "person_audit_personGroupId_idx" ON "person_audit" ("personGroupId");`.execute(db); + + await sql`ALTER TABLE "asset_face" DROP CONSTRAINT "asset_face_personId_fkey";`.execute(db); + await sql`ALTER TABLE "asset_face" RENAME COLUMN "personId" TO "personGroupId";`.execute(db); + await sql`UPDATE "asset_face" SET "personGroupId" = "person"."personGroupId" + FROM "person" + WHERE "person"."id" = "asset_face"."personGroupId";`.execute(db); + await sql`ALTER TABLE "asset_face" ADD CONSTRAINT "asset_face_personGroupId_fkey" FOREIGN KEY ("personGroupId") REFERENCES "person_group" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute( + db, + ); + await sql`CREATE INDEX "asset_face_personGroupId_assetId_idx" ON "asset_face" ("personGroupId", "assetId");`.execute( + db, + ); + await sql`CREATE INDEX "asset_face_personGroupId_assetId_notDeleted_isVisible_idx" ON "asset_face" ("personGroupId", "assetId") WHERE ("deletedAt" IS NULL AND "isVisible" IS TRUE);`.execute( + db, + ); + await sql`CREATE INDEX "asset_face_assetId_personGroupId_idx" ON "asset_face" ("assetId", "personGroupId");`.execute( + db, + ); + await sql`DROP INDEX "asset_face_assetId_personId_idx";`.execute(db); + await sql`DROP INDEX "asset_face_personId_assetId_idx";`.execute(db); + await sql`DROP INDEX "asset_face_personId_assetId_notDeleted_isVisible_idx";`.execute(db); + + // a person is identified by its owner and the group it belongs to + await sql`ALTER TABLE "person" DROP CONSTRAINT "person_pkey";`.execute(db); + await sql`ALTER TABLE "person" DROP COLUMN "id";`.execute(db); + await sql`ALTER TABLE "person" ADD CONSTRAINT "person_pkey" PRIMARY KEY ("ownerId", "personGroupId");`.execute(db); + await sql`DROP INDEX "person_ownerId_idx";`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"person_delete_audit","sql":"CREATE OR REPLACE FUNCTION person_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO person_audit (\\"personGroupId\\", \\"ownerId\\")\\n SELECT \\"personGroupId\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_person_delete_audit';`.execute(db); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"trigger","name":"person_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"person_delete_audit\\"\\n AFTER DELETE ON \\"person\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() <= 1)\\n EXECUTE FUNCTION person_delete_audit();"}'::jsonb WHERE "name" = 'trigger_person_delete_audit';`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_person_group_delete_audit', '{"type":"function","name":"person_group_delete_audit","sql":"CREATE OR REPLACE FUNCTION person_group_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO person_group_audit (\\"personGroupId\\", \\"clusterGroupId\\")\\n SELECT \\"id\\", \\"clusterGroupId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_cluster_group_updatedAt', '{"type":"trigger","name":"cluster_group_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"cluster_group_updatedAt\\"\\n BEFORE UPDATE ON \\"cluster_group\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_person_group_delete_audit', '{"type":"trigger","name":"person_group_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"person_group_delete_audit\\"\\n AFTER DELETE ON \\"person_group\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION person_group_delete_audit();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_person_group_updatedAt', '{"type":"trigger","name":"person_group_updatedAt","sql":"CREATE OR REPLACE TRIGGER \\"person_group_updatedAt\\"\\n BEFORE UPDATE ON \\"person_group\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION updated_at();"}'::jsonb);`.execute(db); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_face_personGroupId_assetId_notDeleted_isVisible_idx', '{"type":"index","name":"asset_face_personGroupId_assetId_notDeleted_isVisible_idx","sql":"CREATE INDEX \\"asset_face_personGroupId_assetId_notDeleted_isVisible_idx\\" ON \\"asset_face\\" (\\"personGroupId\\", \\"assetId\\") WHERE (\\"deletedAt\\" IS NULL AND \\"isVisible\\" IS TRUE);"}'::jsonb);`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_face_personId_assetId_notDeleted_isVisible_idx';`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`CREATE OR REPLACE FUNCTION person_delete_audit() + RETURNS TRIGGER + LANGUAGE PLPGSQL + AS $$ + BEGIN + INSERT INTO person_audit ("personId", "ownerId") + SELECT "id", "ownerId" + FROM OLD; + RETURN NULL; + END + $$;`.execute(db); + await sql`CREATE OR REPLACE TRIGGER "person_delete_audit" + AFTER DELETE ON "person" + REFERENCING OLD TABLE AS "old" + FOR EACH STATEMENT + WHEN (pg_trigger_depth() = 0) + EXECUTE FUNCTION person_delete_audit();`.execute(db); + + await sql`ALTER TABLE "person" DROP CONSTRAINT "person_pkey";`.execute(db); + await sql`ALTER TABLE "person" ADD "id" uuid NOT NULL DEFAULT uuid_generate_v4();`.execute(db); + await sql`UPDATE "person" SET "id" = "personGroupId";`.execute(db); + await sql`ALTER TABLE "person" ADD CONSTRAINT "person_pkey" PRIMARY KEY ("id");`.execute(db); + await sql`CREATE INDEX "person_ownerId_idx" ON "person" ("ownerId");`.execute(db); + + await sql`DROP INDEX "asset_face_assetId_personGroupId_idx";`.execute(db); + await sql`DROP INDEX "asset_face_personGroupId_assetId_notDeleted_isVisible_idx";`.execute(db); + await sql`DROP INDEX "asset_face_personGroupId_assetId_idx";`.execute(db); + await sql`ALTER TABLE "asset_face" DROP CONSTRAINT "asset_face_personGroupId_fkey";`.execute(db); + await sql`ALTER TABLE "asset_face" RENAME COLUMN "personGroupId" TO "personId";`.execute(db); + await sql`UPDATE "asset_face" SET "personId" = "person"."id" + FROM "person" + WHERE "person"."personGroupId" = "asset_face"."personId";`.execute(db); + await sql`ALTER TABLE "asset_face" ADD CONSTRAINT "asset_face_personId_fkey" FOREIGN KEY ("personId") REFERENCES "person" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute( + db, + ); + await sql`CREATE INDEX "asset_face_assetId_personId_idx" ON "asset_face" ("assetId", "personId");`.execute(db); + await sql`CREATE INDEX "asset_face_personId_assetId_idx" ON "asset_face" ("personId", "assetId");`.execute(db); + await sql`CREATE INDEX "asset_face_personId_assetId_notDeleted_isVisible_idx" ON "asset_face" ("personId", "assetId") WHERE ("deletedAt" IS NULL AND "isVisible" IS TRUE);`.execute( + db, + ); + + await sql`ALTER TABLE "person_audit" ADD "personId" uuid;`.execute(db); + await sql`UPDATE "person_audit" SET "personId" = "personGroupId";`.execute(db); + await sql`ALTER TABLE "person_audit" ALTER COLUMN "personId" SET NOT NULL;`.execute(db); + await sql`CREATE INDEX "person_audit_personId_idx" ON "person_audit" ("personId");`.execute(db); + await sql`DROP INDEX "person_audit_personGroupId_idx";`.execute(db); + await sql`ALTER TABLE "person_audit" DROP COLUMN "personGroupId";`.execute(db); + await sql`ALTER TABLE "person" DROP CONSTRAINT "person_personGroupId_fkey";`.execute(db); + await sql`DROP INDEX "person_personGroupId_idx";`.execute(db); + await sql`ALTER TABLE "person" DROP COLUMN "personGroupId";`.execute(db); + await sql`ALTER TABLE "user" DROP CONSTRAINT "user_clusterGroupId_fkey";`.execute(db); + await sql`DROP INDEX "user_clusterGroupId_idx";`.execute(db); + await sql`ALTER TABLE "user" DROP COLUMN "clusterGroupId";`.execute(db); + await sql`DROP TABLE "cluster_group_request";`.execute(db); + await sql`DROP TABLE "person_group_audit";`.execute(db); + await sql`DROP TABLE "person_group";`.execute(db); + await sql`DROP TABLE "cluster_group";`.execute(db); + await sql`DROP FUNCTION person_group_delete_audit;`.execute(db); + + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"function","name":"person_delete_audit","sql":"CREATE OR REPLACE FUNCTION person_delete_audit()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n INSERT INTO person_audit (\\"personId\\", \\"ownerId\\")\\n SELECT \\"id\\", \\"ownerId\\"\\n FROM OLD;\\n RETURN NULL;\\n END\\n $$;"}'::jsonb WHERE "name" = 'function_person_delete_audit';`.execute( + db, + ); + await sql`UPDATE "migration_overrides" SET "value" = '{"type":"trigger","name":"person_delete_audit","sql":"CREATE OR REPLACE TRIGGER \\"person_delete_audit\\"\\n AFTER DELETE ON \\"person\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH STATEMENT\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION person_delete_audit();"}'::jsonb WHERE "name" = 'trigger_person_delete_audit';`.execute( + db, + ); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_person_group_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_cluster_group_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_person_group_delete_audit';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_person_group_updatedAt';`.execute(db); + await sql`DELETE FROM "migration_overrides" WHERE "name" = 'index_asset_face_personGroupId_assetId_notDeleted_isVisible_idx';`.execute( + db, + ); + await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('index_asset_face_personId_assetId_notDeleted_isVisible_idx', '{"type":"index","name":"asset_face_personId_assetId_notDeleted_isVisible_idx","sql":"CREATE INDEX \\"asset_face_personId_assetId_notDeleted_isVisible_idx\\" ON \\"asset_face\\" (\\"personId\\", \\"assetId\\") WHERE (\\"deletedAt\\" IS NULL AND \\"isVisible\\" IS TRUE);"}'::jsonb);`.execute( + db, + ); +} diff --git a/server/src/schema/tables/asset-face.table.ts b/server/src/schema/tables/asset-face.table.ts index b67e5e5dac550..9832f99ce8938 100644 --- a/server/src/schema/tables/asset-face.table.ts +++ b/server/src/schema/tables/asset-face.table.ts @@ -15,7 +15,7 @@ import { SourceType } from 'src/enum'; import { asset_face_source_type } from 'src/schema/enums'; import { asset_face_audit } from 'src/schema/functions'; import { AssetTable } from 'src/schema/tables/asset.table'; -import { PersonTable } from 'src/schema/tables/person.table'; +import { PersonGroupTable } from 'src/schema/tables/person-group.table'; @Table({ name: 'asset_face' }) @UpdatedAtTrigger('asset_face_updatedAt') @@ -26,13 +26,13 @@ import { PersonTable } from 'src/schema/tables/person.table'; when: 'pg_trigger_depth() = 0', }) // schemaFromDatabase does not preserve column order -@Index({ name: 'asset_face_assetId_personId_idx', columns: ['assetId', 'personId'] }) +@Index({ name: 'asset_face_assetId_personGroupId_idx', columns: ['assetId', 'personGroupId'] }) @Index({ - name: 'asset_face_personId_assetId_notDeleted_isVisible_idx', - columns: ['personId', 'assetId'], + name: 'asset_face_personGroupId_assetId_notDeleted_isVisible_idx', + columns: ['personGroupId', 'assetId'], where: '"deletedAt" IS NULL AND "isVisible" IS TRUE', }) -@Index({ columns: ['personId', 'assetId'] }) +@Index({ columns: ['personGroupId', 'assetId'] }) export class AssetFaceTable { @PrimaryGeneratedColumn() id!: Generated; @@ -40,19 +40,19 @@ export class AssetFaceTable { @ForeignKeyColumn(() => AssetTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', - // [assetId, personId] is the PK constraint + // [assetId, personGroupId] is the PK constraint index: false, }) assetId!: string; - @ForeignKeyColumn(() => PersonTable, { + @ForeignKeyColumn(() => PersonGroupTable, { onDelete: 'SET NULL', onUpdate: 'CASCADE', nullable: true, - // [personId, assetId] makes this redundant + // [personGroupId, assetId] makes this redundant index: false, }) - personId!: string | null; + personGroupId!: string | null; @Column({ default: 0, type: 'integer' }) imageWidth!: Generated; diff --git a/server/src/schema/tables/cluster-group-request.table.ts b/server/src/schema/tables/cluster-group-request.table.ts new file mode 100644 index 0000000000000..d85ab06184df4 --- /dev/null +++ b/server/src/schema/tables/cluster-group-request.table.ts @@ -0,0 +1,27 @@ +import { + CreateDateColumn, + ForeignKeyColumn, + Generated, + PrimaryGeneratedColumn, + Table, + Timestamp, + Unique, +} from '@immich/sql-tools'; +import { ClusterGroupTable } from 'src/schema/tables/cluster-group.table'; +import { UserTable } from 'src/schema/tables/user.table'; + +@Table('cluster_group_request') +@Unique({ columns: ['clusterGroupId', 'userId'] }) +export class ClusterGroupRequestTable { + @PrimaryGeneratedColumn() + id!: Generated; + + @ForeignKeyColumn(() => ClusterGroupTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + clusterGroupId!: string; + + @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + userId!: string; + + @CreateDateColumn() + createdAt!: Generated; +} diff --git a/server/src/schema/tables/cluster-group.table.ts b/server/src/schema/tables/cluster-group.table.ts new file mode 100644 index 0000000000000..79c7d91db5de1 --- /dev/null +++ b/server/src/schema/tables/cluster-group.table.ts @@ -0,0 +1,29 @@ +import { + Column, + CreateDateColumn, + Generated, + PrimaryGeneratedColumn, + Table, + Timestamp, + UpdateDateColumn, +} from '@immich/sql-tools'; +import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; + +@Table('cluster_group') +@UpdatedAtTrigger('cluster_group_updatedAt') +export class ClusterGroupTable { + @PrimaryGeneratedColumn() + id!: Generated; + + @Column({ type: 'character varying', nullable: true, default: null }) + name!: string | null; + + @CreateDateColumn() + createdAt!: Generated; + + @UpdateDateColumn() + updatedAt!: Generated; + + @UpdateIdColumn({ index: true }) + updateId!: Generated; +} diff --git a/server/src/schema/tables/person-audit.table.ts b/server/src/schema/tables/person-audit.table.ts index 4fb55f1744639..4045c2917b5a9 100644 --- a/server/src/schema/tables/person-audit.table.ts +++ b/server/src/schema/tables/person-audit.table.ts @@ -7,7 +7,7 @@ export class PersonAuditTable { id!: Generated; @Column({ type: 'uuid', index: true }) - personId!: string; + personGroupId!: string; @Column({ type: 'uuid', index: true }) ownerId!: string; diff --git a/server/src/schema/tables/person-group-audit.table.ts b/server/src/schema/tables/person-group-audit.table.ts new file mode 100644 index 0000000000000..a8a8117d50a73 --- /dev/null +++ b/server/src/schema/tables/person-group-audit.table.ts @@ -0,0 +1,17 @@ +import { Column, CreateDateColumn, Generated, Table, Timestamp } from '@immich/sql-tools'; +import { PrimaryGeneratedUuidV7Column } from 'src/decorators'; + +@Table('person_group_audit') +export class PersonGroupAuditTable { + @PrimaryGeneratedUuidV7Column() + id!: Generated; + + @Column({ type: 'uuid', index: true }) + personGroupId!: string; + + @Column({ type: 'uuid', index: true }) + clusterGroupId!: string; + + @CreateDateColumn({ default: () => 'clock_timestamp()', index: true }) + deletedAt!: Generated; +} diff --git a/server/src/schema/tables/person-group.table.ts b/server/src/schema/tables/person-group.table.ts new file mode 100644 index 0000000000000..36e4c4a949d8c --- /dev/null +++ b/server/src/schema/tables/person-group.table.ts @@ -0,0 +1,41 @@ +import { + AfterDeleteTrigger, + CreateDateColumn, + ForeignKeyColumn, + Generated, + PrimaryGeneratedColumn, + Table, + Timestamp, + UpdateDateColumn, +} from '@immich/sql-tools'; +import { CreateIdColumn, UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; +import { person_group_delete_audit } from 'src/schema/functions'; +import { ClusterGroupTable } from 'src/schema/tables/cluster-group.table'; + +@Table('person_group') +@UpdatedAtTrigger('person_group_updatedAt') +@AfterDeleteTrigger({ + scope: 'statement', + function: person_group_delete_audit, + referencingOldTableAs: 'old', + when: 'pg_trigger_depth() = 0', +}) +export class PersonGroupTable { + @PrimaryGeneratedColumn() + id!: Generated; + + @ForeignKeyColumn(() => ClusterGroupTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + clusterGroupId!: string; + + @CreateDateColumn() + createdAt!: Generated; + + @CreateIdColumn({ index: true }) + createId!: Generated; + + @UpdateDateColumn() + updatedAt!: Generated; + + @UpdateIdColumn({ index: true }) + updateId!: Generated; +} diff --git a/server/src/schema/tables/person.table.ts b/server/src/schema/tables/person.table.ts index 35447acfd08a0..0c9324c928830 100644 --- a/server/src/schema/tables/person.table.ts +++ b/server/src/schema/tables/person.table.ts @@ -6,7 +6,6 @@ import { ForeignKeyColumn, Generated, Index, - PrimaryGeneratedColumn, Table, Timestamp, UpdateDateColumn, @@ -14,6 +13,7 @@ import { import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { person_delete_audit } from 'src/schema/functions'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; +import { PersonGroupTable } from 'src/schema/tables/person-group.table'; import { UserTable } from 'src/schema/tables/user.table'; @Table('person') @@ -27,12 +27,25 @@ import { UserTable } from 'src/schema/tables/user.table'; scope: 'statement', function: person_delete_audit, referencingOldTableAs: 'old', - when: 'pg_trigger_depth() = 0', + when: 'pg_trigger_depth() <= 1', }) @Check({ name: 'person_birthDate_chk', expression: `"birthDate" <= CURRENT_DATE` }) export class PersonTable { - @PrimaryGeneratedColumn('uuid') - id!: Generated; + @ForeignKeyColumn(() => UserTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + primary: true, + // [ownerId, personGroupId] is the PK constraint + index: false, + }) + ownerId!: string; + + @ForeignKeyColumn(() => PersonGroupTable, { + onDelete: 'CASCADE', + onUpdate: 'CASCADE', + primary: true, + }) + personGroupId!: string; @CreateDateColumn() createdAt!: Generated; @@ -40,9 +53,6 @@ export class PersonTable { @UpdateDateColumn() updatedAt!: Generated; - @ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) - ownerId!: string; - @Column({ default: '' }) name!: Generated; diff --git a/server/src/schema/tables/user.table.ts b/server/src/schema/tables/user.table.ts index 50d56d9067fa3..be8b05d129fc8 100644 --- a/server/src/schema/tables/user.table.ts +++ b/server/src/schema/tables/user.table.ts @@ -3,6 +3,7 @@ import { Column, CreateDateColumn, DeleteDateColumn, + ForeignKeyColumn, Generated, Index, PrimaryGeneratedColumn, @@ -14,6 +15,7 @@ import { ColumnType } from 'kysely'; import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators'; import { UserAvatarColor, UserStatus } from 'src/enum'; import { user_delete_audit } from 'src/schema/functions'; +import { ClusterGroupTable } from 'src/schema/tables/cluster-group.table'; @Table('user') @UpdatedAtTrigger('user_updatedAt') @@ -82,4 +84,7 @@ export class UserTable { @UpdateIdColumn({ index: true }) updateId!: Generated; + + @ForeignKeyColumn(() => ClusterGroupTable, { onUpdate: 'CASCADE', nullable: false }) + clusterGroupId!: string; } diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 2b9e1a814c069..d65ebc4d9621a 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -23,8 +23,9 @@ export class ApiKeyService extends BaseService { userId: auth.user.id, permissions: dto.permissions, }); + const apiKey = this.map(entity); - return { secret: token, apiKey: this.map(entity) }; + return { ...apiKey, secret: token, apiKey }; } async update(auth: AuthDto, id: string, dto: ApiKeyUpdateDto): Promise { @@ -58,10 +59,10 @@ export class ApiKeyService extends BaseService { const token = this.cryptoRepository.randomBytesAsText(32); const hashed = this.cryptoRepository.hashSha256(token); - const newKey = await this.apiKeyRepository.update(auth.user.id, id, { key: hashed }); + const apiKey = this.map(newKey); - return { secret: token, apiKey: this.map(newKey) }; + return { ...apiKey, secret: token, apiKey }; } async delete(auth: AuthDto, id: string): Promise { diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 0264484bfcfc0..f5c4304285dac 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -326,6 +326,7 @@ describe(AssetService.name, () => { mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); mocks.asset.getById.mockResolvedValueOnce(getForAsset(asset)); mocks.asset.getById.mockResolvedValueOnce(getForAsset(motionAsset)); + mocks.asset.getById.mockResolvedValueOnce(getForAsset(unlinkedAsset)); mocks.asset.update.mockResolvedValueOnce(getForAsset(unlinkedAsset)); await sut.update(auth, asset.id, { livePhotoVideoId: null }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 15385df0e0c2a..83cc7a653e977 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -65,7 +65,7 @@ export class AssetService extends BaseService { const asset = await this.assetRepository.getById(id, { exifInfo: true, owner: true, - faces: { person: true }, + faces: { person: true, viewingUserId: auth.user.id }, stack: { assets: true }, edits: true, tags: true, @@ -85,7 +85,7 @@ export class AssetService extends BaseService { delete data.owner; } - if (data.ownerId !== auth.user.id || auth.sharedLink) { + if (auth.sharedLink) { data.people = []; } @@ -124,7 +124,7 @@ export class AssetService extends BaseService { throw new BadRequestException('Asset not found'); } - return mapAsset(asset, { auth }); + return this.get(auth, id) as Promise; } async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 6ed8ce925a23c..6c3d5e3df3b1a 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -1,10 +1,10 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { Insertable } from 'kysely'; import sanitize from 'sanitize-filename'; -import { SystemConfig } from 'src/config'; import { SALT_ROUNDS } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { UserAdmin } from 'src/database'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; @@ -14,6 +14,7 @@ import { AppRepository } from 'src/repositories/app.repository'; import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; +import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -74,6 +75,7 @@ export const BASE_SERVICE_DEPENDENCIES = [ AssetRepository, AssetEditRepository, AssetJobRepository, + ClusterGroupRepository, ConfigRepository, CronRepository, CryptoRepository, @@ -134,6 +136,7 @@ export class BaseService { protected assetRepository: AssetRepository, protected assetEditRepository: AssetEditRepository, protected assetJobRepository: AssetJobRepository, + protected clusterGroupRepository: ClusterGroupRepository, protected configRepository: ConfigRepository, protected cronRepository: CronRepository, protected cryptoRepository: CryptoRepository, @@ -203,6 +206,7 @@ export class BaseService { ctx.assetRepository, ctx.assetEditRepository, ctx.assetJobRepository, + ctx.clusterGroupRepository, ctx.configRepository, ctx.cronRepository, ctx.cryptoRepository, @@ -292,7 +296,7 @@ export class BaseService { } } - async createUser(dto: Insertable & { email: string }): Promise { + async createUser(dto: Omit, 'clusterGroupId'> & { email: string }): Promise { const exists = await this.userRepository.getByEmail(dto.email); if (exists) { this.logger.debug('User creation rejected: user already exists'); @@ -306,7 +310,7 @@ export class BaseService { } } - const payload: Insertable = { ...dto }; + const payload: Omit, 'clusterGroupId'> = { ...dto }; if (payload.password) { payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS); } @@ -314,7 +318,8 @@ export class BaseService { payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', '')); } - const user = await this.userRepository.create(payload); + const clusterGroup = await this.clusterGroupRepository.create(); + const user = await this.userRepository.create({ ...payload, clusterGroupId: clusterGroup.id }); await this.eventRepository.emit('UserCreate', user); diff --git a/server/src/services/cluster-group.service.ts b/server/src/services/cluster-group.service.ts new file mode 100644 index 0000000000000..bff5fdf7dddfd --- /dev/null +++ b/server/src/services/cluster-group.service.ts @@ -0,0 +1,90 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { MaybeDuplicate } from 'src/dtos/activity.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { + ClusterGroupRequestCreateDto, + ClusterGroupRequestResponseDto, + mapClusterGroupRequest, +} from 'src/dtos/cluster-group.dto'; +import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; +import { Permission } from 'src/enum'; +import { BaseService } from 'src/services/base.service'; +import { findOrFail } from 'src/utils/misc'; + +@Injectable() +export class ClusterGroupService extends BaseService { + async getRequests(auth: AuthDto): Promise { + const requests = await this.clusterGroupRepository.searchRequests({ userId: auth.user.id }); + return requests.map((request) => mapClusterGroupRequest(request)); + } + + async getRequestsForGroup(auth: AuthDto, clusterGroupId: string): Promise { + await this.requireAccess({ auth, permission: Permission.ClusterGroupRead, ids: [clusterGroupId] }); + + const requests = await this.clusterGroupRepository.searchRequests({ clusterGroupId }); + return requests.map((request) => mapClusterGroupRequest(request)); + } + + async getUsers(auth: AuthDto, clusterGroupId: string): Promise { + await this.requireAccess({ auth, permission: Permission.ClusterGroupRead, ids: [clusterGroupId] }); + + const users = await this.clusterGroupRepository.getUsers({ clusterGroupId, userId: auth.user.id }); + return users.map((user) => mapUser(user)); + } + + async createRequest( + auth: AuthDto, + clusterGroupId: string, + { userId }: ClusterGroupRequestCreateDto, + ): Promise> { + await this.requireAccess({ auth, permission: Permission.ClusterGroupRequestCreate, ids: [clusterGroupId] }); + + if (userId === auth.user.id) { + throw new BadRequestException('Cannot request to join your own cluster group'); + } + + await findOrFail(() => this.userRepository.get(userId, {}), 'User'); + + const request = await findOrFail( + () => this.clusterGroupRepository.createRequest({ clusterGroupId, userId }), + 'Request', + ); + + if (request.isInserted) { + await this.eventRepository.emit('ClusterGroupRequest', { clusterGroupId, userId, senderName: auth.user.name }); + } + + return { duplicate: !request.isInserted, value: mapClusterGroupRequest(request) }; + } + + async acceptRequest(auth: AuthDto, id: string): Promise { + await this.requireAccess({ auth, permission: Permission.ClusterGroupRequestRead, ids: [id] }); + + const request = await findOrFail(() => this.clusterGroupRepository.getRequest(id), 'Request'); + + await this.personRepository.reassignCluster({ userId: auth.user.id, newClusterId: request.clusterGroupId }); + await this.userRepository.update(auth.user.id, { clusterGroupId: request.clusterGroupId }); + await this.clusterGroupRepository.deleteRequest(request.id); + } + + async deleteRequest(auth: AuthDto, id: string): Promise { + await this.requireAccess({ auth, permission: Permission.ClusterGroupRequestDelete, ids: [id] }); + await this.clusterGroupRepository.deleteRequest(id); + } + + async leave(auth: AuthDto, clusterGroupId: string): Promise { + await this.requireAccess({ auth, permission: Permission.ClusterGroupLeave, ids: [clusterGroupId] }); + + const hasOtherMembers = await this.clusterGroupRepository.hasOtherMembers({ + clusterGroupId, + userId: auth.user.id, + }); + if (!hasOtherMembers) { + throw new BadRequestException('Cannot leave a cluster group without any other members'); + } + + const clusterGroup = await this.clusterGroupRepository.create(); + await this.personRepository.reassignCluster({ userId: auth.user.id, newClusterId: clusterGroup.id }); + await this.userRepository.update(auth.user.id, { clusterGroupId: clusterGroup.id }); + } +} diff --git a/server/src/services/database-backup.service.spec.ts b/server/src/services/database-backup.service.spec.ts index 5026386252309..ac808af8ae34a 100644 --- a/server/src/services/database-backup.service.spec.ts +++ b/server/src/services/database-backup.service.spec.ts @@ -1,8 +1,8 @@ import { BadRequestException } from '@nestjs/common'; import { DateTime } from 'luxon'; import { PassThrough, Readable } from 'node:stream'; -import { defaults, SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { ImmichWorker, JobStatus, StorageFolder } from 'src/enum'; import { MaintenanceHealthRepository } from 'src/maintenance/maintenance-health.repository'; import { DatabaseBackupService } from 'src/services/database-backup.service'; diff --git a/server/src/services/hls.service.ts b/server/src/services/hls.service.ts index 94a8a46acea84..d2f7133c0da46 100644 --- a/server/src/services/hls.service.ts +++ b/server/src/services/hls.service.ts @@ -5,7 +5,7 @@ import { HLS_SEGMENT_DURATION, HLS_SEGMENT_FILENAME_REGEX, HLS_VARIANTS, HLS_VER import { StorageCore } from 'src/cores/storage.core'; import { OnEvent } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; -import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { ConfigFFmpegDto } from 'src/dtos/config.dto'; import { CacheControl, ImmichWorker, Permission } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; @@ -124,7 +124,7 @@ export class HlsService extends BaseService { this.websocketRepository.serverSend('HlsSessionEnd', { sessionId }); } - private generateMainPlaylist(sessionId: string, ffmpeg: SystemConfigFFmpegDto, asset: AssetWithStreamInfo) { + private generateMainPlaylist(sessionId: string, ffmpeg: ConfigFFmpegDto, asset: AssetWithStreamInfo) { const fps = (asset.packets.packetCount * asset.videoStream.timeBase) / asset.packets.totalDuration; const roundedFps = fps.toFixed(3); const sourceResolution = Math.min(asset.videoStream.height, asset.videoStream.width); diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 766b5979bc707..d8bb618b01832 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -7,6 +7,7 @@ import { AssetService } from 'src/services/asset.service'; import { AuthAdminService } from 'src/services/auth-admin.service'; import { AuthService } from 'src/services/auth.service'; import { CliService } from 'src/services/cli.service'; +import { ClusterGroupService } from 'src/services/cluster-group.service'; import { DatabaseBackupService } from 'src/services/database-backup.service'; import { DatabaseService } from 'src/services/database.service'; import { DownloadService } from 'src/services/download.service'; @@ -76,6 +77,7 @@ export const services = [ NotificationService, NotificationAdminService, OcrService, + ClusterGroupService, PartnerService, PersonService, PluginService, diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index a464c9e174753..7a7ebb8e023c9 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -50,7 +50,7 @@ describe(JobService.name, () => { jobs: [], }, { - item: { name: JobName.PersonGenerateThumbnail, data: { id: 'asset-1' } }, + item: { name: JobName.PersonGenerateThumbnail, data: { ownerId: 'owner-1', personGroupId: 'person-group-1' } }, jobs: [], }, { @@ -90,6 +90,7 @@ describe(JobService.name, () => { for (const { item, jobs, stub } of tests) { it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => { if (stub) { + mocks.asset.getById.mockResolvedValue(stub[0]); mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue(stub); } diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 6d4f04df370ea..3a9f7e76cb85e 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -124,11 +124,8 @@ export class JobService extends BaseService { } case JobName.PersonGenerateThumbnail: { - const { id } = item.data; - const person = await this.personRepository.getById(id); - if (person) { - this.websocketRepository.clientSend('on_person_thumbnail', person.ownerId, person.id); - } + const { ownerId, personGroupId } = item.data; + this.websocketRepository.clientSend('on_person_thumbnail', ownerId, personGroupId); break; } diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index ae06232605521..575de87598d93 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { Stats } from 'node:fs'; -import { defaults, SystemConfig } from 'src/config'; import { JOBS_LIBRARY_PAGINATION_SIZE } from 'src/constants'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { mapLibrary } from 'src/dtos/library.dto'; import { AssetType, CronJob, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { LibraryService } from 'src/services/library.service'; diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index d64e0088ebd8f..48895c4763ce1 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,7 +1,7 @@ import { ShallowDehydrateObject } from 'kysely'; import { OutputInfo } from 'sharp'; -import { SystemConfig } from 'src/config'; import { Exif } from 'src/database'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AssetEditAction } from 'src/dtos/editing.dto'; import { AssetFileType, @@ -72,7 +72,7 @@ describe(MediaService.name, () => { expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: person.id }, + data: { ownerId: person.ownerId, personGroupId: person.personGroupId }, }, ]); }); @@ -129,7 +129,8 @@ describe(MediaService.name, () => { { name: JobName.PersonGenerateThumbnail, data: { - id: person1.id, + ownerId: person1.ownerId, + personGroupId: person1.personGroupId, }, }, ]); @@ -297,7 +298,12 @@ describe(MediaService.name, () => { expect(mocks.storage.removeEmptyDirs).toHaveBeenCalledTimes(2); expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.AssetFileMigration, data: { id: asset.id } }]); - expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.PersonFileMigration, data: { id: person.id } }]); + expect(mocks.job.queueAll).toHaveBeenCalledWith([ + { + name: JobName.PersonFileMigration, + data: { ownerId: person.ownerId, personGroupId: person.personGroupId }, + }, + ]); }); }); @@ -1518,24 +1524,25 @@ describe(MediaService.name, () => { info: { width: 1000, height: 1000 } as OutputInfo, }); - await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: 'owner-1', personGroupId: 'person-group-1' }), + ).resolves.toBe(JobStatus.Success); expect(mocks.media.generateThumbnail).toHaveBeenCalled(); }); it('should skip a person not found', async () => { - await sut.handleGeneratePersonThumbnail({ id: 'person-1' }); + await sut.handleGeneratePersonThumbnail({ ownerId: 'owner-1', personGroupId: 'person-group-1' }); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); }); it('should skip a person without a face asset id', async () => { const person = PersonFactory.create({ faceAssetId: null }); - mocks.person.getById.mockResolvedValue(person); - await sut.handleGeneratePersonThumbnail({ id: person.id }); + await sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); }); it('should skip a person with face not found', async () => { - await sut.handleGeneratePersonThumbnail({ id: 'person-1' }); + await sut.handleGeneratePersonThumbnail({ ownerId: 'owner-1', personGroupId: 'person-group-1' }); expect(mocks.media.generateThumbnail).not.toHaveBeenCalled(); }); @@ -1548,9 +1555,14 @@ describe(MediaService.name, () => { const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); - expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(person.id); + expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailMiddle.originalPath, { colorspace: Colorspace.P3, @@ -1581,7 +1593,11 @@ describe(MediaService.name, () => { }, expect.any(String), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, thumbnailPath: expect.any(String) }); + expect(mocks.person.update).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + thumbnailPath: expect.any(String), + }); }); it('should use preview path if video', async () => { @@ -1593,9 +1609,14 @@ describe(MediaService.name, () => { const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); - expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith(person.id); + expect(mocks.person.getDataForThumbnailGenerationJob).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + }); expect(mocks.storage.mkdirSync).toHaveBeenCalledWith(expect.any(String)); expect(mocks.media.decodeImage).toHaveBeenCalledWith(expect.any(String), { colorspace: Colorspace.P3, @@ -1626,7 +1647,11 @@ describe(MediaService.name, () => { }, expect.any(String), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, thumbnailPath: expect.any(String) }); + expect(mocks.person.update).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + thumbnailPath: expect.any(String), + }); }); it('should generate a thumbnail without going negative', async () => { @@ -1638,7 +1663,9 @@ describe(MediaService.name, () => { const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailStart.originalPath, { colorspace: Colorspace.P3, @@ -1681,7 +1708,9 @@ describe(MediaService.name, () => { const info = { width: 1000, height: 1000 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.newThumbnailEnd.originalPath, { colorspace: Colorspace.P3, @@ -1724,7 +1753,9 @@ describe(MediaService.name, () => { const info = { width: 4624, height: 3080 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.negativeCoordinate.originalPath, { colorspace: Colorspace.P3, @@ -1767,7 +1798,9 @@ describe(MediaService.name, () => { const info = { width: 4624, height: 3080 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.overflowingCoordinate.originalPath, { colorspace: Colorspace.P3, @@ -1814,7 +1847,9 @@ describe(MediaService.name, () => { mocks.media.decodeImage.mockResolvedValue({ data, info }); mocks.media.getImageMetadata.mockResolvedValue({ width: 2160, height: 3840, isTransparent: false }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); expect(mocks.media.decodeImage).toHaveBeenCalledWith(extracted, { @@ -1857,7 +1892,9 @@ describe(MediaService.name, () => { const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).not.toHaveBeenCalled(); expect(mocks.media.generateThumbnail).toHaveBeenCalled(); @@ -1873,7 +1910,9 @@ describe(MediaService.name, () => { const info = { width: 2160, height: 3840 } as OutputInfo; mocks.media.decodeImage.mockResolvedValue({ data, info }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath, { @@ -1897,7 +1936,9 @@ describe(MediaService.name, () => { mocks.media.extract.mockResolvedValue({ buffer: extracted, format: RawExtractedFormat.Jpeg }); mocks.media.getImageMetadata.mockResolvedValue({ width: 1000, height: 1000, isTransparent: false }); - await expect(sut.handleGeneratePersonThumbnail({ id: person.id })).resolves.toBe(JobStatus.Success); + await expect( + sut.handleGeneratePersonThumbnail({ ownerId: person.ownerId, personGroupId: person.personGroupId }), + ).resolves.toBe(JobStatus.Success); expect(mocks.media.extract).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath); expect(mocks.media.decodeImage).toHaveBeenCalledWith(personThumbnailStub.rawEmbeddedThumbnail.originalPath, { diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 19b1688e881a3..9fda932bc1a3a 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,11 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { SystemConfig } from 'src/config'; import { FACE_THUMBNAIL_SIZE } from 'src/constants'; import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core'; import { AssetFile } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; +import { ConfigFFmpegDto, SystemConfig } from 'src/dtos/config.dto'; import { AssetEditAction, CropParameters } from 'src/dtos/editing.dto'; -import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetFileType, AssetType, @@ -91,16 +90,17 @@ export class MediaService extends BaseService { for await (const people of batched(this.personRepository.getAll(force ? undefined : { thumbnailPath: '' }))) { const jobs: JobItem[] = []; for (const person of people) { + const { ownerId, personGroupId } = person; if (!person.faceAssetId) { - const face = await this.personRepository.getRandomFace(person.id); + const face = await this.personRepository.getRandomFace(personGroupId); if (!face) { continue; } - await this.personRepository.update({ id: person.id, faceAssetId: face.id }); + await this.personRepository.update({ ownerId, personGroupId, faceAssetId: face.id }); } - jobs.push({ name: JobName.PersonGenerateThumbnail, data: { id: person.id } }); + jobs.push({ name: JobName.PersonGenerateThumbnail, data: { ownerId, personGroupId } }); } await this.jobRepository.queueAll(jobs); @@ -125,7 +125,10 @@ export class MediaService extends BaseService { for await (const people of batched(this.personRepository.getAll())) { await this.jobRepository.queueAll( - people.map((person) => ({ name: JobName.PersonFileMigration, data: { id: person.id } })), + people.map(({ ownerId, personGroupId }) => ({ + name: JobName.PersonFileMigration, + data: { ownerId, personGroupId }, + })), ); } @@ -387,19 +390,22 @@ export class MediaService extends BaseService { } @OnJob({ name: JobName.PersonGenerateThumbnail, queue: QueueName.ThumbnailGeneration }) - async handleGeneratePersonThumbnail({ id }: JobOf): Promise { + async handleGeneratePersonThumbnail({ + ownerId, + personGroupId, + }: JobOf): Promise { const { image } = await this.getConfig({ withCache: true }); - const data = await this.personRepository.getDataForThumbnailGenerationJob(id); + const data = await this.personRepository.getDataForThumbnailGenerationJob({ ownerId, personGroupId }); if (!data) { - this.logger.error(`Could not generate person thumbnail for ${id}: missing data`); + this.logger.error(`Could not generate person thumbnail for ${personGroupId}: missing data`); return JobStatus.Failed; } - const { ownerId, x1, y1, x2, y2, oldWidth, oldHeight, exifOrientation, previewPath, originalPath } = data; + const { x1, y1, x2, y2, oldWidth, oldHeight, exifOrientation, previewPath, originalPath } = data; let inputImage: string | Buffer; if (data.type === AssetType.Video) { if (!previewPath) { - this.logger.error(`Could not generate person thumbnail for video ${id}: missing preview path`); + this.logger.error(`Could not generate person thumbnail for video ${personGroupId}: missing preview path`); return JobStatus.Failed; } inputImage = previewPath; @@ -417,7 +423,7 @@ export class MediaService extends BaseService { orientation: Buffer.isBuffer(inputImage) && exifOrientation ? Number(exifOrientation) : undefined, }); - const thumbnailPath = StorageCore.getPersonThumbnailPath({ id, ownerId }); + const thumbnailPath = StorageCore.getPersonThumbnailPath({ ownerId, personGroupId }); this.storageCore.ensureFolders(thumbnailPath); const thumbnailOptions: GenerateThumbnailOptions = { @@ -440,7 +446,7 @@ export class MediaService extends BaseService { }; await this.mediaRepository.generateThumbnail(decodedImage, thumbnailOptions, thumbnailPath); - await this.personRepository.update({ id, thumbnailPath }); + await this.personRepository.update({ ownerId, personGroupId, thumbnailPath }); return JobStatus.Success; } @@ -626,7 +632,7 @@ export class MediaService extends BaseService { } private getTranscodeTarget( - config: SystemConfigFFmpegDto, + config: ConfigFFmpegDto, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo, ): TranscodeTarget { @@ -648,7 +654,7 @@ export class MediaService extends BaseService { return TranscodeTarget.None; } - private isAudioTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream?: AudioStreamInfo): boolean { + private isAudioTranscodeRequired(ffmpegConfig: ConfigFFmpegDto, stream?: AudioStreamInfo): boolean { if (!stream) { return false; } @@ -671,7 +677,7 @@ export class MediaService extends BaseService { } } - private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: VideoStreamInfo): boolean { + private isVideoTranscodeRequired(ffmpegConfig: ConfigFFmpegDto, stream: VideoStreamInfo): boolean { const isScalingEnabled = ffmpegConfig.targetResolution !== 'original'; const targetRes = Number.parseInt(ffmpegConfig.targetResolution); const isLargerThanTargetRes = isScalingEnabled && Math.min(stream.height, stream.width) > targetRes; @@ -703,7 +709,7 @@ export class MediaService extends BaseService { } } - private isRemuxRequired(ffmpegConfig: SystemConfigFFmpegDto, { formatName, formatLongName }: VideoFormat): boolean { + private isRemuxRequired(ffmpegConfig: ConfigFFmpegDto, { formatName, formatLongName }: VideoFormat): boolean { if (ffmpegConfig.transcode === TranscodePolicy.Disabled) { return false; } @@ -828,7 +834,7 @@ export class MediaService extends BaseService { : undefined; const originalDimensions = getDimensions(asset.exifInfo!); - const assetFaces = await this.personRepository.getFaces(asset.id, {}); + const assetFaces = await this.personRepository.getFaces(asset.id, { viewingUserId: asset.ownerId }); const ocrData = await this.ocrRepository.getByAssetId(asset.id, {}); const faceStatuses = checkFaceVisibility(assetFaces, originalDimensions, cropBox); diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 57c029961e4aa..5f1036567c59e 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -2,7 +2,7 @@ import { BinaryField, ExifDateTime } from 'exiftool-vendored'; import { DateTime } from 'luxon'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; -import { defaults } from 'src/config'; +import { defaults } from 'src/dtos/config.dto'; import { AssetFileType, AssetType, @@ -17,6 +17,7 @@ import { import { ImmichTags } from 'src/repositories/metadata.repository'; import { firstDateTime, MetadataService } from 'src/services/metadata.service'; import { AssetFactory } from 'test/factories/asset.factory'; +import { PersonGroupFactory } from 'test/factories/person-group.factory'; import { PersonFactory } from 'test/factories/person.factory'; import { videoInfoStub } from 'test/fixtures/media.stub'; import { tagStub } from 'test/fixtures/tag.stub'; @@ -1388,7 +1389,8 @@ describe(MetadataService.name, () => { mockReadTags(faceTags); mocks.person.getDistinctNames.mockResolvedValue([]); - mocks.person.createAll.mockResolvedValue([person.id]); + mocks.person.createGroups.mockResolvedValue([PersonGroupFactory.create({ id: person.personGroupId })]); + mocks.person.createAll.mockResolvedValue([person]); mocks.person.update.mockResolvedValue(person); await sut.handleMetadataExtraction({ id: asset.id }); @@ -1411,7 +1413,8 @@ describe(MetadataService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags({ Name: person.name })); mocks.person.getDistinctNames.mockResolvedValue([]); - mocks.person.createAll.mockResolvedValue([person.id]); + mocks.person.createGroups.mockResolvedValue([PersonGroupFactory.create({ id: person.personGroupId })]); + mocks.person.createAll.mockResolvedValue([person]); mocks.person.update.mockResolvedValue(person); await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); @@ -1422,7 +1425,7 @@ describe(MetadataService.name, () => { { id: 'random-uuid', assetId: asset.id, - personId: 'random-uuid', + personGroupId: 'random-uuid', imageHeight: 100, imageWidth: 1000, boundingBoxX1: 0, @@ -1435,12 +1438,12 @@ describe(MetadataService.name, () => { [], ); expect(mocks.person.updateAll).toHaveBeenCalledWith([ - { id: 'random-uuid', ownerId: asset.ownerId, faceAssetId: 'random-uuid' }, + { ownerId: asset.ownerId, personGroupId: 'random-uuid', faceAssetId: 'random-uuid' }, ]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: person.id }, + data: { ownerId: asset.ownerId, personGroupId: 'random-uuid' }, }, ]); }); @@ -1452,7 +1455,8 @@ describe(MetadataService.name, () => { mocks.assetJob.getForMetadataExtraction.mockResolvedValue(getForMetadataExtraction(asset)); mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags({ Name: person.name })); - mocks.person.getDistinctNames.mockResolvedValue([{ id: person.id, name: person.name }]); + mocks.person.getDistinctNames.mockResolvedValue([{ personGroupId: person.personGroupId, name: person.name }]); + mocks.person.createGroups.mockResolvedValue([]); mocks.person.createAll.mockResolvedValue([]); mocks.person.update.mockResolvedValue(person); await sut.handleMetadataExtraction({ id: asset.id }); @@ -1464,7 +1468,7 @@ describe(MetadataService.name, () => { { id: 'random-uuid', assetId: asset.id, - personId: person.id, + personGroupId: person.personGroupId, imageHeight: 100, imageWidth: 1000, boundingBoxX1: 0, @@ -1540,7 +1544,8 @@ describe(MetadataService.name, () => { mocks.systemMetadata.get.mockResolvedValue({ metadata: { faces: { import: true } } }); mockReadTags(makeFaceTags({ Name: person.name }, orientation)); mocks.person.getDistinctNames.mockResolvedValue([]); - mocks.person.createAll.mockResolvedValue([person.id]); + mocks.person.createGroups.mockResolvedValue([PersonGroupFactory.create({ id: person.personGroupId })]); + mocks.person.createAll.mockResolvedValue([person]); mocks.person.update.mockResolvedValue(person); await sut.handleMetadataExtraction({ id: asset.id }); expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id); @@ -1553,7 +1558,7 @@ describe(MetadataService.name, () => { { id: 'random-uuid', assetId: asset.id, - personId: 'random-uuid', + personGroupId: 'random-uuid', imageWidth: imgW, imageHeight: imgH, boundingBoxX1: x1, @@ -1566,12 +1571,12 @@ describe(MetadataService.name, () => { [], ); expect(mocks.person.updateAll).toHaveBeenCalledWith([ - { id: 'random-uuid', ownerId: asset.ownerId, faceAssetId: 'random-uuid' }, + { ownerId: asset.ownerId, personGroupId: 'random-uuid', faceAssetId: 'random-uuid' }, ]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: person.id }, + data: { ownerId: asset.ownerId, personGroupId: 'random-uuid' }, }, ]); }, diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index a95d1f1497e9d..c6abad7cbc3b2 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -893,7 +893,13 @@ export class MetadataService extends BaseService { } private async applyTaggedFaces( - asset: { id: string; ownerId: string; faces: { id: string; sourceType: SourceType }[]; originalPath: string }, + asset: { + id: string; + ownerId: string; + clusterGroupId: string; + faces: { id: string; sourceType: SourceType }[]; + originalPath: string; + }, tags: ImmichTags, ) { if (!tags.RegionInfo?.AppliedToDimensions || tags.RegionInfo.RegionList.length === 0) { @@ -902,9 +908,11 @@ export class MetadataService extends BaseService { const facesToAdd: (Insertable & { assetId: string })[] = []; const existingNames = await this.personRepository.getDistinctNames(asset.ownerId, { withHidden: true }); - const existingNameMap = new Map(existingNames.map(({ id, name }) => [name.toLowerCase(), id])); - const missing: (Insertable & { ownerId: string })[] = []; - const missingWithFaceAsset: { id: string; ownerId: string; faceAssetId: string }[] = []; + const existingNameMap = new Map( + existingNames.map(({ personGroupId, name }) => [name.toLowerCase(), personGroupId]), + ); + const missing: (Insertable & { name: string; personGroupId: string; clusterGroupId: string })[] = []; + const missingWithFaceAsset: { personGroupId: string; ownerId: string; faceAssetId: string }[] = []; const adjustedRegionInfo = this.orientRegionInfo(tags.RegionInfo, tags.Orientation); const imageWidth = adjustedRegionInfo.AppliedToDimensions.W; @@ -916,7 +924,7 @@ export class MetadataService extends BaseService { } const loweredName = region.Name.toLowerCase(); - const personId = existingNameMap.get(loweredName) || this.cryptoRepository.randomUUID(); + const personGroupId = existingNameMap.get(loweredName) || this.cryptoRepository.randomUUID(); const X = Number(region.Area.X); const Y = Number(region.Area.Y); @@ -925,7 +933,7 @@ export class MetadataService extends BaseService { const face = { id: this.cryptoRepository.randomUUID(), - personId, + personGroupId, assetId: asset.id, imageWidth, imageHeight, @@ -938,15 +946,27 @@ export class MetadataService extends BaseService { facesToAdd.push(face); if (!existingNameMap.has(loweredName)) { - missing.push({ id: personId, ownerId: asset.ownerId, name: region.Name }); - missingWithFaceAsset.push({ id: personId, ownerId: asset.ownerId, faceAssetId: face.id }); + missing.push({ + personGroupId, + ownerId: asset.ownerId, + clusterGroupId: asset.clusterGroupId, + name: region.Name, + }); + missingWithFaceAsset.push({ personGroupId, ownerId: asset.ownerId, faceAssetId: face.id }); } } if (missing.length > 0) { - this.logger.debugFn(() => `Creating missing persons: ${missing.map((p) => `${p.name}/${p.id}`)}`); - const newPersonIds = await this.personRepository.createAll(missing); - const jobs = newPersonIds.map((id) => ({ name: JobName.PersonGenerateThumbnail, data: { id } }) as const); + this.logger.debugFn(() => `Creating missing persons: ${missing.map((p) => `${p.name}/${p.personGroupId}`)}`); + await this.personRepository.createGroups( + missing.map((item) => ({ id: item.personGroupId, clusterGroupId: asset.clusterGroupId })), + ); + await this.personRepository.createAll(missing); + + const jobs = missing.map( + ({ personGroupId, ownerId }) => + ({ name: JobName.PersonGenerateThumbnail, data: { personGroupId, ownerId } }) as const, + ); await this.jobRepository.queueAll(jobs); } diff --git a/server/src/services/notification-admin.service.spec.ts b/server/src/services/notification-admin.service.spec.ts index c2008977194b6..12fb3e71fb2ce 100644 --- a/server/src/services/notification-admin.service.spec.ts +++ b/server/src/services/notification-admin.service.spec.ts @@ -1,4 +1,4 @@ -import { defaults, SystemConfig } from 'src/config'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { EmailTemplate } from 'src/repositories/email.repository'; import { NotificationService } from 'src/services/notification.service'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/src/services/notification-admin.service.ts b/server/src/services/notification-admin.service.ts index 2fc4584dcadad..63c1da0d2ef24 100644 --- a/server/src/services/notification-admin.service.ts +++ b/server/src/services/notification-admin.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfigSmtpDto } from 'src/dtos/config.dto'; import { mapNotification, NotificationCreateDto } from 'src/dtos/notification.dto'; -import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { NotificationLevel, NotificationType } from 'src/enum'; import { EmailTemplate } from 'src/repositories/email.repository'; import { BaseService } from 'src/services/base.service'; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index d6fcefbe6a65c..47b9b7257ab09 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,5 +1,4 @@ -import { defaults, SystemConfig } from 'src/config'; -import { SystemConfigDto } from 'src/dtos/system-config.dto'; +import { AdminConfigDto, defaults, SystemConfig } from 'src/dtos/config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; import { NotificationService } from 'src/services/notification.service'; import { AlbumFactory } from 'test/factories/album.factory'; @@ -100,7 +99,7 @@ describe(NotificationService.name, () => { it('skips smtp validation with DTO when there are no changes', async () => { const oldConfig = { ...configs.smtpEnabled }; - const newConfig = configs.smtpEnabled as SystemConfigDto; + const newConfig = configs.smtpEnabled as AdminConfigDto; await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index a650b466baab1..6fcbd81915dae 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -3,6 +3,7 @@ import { OnEvent, OnJob } from 'src/decorators'; import { MapAlbumDto } from 'src/dtos/album.dto'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfigSmtpDto } from 'src/dtos/config.dto'; import { mapNotification, NotificationDeleteAllDto, @@ -11,7 +12,6 @@ import { NotificationUpdateAllDto, NotificationUpdateDto, } from 'src/dtos/notification.dto'; -import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, @@ -169,7 +169,7 @@ export class NotificationService extends BaseService { return; } - const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([assetId]); + const [asset] = await this.assetRepository.getByIdsWithAllRelationsButStacks([assetId], userId); if (asset) { this.websocketRepository.clientSend( 'on_asset_update', @@ -236,6 +236,20 @@ export class NotificationService extends BaseService { await this.jobRepository.queue({ name: JobName.NotifyAlbumInvite, data: { id, recipientId: userId, senderName } }); } + @OnEvent({ name: 'ClusterGroupRequest' }) + async onClusterGroupRequest({ clusterGroupId, userId, senderName }: ArgOf<'ClusterGroupRequest'>) { + const item = await this.notificationRepository.create({ + userId, + type: NotificationType.ClusterGroupRequest, + level: NotificationLevel.Info, + title: 'Cluster Group Request', + description: `${senderName} asked you to join their cluster group`, + data: JSON.stringify({ clusterGroupId }), + }); + + this.websocketRepository.clientSend('on_notification', userId, mapNotification(item)); + } + @OnEvent({ name: 'SessionDelete' }) onSessionDelete({ sessionId }: ArgOf<'SessionDelete'>) { // after the response is sent diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index e6a11786af02d..2f56f04548efb 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -2,12 +2,12 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { mapFaces, mapPerson } from 'src/dtos/person.dto'; import { AssetFileType, CacheControl, JobName, JobStatus, SourceType, SystemMetadataKey } from 'src/enum'; -import { FaceSearchResult } from 'src/repositories/search.repository'; import { PersonService } from 'src/services/person.service'; import { ImmichFileResponse } from 'src/utils/file'; import { AssetFaceFactory } from 'test/factories/asset-face.factory'; import { AssetFactory } from 'test/factories/asset.factory'; import { AuthFactory } from 'test/factories/auth.factory'; +import { PersonGroupFactory } from 'test/factories/person-group.factory'; import { PersonFactory } from 'test/factories/person.factory'; import { UserFactory } from 'test/factories/user.factory'; import { authStub } from 'test/fixtures/auth.stub'; @@ -17,6 +17,7 @@ import { getForAsset, getForAssetFace, getForDetectedFaces, + getForFaceSearch, getForFacialRecognitionJob, } from 'test/mappers'; import { newDate, newUuid } from 'test/small.factory'; @@ -49,9 +50,9 @@ describe(PersonService.name, () => { total: 2, hidden: 1, people: [ - expect.objectContaining({ id: person.id, isHidden: false }), + expect.objectContaining({ id: person.personGroupId, isHidden: false }), expect.objectContaining({ - id: hiddenPerson.id, + id: hiddenPerson.personGroupId, isHidden: true, }), ], @@ -76,10 +77,10 @@ describe(PersonService.name, () => { hidden: 1, people: [ expect.objectContaining({ - id: isFavorite.id, + id: isFavorite.personGroupId, isFavorite: true, }), - expect.objectContaining({ id: person.id, isFavorite: false }), + expect.objectContaining({ id: person.personGroupId, isFavorite: false }), ], }); expect(mocks.person.getAllForUser).toHaveBeenCalledWith({ skip: 0, take: 10 }, auth.user.id, { @@ -92,9 +93,9 @@ describe(PersonService.name, () => { it('should require person.read permission', async () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); - mocks.person.getById.mockResolvedValue(person); - await expect(sut.getById(auth, person.id)).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + mocks.person.getByGroupId.mockResolvedValue(person); + await expect(sut.getById(auth, person.personGroupId)).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it('should throw a bad request when person is not found', async () => { @@ -108,11 +109,16 @@ describe(PersonService.name, () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); - mocks.person.getById.mockResolvedValue(person); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - await expect(sut.getById(auth, person.id)).resolves.toEqual(expect.objectContaining({ id: person.id })); - expect(mocks.person.getById).toHaveBeenCalledWith(person.id); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + mocks.person.getByGroupId.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); + await expect(sut.getById(auth, person.personGroupId)).resolves.toEqual( + expect.objectContaining({ id: person.personGroupId }), + ); + expect(mocks.person.getByGroupId).toHaveBeenCalledWith({ + ownerId: auth.user.id, + personGroupId: person.personGroupId, + }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); }); @@ -121,10 +127,10 @@ describe(PersonService.name, () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); - mocks.person.getById.mockResolvedValue(person); - await expect(sut.getThumbnail(auth, person.id)).rejects.toBeInstanceOf(BadRequestException); + mocks.person.getByGroupId.mockResolvedValue(person); + await expect(sut.getThumbnail(auth, person.personGroupId)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.storage.createReadStream).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it('should throw an error when personId is invalid', async () => { @@ -140,27 +146,27 @@ describe(PersonService.name, () => { const auth = AuthFactory.create(); const person = PersonFactory.create({ thumbnailPath: '' }); - mocks.person.getById.mockResolvedValue(person); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - await expect(sut.getThumbnail(auth, person.id)).rejects.toBeInstanceOf(NotFoundException); + mocks.person.getByGroupId.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); + await expect(sut.getThumbnail(auth, person.personGroupId)).rejects.toBeInstanceOf(NotFoundException); expect(mocks.storage.createReadStream).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it('should serve the thumbnail', async () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); - mocks.person.getById.mockResolvedValue(person); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - await expect(sut.getThumbnail(auth, person.id)).resolves.toEqual( + mocks.person.getByGroupId.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); + await expect(sut.getThumbnail(auth, person.personGroupId)).resolves.toEqual( new ImmichFileResponse({ path: person.thumbnailPath, contentType: 'image/jpeg', cacheControl: CacheControl.PrivateWithoutCache, }), ); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); }); @@ -169,10 +175,12 @@ describe(PersonService.name, () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); - mocks.person.getById.mockResolvedValue(person); - await expect(sut.update(auth, person.id, { name: 'Person 1' })).rejects.toBeInstanceOf(BadRequestException); + mocks.person.getByGroupId.mockResolvedValue(person); + await expect(sut.update(auth, person.personGroupId, { name: 'Person 1' })).rejects.toBeInstanceOf( + BadRequestException, + ); expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it('should throw an error when personId is invalid', async () => { @@ -188,26 +196,32 @@ describe(PersonService.name, () => { const auth = AuthFactory.create(); const person = PersonFactory.create({ name: 'Person 1' }); + mocks.person.getByGroupId.mockResolvedValue(person); mocks.person.update.mockResolvedValue(person); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); - await expect(sut.update(auth, person.id, { name: 'Person 1' })).resolves.toEqual( - expect.objectContaining({ id: person.id, name: 'Person 1' }), + await expect(sut.update(auth, person.personGroupId, { name: 'Person 1' })).resolves.toEqual( + expect.objectContaining({ id: person.personGroupId, name: 'Person 1' }), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, name: 'Person 1' }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.person.update).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + name: 'Person 1', + }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it("should update a person's date of birth", async () => { const auth = AuthFactory.create(); const person = PersonFactory.create({ birthDate: new Date('1976-06-30') }); + mocks.person.getByGroupId.mockResolvedValue(person); mocks.person.update.mockResolvedValue(person); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); - await expect(sut.update(auth, person.id, { birthDate: '1976-06-30' })).resolves.toEqual({ - id: person.id, + await expect(sut.update(auth, person.personGroupId, { birthDate: '1976-06-30' })).resolves.toEqual({ + id: person.personGroupId, name: person.name, birthDate: '1976-06-30', thumbnailPath: person.thumbnailPath, @@ -215,40 +229,54 @@ describe(PersonService.name, () => { isFavorite: false, updatedAt: expect.any(String), }); - expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, birthDate: '1976-06-30' }); + expect(mocks.person.update).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + birthDate: '1976-06-30', + }); expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it('should update a person visibility', async () => { const auth = AuthFactory.create(); const person = PersonFactory.create({ isHidden: true }); + mocks.person.getByGroupId.mockResolvedValue(person); mocks.person.update.mockResolvedValue(person); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); - await expect(sut.update(auth, person.id, { isHidden: true })).resolves.toEqual( + await expect(sut.update(auth, person.personGroupId, { isHidden: true })).resolves.toEqual( expect.objectContaining({ isHidden: true }), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, isHidden: true }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.person.update).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + isHidden: true, + }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it('should update a person favorite status', async () => { const auth = AuthFactory.create(); const person = PersonFactory.create({ isFavorite: true }); + mocks.person.getByGroupId.mockResolvedValue(person); mocks.person.update.mockResolvedValue(person); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); - await expect(sut.update(auth, person.id, { isFavorite: true })).resolves.toEqual( + await expect(sut.update(auth, person.personGroupId, { isFavorite: true })).resolves.toEqual( expect.objectContaining({ isFavorite: true }), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, isFavorite: true }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.person.update).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + isFavorite: true, + }); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it("should update a person's thumbnailPath", async () => { @@ -256,37 +284,44 @@ describe(PersonService.name, () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); + mocks.person.getByGroupId.mockResolvedValue(person); mocks.person.update.mockResolvedValue(person); mocks.person.getForFeatureFaceUpdate.mockResolvedValue(face); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([face.assetId])); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); - await expect(sut.update(auth, person.id, { featureFaceAssetId: face.assetId })).resolves.toEqual( - expect.objectContaining({ id: person.id }), + await expect(sut.update(auth, person.personGroupId, { featureFaceAssetId: face.assetId })).resolves.toEqual( + expect.objectContaining({ id: person.personGroupId }), ); - expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, faceAssetId: face.id }); + expect(mocks.person.update).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + faceAssetId: face.id, + }); expect(mocks.person.getForFeatureFaceUpdate).toHaveBeenCalledWith({ assetId: face.assetId, - personId: person.id, + personGroupId: person.personGroupId, }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.PersonGenerateThumbnail, - data: { id: person.id }, + data: { ownerId: person.ownerId, personGroupId: person.personGroupId }, }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it('should throw an error when the face feature assetId is invalid', async () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); - mocks.person.getById.mockResolvedValue(person); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.person.getByGroupId.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); - await expect(sut.update(auth, person.id, { featureFaceAssetId: '-1' })).rejects.toThrow(BadRequestException); + await expect(sut.update(auth, person.personGroupId, { featureFaceAssetId: '-1' })).rejects.toThrow( + BadRequestException, + ); expect(mocks.person.update).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); }); @@ -320,8 +355,8 @@ describe(PersonService.name, () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - mocks.person.getById.mockResolvedValue(person); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); + mocks.person.getByGroupId.mockResolvedValue(person); mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id])); mocks.person.getFacesByIds.mockResolvedValue([getForAssetFace(face)]); mocks.person.reassignFace.mockResolvedValue(1); @@ -331,15 +366,15 @@ describe(PersonService.name, () => { mocks.person.update.mockResolvedValue(person); await expect( - sut.reassignFaces(auth, person.id, { - data: [{ personId: person.id, assetId: face.assetId }], + sut.reassignFaces(auth, person.personGroupId, { + data: [{ personId: person.personGroupId, assetId: face.assetId }], }), ).resolves.toBeDefined(); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: person.id }, + data: { ownerId: person.ownerId, personGroupId: person.personGroupId }, }, ]); }); @@ -381,21 +416,21 @@ describe(PersonService.name, () => { const person = PersonFactory.create({ faceAssetId: null }); const featureFace = AssetFaceFactory.create({ assetId: asset.id, - personId: person.id, + personGroupId: person.personGroupId, sourceType: SourceType.Manual, }); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); mocks.asset.getById.mockResolvedValue(getForAsset(asset)); - mocks.person.getById.mockResolvedValue(person); + mocks.person.getByGroupId.mockResolvedValue(person); mocks.person.getRandomFace.mockResolvedValue(featureFace); mocks.person.update.mockResolvedValue({ ...person, faceAssetId: featureFace.id }); await expect( sut.createFace(auth, { assetId: asset.id, - personId: person.id, + personId: person.personGroupId, imageHeight: 500, imageWidth: 400, x: 10, @@ -408,7 +443,7 @@ describe(PersonService.name, () => { expect(mocks.asset.getById).toHaveBeenCalledWith(asset.id, { edits: true, exifInfo: true }); expect(mocks.person.createAssetFace).toHaveBeenCalledWith({ assetId: asset.id, - personId: person.id, + personGroupId: person.personGroupId, imageHeight: 500, imageWidth: 400, boundingBoxX1: 10, @@ -417,10 +452,17 @@ describe(PersonService.name, () => { boundingBoxY2: 130, sourceType: SourceType.Manual, }); - expect(mocks.person.getRandomFace).toHaveBeenCalledWith(person.id); - expect(mocks.person.update).toHaveBeenCalledWith({ id: person.id, faceAssetId: featureFace.id }); + expect(mocks.person.getRandomFace).toHaveBeenCalledWith(person.personGroupId); + expect(mocks.person.update).toHaveBeenCalledWith({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + faceAssetId: featureFace.id, + }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ - { name: JobName.PersonGenerateThumbnail, data: { id: person.id } }, + { + name: JobName.PersonGenerateThumbnail, + data: { ownerId: person.ownerId, personGroupId: person.personGroupId }, + }, ]); }); @@ -430,14 +472,14 @@ describe(PersonService.name, () => { const person = PersonFactory.create({ faceAssetId: newUuid() }); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id])); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); mocks.asset.getById.mockResolvedValue(getForAsset(asset)); - mocks.person.getById.mockResolvedValue(person); + mocks.person.getByGroupId.mockResolvedValue(person); await expect( sut.createFace(auth, { assetId: asset.id, - personId: person.id, + personId: person.personGroupId, imageHeight: 500, imageWidth: 400, x: 10, @@ -459,12 +501,12 @@ describe(PersonService.name, () => { const person = PersonFactory.create({ faceAssetId: null }); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set()); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); await expect( sut.createFace(auth, { assetId: asset.id, - personId: person.id, + personId: person.personGroupId, imageHeight: 500, imageWidth: 400, x: 10, @@ -483,11 +525,11 @@ describe(PersonService.name, () => { const person = PersonFactory.create(); mocks.person.getRandomFace.mockResolvedValue(AssetFaceFactory.create()); - await sut.createNewFeaturePhoto([person.id]); + await sut.createNewFeaturePhoto([person]); expect(mocks.job.queueAll).toHaveBeenCalledWith([ { name: JobName.PersonGenerateThumbnail, - data: { id: person.id }, + data: { ownerId: person.ownerId, personGroupId: person.personGroupId }, }, ]); }); @@ -498,20 +540,22 @@ describe(PersonService.name, () => { const face = AssetFaceFactory.create(); const person = PersonFactory.create(); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); mocks.access.person.checkFaceOwnerAccess.mockResolvedValue(new Set([face.id])); mocks.person.getFaceById.mockResolvedValue(getForAssetFace(face)); mocks.person.reassignFace.mockResolvedValue(1); - mocks.person.getById.mockResolvedValue(person); - await expect(sut.reassignFacesById(AuthFactory.create(), person.id, { id: face.id })).resolves.toEqual({ - birthDate: person.birthDate, - isHidden: person.isHidden, - isFavorite: person.isFavorite, - id: person.id, - name: person.name, - thumbnailPath: person.thumbnailPath, - updatedAt: expect.any(String), - }); + mocks.person.getByGroupId.mockResolvedValue(person); + await expect(sut.reassignFacesById(AuthFactory.create(), person.personGroupId, { id: face.id })).resolves.toEqual( + { + birthDate: person.birthDate, + isHidden: person.isHidden, + isFavorite: person.isFavorite, + id: person.personGroupId, + name: person.name, + thumbnailPath: person.thumbnailPath, + updatedAt: expect.any(String), + }, + ); expect(mocks.job.queue).not.toHaveBeenCalledWith(); expect(mocks.job.queueAll).not.toHaveBeenCalledWith(); @@ -521,12 +565,12 @@ describe(PersonService.name, () => { const face = AssetFaceFactory.create(); const person = PersonFactory.create(); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); mocks.person.getFaceById.mockResolvedValue(getForAssetFace(face)); mocks.person.reassignFace.mockResolvedValue(1); - mocks.person.getById.mockResolvedValue(person); + mocks.person.getByGroupId.mockResolvedValue(person); await expect( - sut.reassignFacesById(AuthFactory.create(), person.id, { + sut.reassignFacesById(AuthFactory.create(), person.personGroupId, { id: face.id, }), ).rejects.toBeInstanceOf(BadRequestException); @@ -537,13 +581,16 @@ describe(PersonService.name, () => { }); describe('createPerson', () => { - it('should create a new person', async () => { + it('should create a new person in a new group', async () => { const auth = AuthFactory.create(); + const group = PersonGroupFactory.create(); - mocks.person.create.mockResolvedValue(PersonFactory.create()); + mocks.person.createGroup.mockResolvedValue(group); + mocks.person.create.mockResolvedValue(PersonFactory.create({ personGroupId: group.id })); await expect(sut.create(auth, {})).resolves.toBeDefined(); - expect(mocks.person.create).toHaveBeenCalledWith({ ownerId: auth.user.id }); + expect(mocks.person.createGroup).toHaveBeenCalledWith(auth.user.id); + expect(mocks.person.create).toHaveBeenCalledWith({ ownerId: auth.user.id, personGroupId: group.id }); }); }); @@ -552,10 +599,12 @@ describe(PersonService.name, () => { const person = PersonFactory.create(); mocks.person.getAllWithoutFaces.mockResolvedValue([person]); + mocks.person.delete.mockResolvedValue([person]); await sut.handlePersonCleanup(); - expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.person.delete).toHaveBeenCalledWith([person.personGroupId], undefined); + expect(mocks.person.deleteEmptyGroups).toHaveBeenCalledWith(); expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); }); }); @@ -592,11 +641,13 @@ describe(PersonService.name, () => { mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); mocks.person.getAllWithoutFaces.mockResolvedValue([person]); + mocks.person.delete.mockResolvedValue([person]); await sut.handleQueueDetectFaces({ force: true }); expect(mocks.person.deleteFaces).toHaveBeenCalledWith({ sourceType: SourceType.MachineLearning }); - expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.person.delete).toHaveBeenCalledWith([person.personGroupId], undefined); + expect(mocks.person.deleteEmptyGroups).toHaveBeenCalledWith(); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true }); expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); expect(mocks.assetJob.streamForDetectFacesJob).toHaveBeenCalledWith(true); @@ -614,7 +665,7 @@ describe(PersonService.name, () => { await sut.handleQueueDetectFaces({ force: undefined }); - expect(mocks.person.delete).not.toHaveBeenCalled(); + expect(mocks.person.deleteGroups).not.toHaveBeenCalled(); expect(mocks.person.deleteFaces).not.toHaveBeenCalled(); expect(mocks.person.vacuum).not.toHaveBeenCalled(); expect(mocks.storage.unlink).not.toHaveBeenCalled(); @@ -637,6 +688,7 @@ describe(PersonService.name, () => { mocks.person.getAllFaces.mockReturnValue(makeStream([face])); mocks.assetJob.streamForDetectFacesJob.mockReturnValue(makeStream([asset])); mocks.person.getAllWithoutFaces.mockResolvedValue([person]); + mocks.person.delete.mockResolvedValue([person]); mocks.person.deleteFaces.mockResolvedValue(); await sut.handleQueueDetectFaces({ force: true }); @@ -648,7 +700,8 @@ describe(PersonService.name, () => { data: { id: asset.id }, }, ]); - expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.person.delete).toHaveBeenCalledWith([person.personGroupId], undefined); + expect(mocks.person.deleteEmptyGroups).toHaveBeenCalledWith(); expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: true }); }); @@ -703,7 +756,7 @@ describe(PersonService.name, () => { await sut.handleQueueRecognizeFaces({}); expect(mocks.person.getAllFaces).toHaveBeenCalledWith({ - personId: null, + personGroupId: null, sourceType: SourceType.MachineLearning, }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ @@ -769,7 +822,7 @@ describe(PersonService.name, () => { expect(mocks.systemMetadata.get).toHaveBeenCalledWith(SystemMetadataKey.FacialRecognitionState); expect(mocks.person.getLatestFaceDate).toHaveBeenCalledOnce(); expect(mocks.person.getAllFaces).toHaveBeenCalledWith({ - personId: null, + personGroupId: null, sourceType: SourceType.MachineLearning, }); expect(mocks.job.queueAll).toHaveBeenCalledWith([ @@ -817,6 +870,7 @@ describe(PersonService.name, () => { mocks.person.getAll.mockReturnValue(makeStream([face.person!, person])); mocks.person.getAllFaces.mockReturnValue(makeStream([face])); mocks.person.getAllWithoutFaces.mockResolvedValue([person]); + mocks.person.delete.mockResolvedValue([person]); mocks.person.unassignFaces.mockResolvedValue(); await sut.handleQueueRecognizeFaces({ force: true }); @@ -829,7 +883,8 @@ describe(PersonService.name, () => { data: { id: face.id, deferred: false }, }, ]); - expect(mocks.person.delete).toHaveBeenCalledWith([person.id]); + expect(mocks.person.delete).toHaveBeenCalledWith([person.personGroupId], undefined); + expect(mocks.person.deleteEmptyGroups).toHaveBeenCalledWith(); expect(mocks.storage.unlink).toHaveBeenCalledWith(person.thumbnailPath); expect(mocks.person.vacuum).toHaveBeenCalledWith({ reindexVectors: false }); }); @@ -878,7 +933,7 @@ describe(PersonService.name, () => { const face = AssetFaceFactory.create({ assetId: asset.id }); mocks.crypto.randomUUID.mockReturnValue(face.id); mocks.machineLearning.detectFaces.mockResolvedValue(getAsDetectedFace(face)); - mocks.search.searchFaces.mockResolvedValue([{ ...face, distance: 0.7 }]); + mocks.search.searchFaces.mockResolvedValue([getForFaceSearch(face, 0.7)]); mocks.assetJob.getForDetectFacesJob.mockResolvedValue(getForDetectedFaces(asset)); mocks.person.refreshFaces.mockResolvedValue(); @@ -1014,20 +1069,21 @@ describe(PersonService.name, () => { const [noPerson1, noPerson2, primaryFace, face] = [ AssetFaceFactory.create({ assetId: asset.id }), AssetFaceFactory.create(), - AssetFaceFactory.from().person().build(), - AssetFaceFactory.from().person().build(), + AssetFaceFactory.from().person({ ownerId: asset.ownerId }).build(), + AssetFaceFactory.from().person({ ownerId: asset.ownerId }).build(), ]; const faces = [ - { ...noPerson1, distance: 0 }, - { ...primaryFace, distance: 0.2 }, - { ...noPerson2, distance: 0.3 }, - { ...face, distance: 0.4 }, - ] as FaceSearchResult[]; + getForFaceSearch(noPerson1, 0), + getForFaceSearch(primaryFace, 0.2), + getForFaceSearch(noPerson2, 0.3), + getForFaceSearch(face, 0.4), + ]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); mocks.search.searchFaces.mockResolvedValue(faces); mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson1, asset)); + mocks.person.getByGroupId.mockResolvedValue(primaryFace.person!); mocks.person.create.mockResolvedValue(primaryFace.person!); await sut.handleRecognizeFaces({ id: noPerson1.id }); @@ -1036,11 +1092,11 @@ describe(PersonService.name, () => { expect(mocks.person.reassignFaces).toHaveBeenCalledTimes(1); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ faceIds: expect.arrayContaining([noPerson1.id]), - newPersonId: primaryFace.person!.id, + newPersonGroupId: primaryFace.person!.personGroupId, }); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ faceIds: expect.not.arrayContaining([face.id]), - newPersonId: primaryFace.person!.id, + newPersonGroupId: primaryFace.person!.personGroupId, }); }); @@ -1048,19 +1104,20 @@ describe(PersonService.name, () => { const asset = AssetFactory.create(); const [noPerson, face, faceWithBirthDate] = [ AssetFaceFactory.create({ assetId: asset.id }), - AssetFaceFactory.from().person().build(), - AssetFaceFactory.from().person({ birthDate: newDate() }).build(), + AssetFaceFactory.from().person({ ownerId: asset.ownerId }).build(), + AssetFaceFactory.from().person({ ownerId: asset.ownerId, birthDate: newDate() }).build(), ]; const faces = [ - { ...noPerson, distance: 0 }, - { ...face, distance: 0.2 }, - { ...faceWithBirthDate, distance: 0.3 }, - ] as FaceSearchResult[]; + getForFaceSearch(noPerson, 0), + getForFaceSearch(face, 0.2), + getForFaceSearch(faceWithBirthDate, 0.3), + ]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); mocks.search.searchFaces.mockResolvedValue(faces); mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson, asset)); + mocks.person.getByGroupId.mockResolvedValue(face.person!); mocks.person.create.mockResolvedValue(face.person!); await sut.handleRecognizeFaces({ id: noPerson.id }); @@ -1069,11 +1126,11 @@ describe(PersonService.name, () => { expect(mocks.person.reassignFaces).toHaveBeenCalledTimes(1); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ faceIds: expect.arrayContaining([noPerson.id]), - newPersonId: face.person!.id, + newPersonGroupId: face.person!.personGroupId, }); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ faceIds: expect.not.arrayContaining([face.id]), - newPersonId: face.person!.id, + newPersonGroupId: face.person!.personGroupId, }); }); @@ -1081,19 +1138,20 @@ describe(PersonService.name, () => { const asset = AssetFactory.create(); const [noPerson, face, faceWithBirthDate] = [ AssetFaceFactory.create({ assetId: asset.id }), - AssetFaceFactory.from().person().build(), - AssetFaceFactory.from().person({ birthDate: newDate() }).build(), + AssetFaceFactory.from().person({ ownerId: asset.ownerId }).build(), + AssetFaceFactory.from().person({ ownerId: asset.ownerId, birthDate: newDate() }).build(), ]; const faces = [ - { ...noPerson, distance: 0 }, - { ...faceWithBirthDate, distance: 0.2 }, - { ...face, distance: 0.3 }, - ] as FaceSearchResult[]; + getForFaceSearch(noPerson, 0), + getForFaceSearch(faceWithBirthDate, 0.2), + getForFaceSearch(face, 0.3), + ]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); mocks.search.searchFaces.mockResolvedValue(faces); mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson, asset)); + mocks.person.getByGroupId.mockResolvedValue(faceWithBirthDate.person!); mocks.person.create.mockResolvedValue(face.person!); await sut.handleRecognizeFaces({ id: noPerson.id }); @@ -1102,11 +1160,11 @@ describe(PersonService.name, () => { expect(mocks.person.reassignFaces).toHaveBeenCalledTimes(1); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ faceIds: expect.arrayContaining([noPerson.id]), - newPersonId: faceWithBirthDate.person!.id, + newPersonGroupId: faceWithBirthDate.person!.personGroupId, }); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ faceIds: expect.not.arrayContaining([face.id]), - newPersonId: faceWithBirthDate.person!.id, + newPersonGroupId: faceWithBirthDate.person!.personGroupId, }); }); @@ -1115,32 +1173,64 @@ describe(PersonService.name, () => { const [noPerson1, noPerson2] = [AssetFaceFactory.create({ assetId: asset.id }), AssetFaceFactory.create()]; const person = PersonFactory.create(); - const faces = [ - { ...noPerson1, distance: 0 }, - { ...noPerson2, distance: 0.3 }, - ] as FaceSearchResult[]; + const faces = [getForFaceSearch(noPerson1, 0), getForFaceSearch(noPerson2, 0.3)]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); mocks.search.searchFaces.mockResolvedValue(faces); mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson1, asset)); + mocks.person.createGroup.mockResolvedValue(PersonGroupFactory.create({ id: person.personGroupId })); mocks.person.create.mockResolvedValue(person); await sut.handleRecognizeFaces({ id: noPerson1.id }); + expect(mocks.person.createGroup).toHaveBeenCalledWith(asset.ownerId); expect(mocks.person.create).toHaveBeenCalledWith({ ownerId: asset.ownerId, faceAssetId: noPerson1.id, + personGroupId: person.personGroupId, }); expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ faceIds: [noPerson1.id], - newPersonId: person.id, + newPersonGroupId: person.personGroupId, + }); + }); + + it('should create a person in the matched group when the match belongs to another user', async () => { + const asset = AssetFactory.create(); + const [noPerson, otherOwnerFace] = [ + AssetFaceFactory.create({ assetId: asset.id }), + AssetFaceFactory.from().person().build(), + ]; + const person = PersonFactory.create({ + ownerId: asset.ownerId, + personGroupId: otherOwnerFace.person!.personGroupId, + }); + + const faces = [getForFaceSearch(noPerson, 0), getForFaceSearch(otherOwnerFace, 0.2)]; + + mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } }); + mocks.search.searchFaces.mockResolvedValue(faces); + mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(noPerson, asset)); + mocks.person.create.mockResolvedValue(person); + + await sut.handleRecognizeFaces({ id: noPerson.id }); + + expect(mocks.person.createGroup).not.toHaveBeenCalled(); + expect(mocks.person.create).toHaveBeenCalledWith({ + ownerId: asset.ownerId, + faceAssetId: noPerson.id, + personGroupId: otherOwnerFace.person!.personGroupId, + }); + expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ + faceIds: [noPerson.id], + newPersonGroupId: otherOwnerFace.person!.personGroupId, }); }); it('should not queue face with no matches', async () => { const asset = AssetFactory.create(); const face = AssetFaceFactory.create({ assetId: asset.id }); - const faces = [{ ...face, distance: 0 }] as FaceSearchResult[]; + const faces = [getForFaceSearch(face, 0)]; mocks.search.searchFaces.mockResolvedValue(faces); mocks.person.getFaceForFacialRecognitionJob.mockResolvedValue(getForFacialRecognitionJob(face, asset)); @@ -1158,10 +1248,7 @@ describe(PersonService.name, () => { const asset = AssetFactory.create(); const [noPerson1, noPerson2] = [AssetFaceFactory.create({ assetId: asset.id }), AssetFaceFactory.create()]; - const faces = [ - { ...noPerson1, distance: 0 }, - { ...noPerson2, distance: 0.4 }, - ] as FaceSearchResult[]; + const faces = [getForFaceSearch(noPerson1, 0), getForFaceSearch(noPerson2, 0.4)]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } }); mocks.search.searchFaces.mockResolvedValue(faces); @@ -1183,10 +1270,7 @@ describe(PersonService.name, () => { const asset = AssetFactory.create(); const [noPerson1, noPerson2] = [AssetFaceFactory.create({ assetId: asset.id }), AssetFaceFactory.create()]; - const faces = [ - { ...noPerson1, distance: 0 }, - { ...noPerson2, distance: 0.4 }, - ] as FaceSearchResult[]; + const faces = [getForFaceSearch(noPerson1, 0), getForFaceSearch(noPerson2, 0.4)]; mocks.systemMetadata.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } }); mocks.search.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]); @@ -1202,141 +1286,26 @@ describe(PersonService.name, () => { }); }); - describe('mergePerson', () => { - it('should require person.write and person.merge permission', async () => { - const auth = AuthFactory.create(); - const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()]; - - mocks.person.getById.mockResolvedValueOnce(person); - mocks.person.getById.mockResolvedValueOnce(mergePerson); - - await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).rejects.toBeInstanceOf( - BadRequestException, - ); - - expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); - - expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); - }); - - it('should merge two people without smart merge', async () => { - const auth = AuthFactory.create(); - const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()]; - - mocks.person.getById.mockResolvedValueOnce(person); - mocks.person.getById.mockResolvedValueOnce(mergePerson); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id])); - - await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([ - { id: mergePerson.id, success: true }, - ]); - - expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - newPersonId: person.id, - oldPersonId: mergePerson.id, - }); - - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); - }); - - it('should merge two people with smart merge', async () => { - const auth = AuthFactory.create(); - const [person, mergePerson] = [ - PersonFactory.create({ name: undefined }), - PersonFactory.create({ name: 'Merge person' }), - ]; - - mocks.person.getById.mockResolvedValueOnce(person); - mocks.person.getById.mockResolvedValueOnce(mergePerson); - mocks.person.update.mockResolvedValue({ ...person, name: mergePerson.name }); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id])); - - await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([ - { id: mergePerson.id, success: true }, - ]); - - expect(mocks.person.reassignFaces).toHaveBeenCalledWith({ - newPersonId: person.id, - oldPersonId: mergePerson.id, - }); - - expect(mocks.person.update).toHaveBeenCalledWith({ - id: person.id, - name: mergePerson.name, - }); - - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); - }); - - it('should throw an error when the primary person is not found', async () => { - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - - await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( - BadRequestException, - ); - - expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); - }); - - it('should handle invalid merge ids', async () => { - const auth = AuthFactory.create(); - const person = PersonFactory.create(); - - mocks.person.getById.mockResolvedValueOnce(person); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['unknown'])); - - await expect(sut.mergePerson(auth, person.id, { ids: ['unknown'] })).resolves.toEqual([ - { id: 'unknown', success: false, error: BulkIdErrorReason.NOT_FOUND }, - ]); - - expect(mocks.person.reassignFaces).not.toHaveBeenCalled(); - expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); - }); - - it('should handle an error reassigning faces', async () => { - const auth = AuthFactory.create(); - const [person, mergePerson] = [PersonFactory.create(), PersonFactory.create()]; - - mocks.person.getById.mockResolvedValueOnce(person); - mocks.person.getById.mockResolvedValueOnce(mergePerson); - mocks.person.reassignFaces.mockRejectedValue(new Error('update failed')); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([person.id])); - mocks.access.person.checkOwnerAccess.mockResolvedValueOnce(new Set([mergePerson.id])); - - await expect(sut.mergePerson(auth, person.id, { ids: [mergePerson.id] })).resolves.toEqual([ - { id: mergePerson.id, success: false, error: BulkIdErrorReason.UNKNOWN }, - ]); - - expect(mocks.person.delete).not.toHaveBeenCalled(); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); - }); - }); - describe('getStatistics', () => { it('should get correct number of person', async () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); - mocks.person.getById.mockResolvedValue(person); + mocks.person.getByGroupId.mockResolvedValue(person); mocks.person.getStatistics.mockResolvedValue({ assets: 3 }); - mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id])); - await expect(sut.getStatistics(auth, person.id)).resolves.toEqual({ assets: 3 }); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.personGroupId])); + await expect(sut.getStatistics(auth, person.personGroupId)).resolves.toEqual({ assets: 3 }); + expect(mocks.person.getStatistics).toHaveBeenCalledWith(person.personGroupId, auth.user.id); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); it('should require person.read permission', async () => { const auth = AuthFactory.create(); const person = PersonFactory.create(); - mocks.person.getById.mockResolvedValue(person); - await expect(sut.getStatistics(auth, person.id)).rejects.toBeInstanceOf(BadRequestException); - expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.id])); + mocks.person.getByGroupId.mockResolvedValue(person); + await expect(sut.getStatistics(auth, person.personGroupId)).rejects.toBeInstanceOf(BadRequestException); + expect(mocks.access.person.checkOwnerAccess).toHaveBeenCalledWith(auth.user.id, new Set([person.personGroupId])); }); }); @@ -1363,11 +1332,5 @@ describe(PersonService.name, () => { it('should not map person if person is null', () => { expect(mapFaces(getForAssetFace(AssetFaceFactory.create()), AuthFactory.create()).person).toBeNull(); }); - - it('should not map person if person does not match auth user id', () => { - expect( - mapFaces(getForAssetFace(AssetFaceFactory.from().person().build()), AuthFactory.create()).person, - ).toBeNull(); - }); }); }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 7c08c36e176c8..54c84eb22edac 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; -import { Insertable, Updateable } from 'kysely'; +import { Insertable, Selectable, Updateable } from 'kysely'; import { Person } from 'src/database'; import { Chunked, OnJob } from 'src/decorators'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; @@ -34,9 +34,10 @@ import { VectorIndex, } from 'src/enum'; import { BoundingBox } from 'src/repositories/machine-learning.repository'; -import { UpdateFacesData } from 'src/repositories/person.repository'; +import { PersonId, UpdateFacesData } from 'src/repositories/person.repository'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { FaceSearchTable } from 'src/schema/tables/face-search.table'; +import { PersonTable } from 'src/schema/tables/person.table'; import { BaseService } from 'src/services/base.service'; import { JobItem, JobOf } from 'src/types'; import { getDimensions } from 'src/utils/asset.util'; @@ -45,6 +46,8 @@ import { mimeTypes } from 'src/utils/mime-types'; import { batched, findOrFail, isFacialRecognitionEnabled } from 'src/utils/misc'; import { Point, transformPoints } from 'src/utils/transform'; +const personKey = ({ ownerId, personGroupId }: PersonId) => `${ownerId}/${personGroupId}`; + @Injectable() export class PersonService extends BaseService { async getAll(auth: AuthDto, dto: PersonSearchDto): Promise { @@ -56,7 +59,10 @@ export class PersonService extends BaseService { }; if (closestPersonId) { - const person = await this.personRepository.getById(closestPersonId); + const person = await this.personRepository.getByGroupId({ + ownerId: auth.user.id, + personGroupId: closestPersonId, + }); if (!person?.faceAssetId) { throw new NotFoundException('Person not found'); } @@ -76,92 +82,94 @@ export class PersonService extends BaseService { }; } - async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise { - await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personId] }); - const person = await this.findOrFail(personId); + async reassignFaces(auth: AuthDto, personGroupId: string, dto: AssetFaceUpdateDto): Promise { + await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personGroupId] }); + const person = await this.findOrFail(auth, personGroupId); const result: PersonResponseDto[] = []; - const changeFeaturePhoto: string[] = []; + const changeFeaturePhoto = new Map(); for (const data of dto.data) { - const faces = await this.personRepository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); + const faces = await this.personRepository.getFacesByIds( + [{ personGroupId: data.personId, assetId: data.assetId }], + { viewingUserId: auth.user.id }, + ); for (const face of faces) { await this.requireAccess({ auth, permission: Permission.PersonCreate, ids: [face.id] }); if (person.faceAssetId === null) { - changeFeaturePhoto.push(person.id); + changeFeaturePhoto.set(personKey(person), person); } if (face.person && face.person.faceAssetId === face.id) { - changeFeaturePhoto.push(face.person.id); + changeFeaturePhoto.set(personKey(face.person), face.person); } - await this.personRepository.reassignFace(face.id, personId); + await this.personRepository.reassignFace(face.id, person.personGroupId); } result.push(mapPerson(person)); } - if (changeFeaturePhoto.length > 0) { - // Remove duplicates - await this.createNewFeaturePhoto([...new Set(changeFeaturePhoto)]); + if (changeFeaturePhoto.size > 0) { + await this.createNewFeaturePhoto(changeFeaturePhoto.values().toArray()); } return result; } - async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise { - await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personId] }); + async reassignFacesById(auth: AuthDto, personGroupId: string, dto: FaceDto): Promise { + await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personGroupId] }); await this.requireAccess({ auth, permission: Permission.PersonCreate, ids: [dto.id] }); - const face = await this.personRepository.getFaceById(dto.id); - const person = await this.findOrFail(personId); + const face = await this.personRepository.getFaceById(dto.id, { viewingUserId: auth.user.id }); + const person = await this.findOrFail(auth, personGroupId); - await this.personRepository.reassignFace(face.id, personId); + await this.personRepository.reassignFace(face.id, person.personGroupId); if (person.faceAssetId === null) { - await this.createNewFeaturePhoto([person.id]); + await this.createNewFeaturePhoto([person]); } if (face.person && face.person.faceAssetId === face.id) { - await this.createNewFeaturePhoto([face.person.id]); + await this.createNewFeaturePhoto([face.person]); } - return mapPerson(await this.findOrFail(personId)); + return mapPerson(await this.findOrFail(auth, personGroupId)); } async getFacesById(auth: AuthDto, dto: FaceDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.id] }); - const faces = await this.personRepository.getFaces(dto.id); + const faces = await this.personRepository.getFaces(dto.id, { viewingUserId: auth.user.id, isVisible: true }); const asset = await this.assetRepository.getForFaces(dto.id); const assetDimensions = getDimensions(asset); return faces.map((face) => mapFaces(face, auth, asset.edits, assetDimensions)); } - async createNewFeaturePhoto(changeFeaturePhoto: string[]) { + async createNewFeaturePhoto(changeFeaturePhoto: PersonId[]) { this.logger.debug( `Changing feature photos for ${changeFeaturePhoto.length} ${changeFeaturePhoto.length > 1 ? 'people' : 'person'}`, ); const jobs: JobItem[] = []; - for (const personId of changeFeaturePhoto) { - const assetFace = await this.personRepository.getRandomFace(personId); + for (const { ownerId, personGroupId } of changeFeaturePhoto) { + const assetFace = await this.personRepository.getRandomFace(personGroupId); if (assetFace) { - await this.personRepository.update({ id: personId, faceAssetId: assetFace.id }); - jobs.push({ name: JobName.PersonGenerateThumbnail, data: { id: personId } }); + await this.personRepository.update({ ownerId, personGroupId, faceAssetId: assetFace.id }); + jobs.push({ name: JobName.PersonGenerateThumbnail, data: { ownerId, personGroupId } }); } } await this.jobRepository.queueAll(jobs); } - async getById(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] }); - return mapPerson(await this.findOrFail(id)); + async getById(auth: AuthDto, personGroupId: string): Promise { + await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [personGroupId] }); + return mapPerson(await this.findOrFail(auth, personGroupId)); } - async getStatistics(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] }); - return this.personRepository.getStatistics(id); + async getStatistics(auth: AuthDto, personGroupId: string): Promise { + await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [personGroupId] }); + return this.personRepository.getStatistics(personGroupId, auth.user.id); } - async getThumbnail(auth: AuthDto, id: string): Promise { - await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] }); - const person = await this.personRepository.getById(id); + async getThumbnail(auth: AuthDto, personGroupId: string): Promise { + await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [personGroupId] }); + const person = await this.personRepository.getByGroupId({ ownerId: auth.user.id, personGroupId }); if (!person || !person.thumbnailPath) { throw new NotFoundException(); } @@ -174,8 +182,10 @@ export class PersonService extends BaseService { } async create(auth: AuthDto, dto: PersonCreateDto): Promise { + const group = await this.personRepository.createGroup(auth.user.id); const person = await this.personRepository.create({ ownerId: auth.user.id, + personGroupId: group.id, name: dto.name, birthDate: dto.birthDate, isHidden: dto.isHidden, @@ -186,15 +196,16 @@ export class PersonService extends BaseService { return mapPerson(person); } - async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { - await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [id] }); + async update(auth: AuthDto, personGroupId: string, dto: PersonUpdateDto): Promise { + await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personGroupId] }); + const { ownerId } = await this.findOrFail(auth, personGroupId); const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto; // TODO: set by faceId directly let faceId: string | undefined; if (assetId) { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [assetId] }); - const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId }); + const face = await this.personRepository.getForFeatureFaceUpdate({ personGroupId, assetId }); if (!face) { throw new BadRequestException('Invalid assetId for feature face or asset is offline'); } @@ -203,7 +214,8 @@ export class PersonService extends BaseService { } const person = await this.personRepository.update({ - id, + ownerId, + personGroupId, faceAssetId: faceId, name, birthDate, @@ -213,7 +225,7 @@ export class PersonService extends BaseService { }); if (assetId) { - await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id } }); + await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { ownerId, personGroupId } }); } return mapPerson(person); @@ -245,21 +257,32 @@ export class PersonService extends BaseService { async deleteAll(auth: AuthDto, { ids }: BulkIdsDto): Promise { await this.requireAccess({ auth, permission: Permission.PersonDelete, ids }); - const people = await this.personRepository.getForPeopleDelete(ids); - await this.removeAllPeople(people); + await this.removeAllPersonGroups(ids, auth.user.id); } @Chunked() - private async removeAllPeople(people: { id: string; thumbnailPath: string }[]) { + private async removeAllPersonGroups(groupIds: string[], ownerId?: string) { + if (groupIds.length === 0) { + return; + } + + const people = await this.personRepository.delete(groupIds, ownerId); await Promise.all(people.map((person) => this.storageRepository.unlink(person.thumbnailPath))); - await this.personRepository.delete(people.map((person) => person.id)); - this.logger.debug(`Deleted ${people.length} people`); + await this.personRepository.deleteEmptyGroups(); + this.logger.debug(`Deleted ${groupIds.length} people`); } @OnJob({ name: JobName.PersonCleanup, queue: QueueName.BackgroundTask }) async handlePersonCleanup(): Promise { + // each step can leave the next one something to clean up, so the order matters const people = await this.personRepository.getAllWithoutFaces(); - await this.removeAllPeople(people); + await this.removeAllPersonGroups(people.map((person) => person.personGroupId)); + + const personGroups = await this.personRepository.deleteEmptyGroups(); + const clusterGroups = await this.personRepository.deleteOrphanedClusterGroups(); + + this.logger.debug(`Deleted ${personGroups} empty person groups and ${clusterGroups} orphaned cluster groups`); + return JobStatus.Success; } @@ -429,7 +452,7 @@ export class PersonService extends BaseService { const lastRun = new Date().toISOString(); const faces = this.personRepository.getAllFaces( - force ? undefined : { personId: null, sourceType: SourceType.MachineLearning }, + force ? undefined : { personGroupId: null, sourceType: SourceType.MachineLearning }, ); for await (const batch of batched(faces)) { await this.jobRepository.queueAll( @@ -465,13 +488,14 @@ export class PersonService extends BaseService { return JobStatus.Failed; } - if (face.personId) { + if (face.personGroupId) { this.logger.debug(`Face ${id} already has a person assigned`); return JobStatus.Skipped; } + const { ownerId, clusterGroupId } = face.asset; const matches = await this.searchRepository.searchFaces({ - userIds: [face.asset.ownerId], + clusterGroupId, embedding: face.faceSearch.embedding, maxDistance: machineLearning.facialRecognition.maxDistance, numResults: machineLearning.facialRecognition.minFaces, @@ -495,10 +519,10 @@ export class PersonService extends BaseService { return JobStatus.Skipped; } - let personId = matches.find((match) => match.personId)?.personId; - if (!personId) { - const matchWithPerson = await this.searchRepository.searchFaces({ - userIds: [face.asset.ownerId], + let personGroupId = matches.find((match) => match.personGroupId)?.personGroupId; + if (!personGroupId) { + const [matchWithPerson] = await this.searchRepository.searchFaces({ + clusterGroupId, embedding: face.faceSearch.embedding, maxDistance: machineLearning.facialRecognition.maxDistance, numResults: 1, @@ -506,29 +530,38 @@ export class PersonService extends BaseService { minBirthDate: new Date(face.asset.fileCreatedAt), }); - if (matchWithPerson.length > 0) { - personId = matchWithPerson[0].personId; - } + personGroupId = matchWithPerson?.personGroupId ?? undefined; } - if (isCore && !personId) { - this.logger.log(`Creating new person for face ${id}`); - const newPerson = await this.personRepository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id }); - await this.jobRepository.queue({ name: JobName.PersonGenerateThumbnail, data: { id: newPerson.id } }); - personId = newPerson.id; + if (!personGroupId && isCore) { + const group = await this.personRepository.createGroup(ownerId); + personGroupId = group.id; + this.logger.log(`Created person group ${personGroupId} for face ${id}`); } - if (personId) { - this.logger.debug(`Assigning face ${id} to person ${personId}`); - await this.personRepository.reassignFaces({ faceIds: [id], newPersonId: personId }); + if (personGroupId) { + const person = await this.personRepository.getByGroupId({ ownerId, personGroupId }); + if (person) { + this.logger.debug(`Face ${id} matched person ${person.personGroupId}`); + } else { + await this.personRepository.create({ ownerId, faceAssetId: face.id, personGroupId }); + this.logger.log(`Created person for face ${id} in group ${personGroupId}`); + await this.jobRepository.queue({ + name: JobName.PersonGenerateThumbnail, + data: { ownerId, personGroupId }, + }); + } + + this.logger.debug(`Assigning face ${id} to person group ${personGroupId}`); + await this.personRepository.reassignFaces({ faceIds: [id], newPersonGroupId: personGroupId }); } return JobStatus.Success; } @OnJob({ name: JobName.PersonFileMigration, queue: QueueName.Migration }) - async handlePersonMigration({ id }: JobOf): Promise { - const person = await this.personRepository.getById(id); + async handlePersonMigration({ ownerId, personGroupId }: JobOf): Promise { + const person = await this.personRepository.getByGroupId({ ownerId, personGroupId }); if (!person) { return JobStatus.Failed; } @@ -538,15 +571,13 @@ export class PersonService extends BaseService { return JobStatus.Success; } - async mergePerson(auth: AuthDto, id: string, dto: MergePersonDto): Promise { + async mergePerson(auth: AuthDto, personGroupId: string, dto: MergePersonDto): Promise { const mergeIds = dto.ids; - if (mergeIds.includes(id)) { + if (mergeIds.includes(personGroupId)) { throw new BadRequestException('Cannot merge a person into themselves'); } - await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [id] }); - let primaryPerson = await this.findOrFail(id); - const primaryName = primaryPerson.name || primaryPerson.id; + await this.requireAccess({ auth, permission: Permission.PersonUpdate, ids: [personGroupId] }); const results: BulkIdResponseDto[] = []; @@ -556,52 +587,71 @@ export class PersonService extends BaseService { ids: mergeIds, }); - for (const mergeId of mergeIds) { + let primaryPerson: Selectable | undefined; + + for (const mergePerson of await this.personRepository.getForMergePerson(mergeIds)) { + const mergeId = mergePerson.personGroupId; const hasAccess = allowedIds.has(mergeId); if (!hasAccess) { results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); continue; } - try { - const mergePerson = await this.personRepository.getById(mergeId); - if (!mergePerson) { - results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NOT_FOUND }); + if (!primaryPerson || primaryPerson.ownerId !== mergePerson.ownerId) { + primaryPerson = await this.personRepository.getByGroupId({ ownerId: mergePerson.ownerId, personGroupId }); + if (!primaryPerson) { continue; } + } - const update: Updateable & { id: string } = { id: primaryPerson.id }; - if (!primaryPerson.name && mergePerson.name) { - update.name = mergePerson.name; - } + const changes: Updateable = {}; + if (!primaryPerson.name && mergePerson.name) { + changes.name = mergePerson.name; + } - if (!primaryPerson.birthDate && mergePerson.birthDate) { - update.birthDate = mergePerson.birthDate; - } + if (!primaryPerson.birthDate && mergePerson.birthDate) { + changes.birthDate = mergePerson.birthDate; + } - if (Object.keys(update).length > 1) { - primaryPerson = await this.personRepository.update(update); - } + if ( + (mergePerson.name && mergePerson.name !== primaryPerson.name) || + (mergePerson.birthDate && mergePerson.birthDate !== primaryPerson.birthDate) + ) { + continue; + } - const mergeName = mergePerson.name || mergePerson.id; - const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id }; - this.logger.log(`Merging ${mergeName} into ${primaryName}`); + if (Object.keys(changes).length > 0) { + primaryPerson = await this.personRepository.update({ + ownerId: primaryPerson.ownerId, + personGroupId: primaryPerson.personGroupId, + ...changes, + }); + } + const mergeName = mergePerson.name || mergePerson.personGroupId; + const mergeData: UpdateFacesData = { + oldPersonGroupId: mergeId, + newPersonGroupId: primaryPerson.personGroupId, + ownerId: primaryPerson.ownerId, + }; + this.logger.log(`Merging ${mergeName} into ${primaryPerson.name || primaryPerson.personGroupId}`); + + try { await this.personRepository.reassignFaces(mergeData); - await this.removeAllPeople([mergePerson]); + await this.removeAllPersonGroups([mergeId], primaryPerson.ownerId); - this.logger.log(`Merged ${mergeName} into ${primaryName}`); + this.logger.log(`Merged ${mergeName} into ${primaryPerson.name || primaryPerson.personGroupId}`); results.push({ id: mergeId, success: true }); - } catch (error: Error | any) { - this.logger.error(`Unable to merge ${mergeId} into ${id}: ${error}`, error?.stack); + } catch (error: any) { + this.logger.error(`Unable to merge ${mergeId} into ${personGroupId}: ${error}`, error?.stack); results.push({ id: mergeId, success: false, error: BulkIdErrorReason.UNKNOWN }); } } return results; } - private findOrFail(id: string) { - return findOrFail(() => this.personRepository.getById(id), 'Person'); + private findOrFail(auth: AuthDto, personGroupId: string) { + return findOrFail(() => this.personRepository.getByGroupId({ ownerId: auth.user.id, personGroupId }), 'Person'); } // TODO return a asset face response @@ -613,7 +663,7 @@ export class PersonService extends BaseService { const [asset, person] = await Promise.all([ this.assetRepository.getById(dto.assetId, { edits: true, exifInfo: true }), - this.findOrFail(dto.personId), + this.findOrFail(auth, dto.personId), ]); if (!asset) { @@ -661,7 +711,7 @@ export class PersonService extends BaseService { } await this.personRepository.createAssetFace({ - personId: dto.personId, + personGroupId: person.personGroupId, assetId: dto.assetId, imageHeight: dto.imageHeight, imageWidth: dto.imageWidth, @@ -673,7 +723,7 @@ export class PersonService extends BaseService { }); if (!person.faceAssetId) { - await this.createNewFeaturePhoto([person.id]); + await this.createNewFeaturePhoto([person]); } } diff --git a/server/src/services/queue.service.spec.ts b/server/src/services/queue.service.spec.ts index 5643c5eced84b..3490c634e6e23 100644 --- a/server/src/services/queue.service.spec.ts +++ b/server/src/services/queue.service.spec.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { defaults, SystemConfig } from 'src/config'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { ImmichWorker, JobName, QueueCommand, QueueName } from 'src/enum'; import { QueueService } from 'src/services/queue.service'; import { factory } from 'test/small.factory'; diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index 1f0bf6c76b7c4..7130146c15405 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { SystemConfig } from 'src/config'; import { OnEvent } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfig } from 'src/dtos/config.dto'; import { mapQueueLegacy, mapQueuesLegacy, diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index c0f5a3614d288..bc59a4ad727b7 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -250,7 +250,13 @@ describe(SearchService.name, () => { ); expect(mocks.search.searchSmart).toHaveBeenCalledWith( { page: 1, size: 100 }, - { query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id], visibility: 'not-locked' }, + { + query: 'test', + embedding: '[1, 2, 3]', + userIds: [authStub.user1.user.id], + viewingUserId: authStub.user1.user.id, + visibility: 'not-locked', + }, ); }); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index c99b340daeaa5..1612b0e29c75b 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -44,12 +44,14 @@ export class SearchService extends BaseService { const cities = await this.assetRepository.getAssetIdByCity(auth.user.id, options); const cityAssets = await this.assetRepository.getByIdsWithAllRelationsButStacks( cities.items.map(({ data }) => data), + auth.user.id, ); const cityItems = cityAssets.map((asset) => ({ value: asset.exifInfo!.city!, data: mapAsset(asset, { auth }) })); const recents = await this.assetRepository.getRecentlyCreatedAssetIds(auth.user.id, options.maxFields); const recentAssets = await this.assetRepository.getByIdsWithAllRelationsButStacks( recents.items.map((item) => item.data), + auth.user.id, ); const recentItems = recentAssets.map((asset) => ({ value: asset.createdAt.toISOString(), @@ -92,6 +94,7 @@ export class SearchService extends BaseService { checksum, visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), userIds, + viewingUserId: auth.user.id, orderDirection: dto.order ?? AssetOrder.Desc, }, ); @@ -109,6 +112,7 @@ export class SearchService extends BaseService { ...dto, visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), userIds, + viewingUserId: auth.user.id, }); } @@ -122,6 +126,7 @@ export class SearchService extends BaseService { ...dto, visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), userIds, + viewingUserId: auth.user.id, }); return items.map((item) => mapAsset(item, { auth })); } @@ -136,6 +141,7 @@ export class SearchService extends BaseService { ...dto, visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), userIds, + viewingUserId: auth.user.id, }); return items.map((item) => mapAsset(item, { auth })); } @@ -180,6 +186,7 @@ export class SearchService extends BaseService { { ...dto, userIds: await userIds, + viewingUserId: auth.user.id, embedding, visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'), }, diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 6bd0a3c9b2064..354672a6e5945 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -1,4 +1,4 @@ -import { SystemConfig } from 'src/config'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AssetFileType, AssetVisibility, ImmichWorker, JobName, JobStatus } from 'src/enum'; import { SmartInfoService } from 'src/services/smart-info.service'; import { getCLIPModelInfo } from 'src/utils/misc'; diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 19a17744928f3..e147d3da7fc3b 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { SystemConfig } from 'src/config'; +import { SystemConfig } from 'src/dtos/config.dto'; import { OnEvent, OnJob } from 'src/decorators'; import { AssetVisibility, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum'; diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 8f11a1dfa2ee1..72d8802e1b15c 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -1,5 +1,5 @@ import { Stats } from 'node:fs'; -import { defaults, SystemConfig } from 'src/config'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { AssetPathType, AssetType, JobStatus } from 'src/enum'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { AlbumFactory } from 'test/factories/album.factory'; diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index de731f46c6697..c2aaf986eb738 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -5,7 +5,7 @@ import path from 'node:path'; import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; -import { SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; +import { ConfigTemplateStorageOptionDto } from 'src/dtos/config.dto'; import { AssetFileType, AssetPathType, @@ -129,7 +129,7 @@ export class StorageTemplateService extends BaseService { } } - getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { + getStorageTemplateOptions(): ConfigTemplateStorageOptionDto { return { ...storageTokens, presetOptions: storagePresets }; } diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 39ccd9ae6f68b..c9b98c5d668ad 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -227,6 +227,7 @@ export class SyncService extends BaseService { await this.syncRepository.memoryToAsset.cleanupAuditTable(pruneThreshold); await this.syncRepository.partner.cleanupAuditTable(pruneThreshold); await this.syncRepository.person.cleanupAuditTable(pruneThreshold); + await this.syncRepository.personGroup.cleanupAuditTable(pruneThreshold); await this.syncRepository.stack.cleanupAuditTable(pruneThreshold); await this.syncRepository.user.cleanupAuditTable(pruneThreshold); await this.syncRepository.userMetadata.cleanupAuditTable(pruneThreshold); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 08851da96aff9..90d91ccf48cd9 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -1,6 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { defaults, SystemConfig } from 'src/config'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; +import { defaults, SystemConfig } from 'src/dtos/config.dto'; import { AudioCodec, Colorspace, @@ -10,6 +9,7 @@ import { LogLevel, OAuthTokenEndpointAuthMethod, QueueName, + ReleaseChannel, ToneMapping, TranscodeHardwareAcceleration, TranscodePolicy, @@ -269,7 +269,7 @@ describe(SystemConfigService.name, () => { it('should return the default config', () => { mocks.systemMetadata.get.mockResolvedValue(partialConfig); - expect(sut.getDefaults()).toEqual(defaults); + expect(sut.getAdminConfigDefaults()).toEqual(defaults); expect(mocks.systemMetadata.get).not.toHaveBeenCalled(); }); }); @@ -278,7 +278,7 @@ describe(SystemConfigService.name, () => { it('should return the default config', async () => { mocks.systemMetadata.get.mockResolvedValue({}); - await expect(sut.getSystemConfig()).resolves.toEqual(defaults); + await expect(sut.getAdminConfig()).resolves.toEqual(defaults); }); it('should merge the overrides', async () => { @@ -289,14 +289,14 @@ describe(SystemConfigService.name, () => { user: { deleteDelay: 15 }, }); - await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig); + await expect(sut.getAdminConfig()).resolves.toEqual(updatedConfig); }); it('should load the config from a json file', async () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig)); - await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig); + await expect(sut.getAdminConfig()).resolves.toEqual(updatedConfig); expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json'); }); @@ -305,7 +305,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { twoPass: 'false' } })); - await expect(sut.getSystemConfig()).resolves.toMatchObject({ + await expect(sut.getAdminConfig()).resolves.toMatchObject({ ffmpeg: expect.objectContaining({ twoPass: false }), }); }); @@ -314,7 +314,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ ffmpeg: { threads: '42' } })); - await expect(sut.getSystemConfig()).resolves.toMatchObject({ + await expect(sut.getAdminConfig()).resolves.toMatchObject({ ffmpeg: expect.objectContaining({ threads: 42 }), }); }); @@ -325,7 +325,7 @@ describe(SystemConfigService.name, () => { JSON.stringify({ library: { scan: { cronExpression: '0 0 */3 * *' } } }), ); - await expect(sut.getSystemConfig()).resolves.toMatchObject({ + await expect(sut.getAdminConfig()).resolves.toMatchObject({ library: { scan: { enabled: true, @@ -339,7 +339,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ oauth: { issuerUrl: 'accounts.google.com' } })); - await expect(sut.getSystemConfig()).rejects.toThrow( + await expect(sut.getAdminConfig()).rejects.toThrow( '[oauth.issuerUrl] Issuer URL must be an empty string or a valid URL', ); }); @@ -348,7 +348,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } })); - await expect(sut.getSystemConfig()).rejects.toThrow('[library.scan.cronExpression] Invalid cron expression'); + await expect(sut.getAdminConfig()).rejects.toThrow('[library.scan.cronExpression] Invalid cron expression'); }); it('should log errors with the config file', async () => { @@ -356,7 +356,7 @@ describe(SystemConfigService.name, () => { mocks.systemMetadata.readFile.mockResolvedValue(`{ "ffmpeg2": true, "ffmpeg2": true }`); - await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error); + await expect(sut.getAdminConfig()).rejects.toBeInstanceOf(Error); expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json'); expect(mocks.logger.error).toHaveBeenCalledTimes(2); @@ -380,7 +380,7 @@ describe(SystemConfigService.name, () => { `; mocks.systemMetadata.readFile.mockResolvedValue(partialConfig); - await expect(sut.getSystemConfig()).resolves.toEqual(updatedConfig); + await expect(sut.getAdminConfig()).resolves.toEqual(updatedConfig); expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.yaml'); }); @@ -389,7 +389,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({})); - await expect(sut.getSystemConfig()).resolves.toEqual(defaults); + await expect(sut.getAdminConfig()).resolves.toEqual(defaults); expect(mocks.systemMetadata.readFile).toHaveBeenCalledWith('immich-config.json'); }); @@ -399,7 +399,7 @@ describe(SystemConfigService.name, () => { const partialConfig = { machineLearning: { urls: ['immich_machine_learning'] } }; mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig)); - const config = await sut.getSystemConfig(); + const config = await sut.getAdminConfig(); expect(config.machineLearning.urls).toEqual(['immich_machine_learning']); }); @@ -420,7 +420,7 @@ describe(SystemConfigService.name, () => { const partialConfig = { server: { externalDomain } }; mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig)); - const config = await sut.getSystemConfig(); + const config = await sut.getAdminConfig(); expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app'); }); } @@ -432,7 +432,7 @@ describe(SystemConfigService.name, () => { `; mocks.systemMetadata.readFile.mockResolvedValue(partialConfig); - await sut.getSystemConfig(); + await sut.getAdminConfig(); expect(mocks.logger.warn).toHaveBeenCalled(); }); @@ -467,12 +467,12 @@ describe(SystemConfigService.name, () => { mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(test.config)); if (test.throws) { - await expect(sut.getSystemConfig()).rejects.toThrow(test.throws); + await expect(sut.getAdminConfig()).rejects.toThrow(test.throws); } else if (test.warn) { - await sut.getSystemConfig(); + await sut.getAdminConfig(); expect(mocks.logger.warn).toHaveBeenCalled(); } else { - const config = await sut.getSystemConfig(); + const config = await sut.getAdminConfig(); test.check!(config); } }); @@ -482,7 +482,7 @@ describe(SystemConfigService.name, () => { describe('updateConfig', () => { it('should update the config and emit an event', async () => { mocks.systemMetadata.get.mockResolvedValue(partialConfig); - await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig); + await expect(sut.updateAdminConfig(updatedConfig)).resolves.toEqual(updatedConfig); expect(mocks.event.emit).toHaveBeenCalledWith( 'ConfigUpdate', expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }), @@ -492,7 +492,7 @@ describe(SystemConfigService.name, () => { it('should throw an error if a config file is in use', async () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({})); - await expect(sut.updateSystemConfig(defaults)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.updateAdminConfig(defaults)).rejects.toBeInstanceOf(BadRequestException); expect(mocks.systemMetadata.set).not.toHaveBeenCalled(); }); }); diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index faa4f8d423bab..055a21f1c5044 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,8 +1,15 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import _ from 'lodash'; -import { defaults } from 'src/config'; import { OnEvent } from 'src/decorators'; -import { mapConfig, SystemConfigDto } from 'src/dtos/system-config.dto'; +import { + AdminConfigDto, + defaults, + mapAdminConfig, + mapPublicConfig, + mapUserConfig, + PublicConfigDto, + UserConfigDto, +} from 'src/dtos/config.dto'; import { BootstrapEventPriority } from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; @@ -22,13 +29,31 @@ export class SystemConfigService extends BaseService { this.machineLearningRepository.teardown(); } - async getSystemConfig(): Promise { + async getAdminConfig(): Promise { const config = await this.getConfig({ withCache: false }); - return mapConfig(config); + return mapAdminConfig(config); } - getDefaults(): SystemConfigDto { - return mapConfig(defaults); + getAdminConfigDefaults(): AdminConfigDto { + return mapAdminConfig(defaults); + } + + async getUserConfig(): Promise { + const config = await this.getConfig({ withCache: false }); + return mapUserConfig(config); + } + + getUserConfigDefaults(): UserConfigDto { + return mapUserConfig(defaults); + } + + async getPublicConfig(): Promise { + const config = await this.getConfig({ withCache: false }); + return mapPublicConfig(config); + } + + getPublicConfigDefaults(): PublicConfigDto { + return mapPublicConfig(defaults); } @OnEvent({ name: 'ConfigInit', priority: -100 }) @@ -56,7 +81,7 @@ export class SystemConfigService extends BaseService { } } - async updateSystemConfig(dto: SystemConfigDto): Promise { + async updateAdminConfig(dto: AdminConfigDto): Promise { const { configFile } = this.configRepository.getEnv(); if (configFile) { throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use'); @@ -75,7 +100,7 @@ export class SystemConfigService extends BaseService { await this.eventRepository.emit('ConfigUpdate', { newConfig, oldConfig }); - return mapConfig(newConfig); + return mapAdminConfig(newConfig); } async getCustomCss(): Promise { diff --git a/server/src/services/timeline.service.spec.ts b/server/src/services/timeline.service.spec.ts index ca668b606874c..76a2ef0c67f8f 100644 --- a/server/src/services/timeline.service.spec.ts +++ b/server/src/services/timeline.service.spec.ts @@ -19,9 +19,12 @@ describe(TimelineService.name, () => { await expect(sut.getTimeBuckets(authStub.admin, {})).resolves.toEqual( expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]), ); - expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith({ - userIds: [authStub.admin.user.id], - }); + expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith( + { + userIds: [authStub.admin.user.id], + }, + authStub.admin, + ); }); it('should pass bbox options to repository when all bbox fields are provided', async () => { @@ -36,10 +39,13 @@ describe(TimelineService.name, () => { }, }); - expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith({ - userIds: [authStub.admin.user.id], - bbox: { west: -70, south: -30, east: 120, north: 55 }, - }); + expect(mocks.asset.getTimeBuckets).toHaveBeenCalledWith( + { + userIds: [authStub.admin.user.id], + bbox: { west: -70, south: -30, east: 120, north: 55 }, + }, + authStub.admin, + ); }); }); diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index d043267745106..197b331cc3d86 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -12,7 +12,7 @@ export class TimelineService extends BaseService { async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { await this.timeBucketChecks(auth, dto); const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto); - return await this.assetRepository.getTimeBuckets(timeBucketOptions); + return await this.assetRepository.getTimeBuckets(timeBucketOptions, auth); } // pre-jsonified response diff --git a/server/src/services/user-admin.service.spec.ts b/server/src/services/user-admin.service.spec.ts index 49aefaa870132..c52ae54221175 100644 --- a/server/src/services/user-admin.service.spec.ts +++ b/server/src/services/user-admin.service.spec.ts @@ -53,6 +53,7 @@ describe(UserAdminService.name, () => { name: userStub.user1.name, storageLabel: 'label', password: expect.anything(), + clusterGroupId: expect.any(String), }); }); }); diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 0044730ceeb22..b5fc6764f2194 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -1,8 +1,7 @@ import { DateTime } from 'luxon'; import { SemVer } from 'semver'; -import { defaults } from 'src/config'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; -import { CronJob, JobName, JobStatus, SystemMetadataKey } from 'src/enum'; +import { defaults } from 'src/dtos/config.dto'; +import { CronJob, JobName, JobStatus, ReleaseChannel, SystemMetadataKey } from 'src/enum'; import { VersionService } from 'src/services/version.service'; import { factory } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index f1abeedb30ab0..85bf579fbdb21 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -4,8 +4,16 @@ import semver, { SemVer } from 'semver'; import { serverVersion } from 'src/constants'; import { OnEvent, OnJob } from 'src/decorators'; import { ReleaseEventV1, ReleaseType, ServerVersionResponseDto } from 'src/dtos/server.dto'; -import { ReleaseChannel } from 'src/dtos/system-config.dto'; -import { CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, SystemMetadataKey } from 'src/enum'; +import { + CronJob, + DatabaseLock, + ImmichWorker, + JobName, + JobStatus, + QueueName, + ReleaseChannel, + SystemMetadataKey, +} from 'src/enum'; import { ArgOf } from 'src/repositories/event.repository'; import { BaseService } from 'src/services/base.service'; import { VersionCheckMetadata } from 'src/types'; diff --git a/server/src/types.ts b/server/src/types.ts index d31841ffd320a..2eb996d14ab93 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -1,9 +1,9 @@ import { ShallowDehydrateObject } from 'kysely'; -import { SystemConfig } from 'src/config'; import { VECTOR_EXTENSIONS } from 'src/constants'; import { AssetFile } from 'src/database'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; import { SetMaintenanceModeDto } from 'src/dtos/maintenance.dto'; import { @@ -34,10 +34,10 @@ import { Mocked } from 'vitest'; export type DeepPartial = T extends Date ? T - : T extends Record - ? { [K in keyof T]?: DeepPartial } - : T extends Array - ? DeepPartial[] + : T extends Array + ? DeepPartial[] + : T extends object + ? { [K in keyof T]?: DeepPartial } : T; export type RepositoryInterface = Pick; @@ -215,6 +215,11 @@ export interface IDelayedJob extends IBaseJob { } export type JobSource = 'upload' | 'sidecar-write' | 'copy' | 'edit'; +export interface IPersonJob { + ownerId: string; + personGroupId: string; +} + export interface IEntityJob extends IBaseJob { id: string; source?: JobSource; @@ -352,7 +357,7 @@ export type JobItem = // Migration | { name: JobName.FileMigrationQueueAll; data?: IBaseJob } | { name: JobName.AssetFileMigration; data: IEntityJob } - | { name: JobName.PersonFileMigration; data: IEntityJob } + | { name: JobName.PersonFileMigration; data: IPersonJob } // Metadata Extraction | { name: JobName.AssetExtractMetadataQueueAll; data: IBaseJob } @@ -371,7 +376,7 @@ export type JobItem = | { name: JobName.AssetDetectFaces; data: IEntityJob } | { name: JobName.FacialRecognitionQueueAll; data: INightlyJob } | { name: JobName.FacialRecognition; data: IDeferrableJob } - | { name: JobName.PersonGenerateThumbnail; data: IEntityJob } + | { name: JobName.PersonGenerateThumbnail; data: IPersonJob } // Smart Search | { name: JobName.SmartSearchQueueAll; data: IBaseJob } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 0d6f4b4eebb82..97c1e8e0c5e42 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -299,8 +299,29 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe return access.person.checkFaceOwnerAccess(auth.user.id, ids); } + case Permission.ClusterGroupRead: { + const isMember = await access.clusterGroup.checkOwnerAccess(auth.user.id, ids); + const isInvited = await access.clusterGroup.checkInviteAccess(auth.user.id, setDifference(ids, isMember)); + return setUnion(isMember, isInvited); + } + + case Permission.ClusterGroupLeave: + case Permission.ClusterGroupRequestCreate: { + return access.clusterGroup.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ClusterGroupRequestDelete: { + const isOwner = await access.clusterGroupRequest.checkOwnerAccess(auth.user.id, ids); + const isGroupMember = await access.clusterGroupRequest.checkGroupAccess(auth.user.id, ids); + return setUnion(isOwner, isGroupMember); + } + + case Permission.ClusterGroupRequestRead: { + return access.clusterGroupRequest.checkOwnerAccess(auth.user.id, ids); + } + case Permission.PartnerUpdate: { - return await access.partner.checkUpdateAccess(auth.user.id, ids); + return access.partner.checkUpdateAccess(auth.user.id, ids); } case Permission.SessionRead: diff --git a/server/src/utils/config.ts b/server/src/utils/config.ts index a6073471d16fe..38e5facb03dac 100644 --- a/server/src/utils/config.ts +++ b/server/src/utils/config.ts @@ -1,8 +1,7 @@ import AsyncLock from 'async-lock'; import { load as loadYaml } from 'js-yaml'; import * as _ from 'lodash'; -import { SystemConfig, defaults } from 'src/config'; -import { SystemConfigSchema } from 'src/dtos/system-config.dto'; +import { AdminConfigDto, SystemConfig, defaults } from 'src/dtos/config.dto'; import { DatabaseLock, SystemMetadataKey } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -100,7 +99,7 @@ const buildConfig = async (repos: RepoDeps) => { } // validate with Zod schema - const result = SystemConfigSchema.safeParse(rawConfig); + const result = AdminConfigDto.schema.safeParse(rawConfig); if (!result.success) { const messages = ['Invalid system config: ']; for (const issue of result.error.issues) { diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 5122b0a9d4b33..275b354c1611d 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -235,43 +235,68 @@ export function withFilePath(eb: ExpressionBuilder, type: AssetFile .where('asset_file.isEdited', '=', sql.lit(isEdited)); } -export function withFacesAndPeople( - eb: ExpressionBuilder, - withHidden?: boolean, - withDeletedFace?: boolean, -) { - return jsonArrayFrom( - eb - .selectFrom('asset_face') - .leftJoinLateral( - (eb) => - eb.selectFrom('person').selectAll('person').whereRef('asset_face.personId', '=', 'person.id').as('person'), - (join) => join.onTrue(), - ) - .selectAll('asset_face') - .select((eb) => eb.table('person').$castTo>().as('person')) - .whereRef('asset_face.assetId', '=', 'asset.id') - .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null)) - .$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true)), - ).as('faces'); +export type WithFacesAndPeopleOptions = { + /** whose version of the person to select */ + viewingUserId?: string; + withHidden?: boolean; + withDeletedFace?: boolean; +}; + +export function withFacesAndPeople({ viewingUserId, withHidden, withDeletedFace }: WithFacesAndPeopleOptions) { + return (eb: ExpressionBuilder) => + jsonArrayFrom( + eb + .selectFrom('asset_face') + .leftJoinLateral( + (eb) => + eb + .selectFrom('person') + .selectAll('person') + .whereRef('person.personGroupId', '=', 'asset_face.personGroupId') + .$if(!viewingUserId, (qb) => qb.whereRef('person.ownerId', '=', 'asset.ownerId')) + .$if(!!viewingUserId, (qb) => qb.where('person.ownerId', '=', viewingUserId!)) + .as('person'), + (join) => join.onTrue(), + ) + .selectAll('asset_face') + .select((eb) => eb.table('person').$castTo>().as('person')) + .whereRef('asset_face.assetId', '=', 'asset.id') + .$if(!withDeletedFace, (qb) => qb.where('asset_face.deletedAt', 'is', null)) + .$if(!withHidden, (qb) => qb.where('asset_face.isVisible', 'is', true)), + ).as('faces'); } -export function hasPeople(qb: SelectQueryBuilder, personIds: string[]) { +export function hasPeople(qb: SelectQueryBuilder, personGroupIds: string[]) { return qb.innerJoin( (eb) => eb .selectFrom('asset_face') .select('assetId') - .where('personId', '=', anyUuid(personIds!)) + .where('personGroupId', '=', anyUuid(personGroupIds!)) .where('deletedAt', 'is', null) .where('isVisible', 'is', true) .groupBy('assetId') - .having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length) + .having((eb) => eb.fn.count('personGroupId').distinct(), '=', personGroupIds.length) .as('has_people'), (join) => join.onRef('has_people.assetId', '=', 'asset.id'), ); } +export function inSharedAlbum(eb: ExpressionBuilder, userId: string) { + return eb.exists( + eb + .selectFrom('album_asset') + .select(sql.lit(1).as('exists')) + .innerJoin('album', (join) => + join.onRef('album.id', '=', 'album_asset.albumId').on('album.deletedAt', 'is', null), + ) + .innerJoin('album_user', (join) => + join.onRef('album_user.albumId', '=', 'album.id').on('album_user.userId', '=', asUuid(userId)), + ) + .whereRef('album_asset.assetId', '=', 'asset.id'), + ); +} + export function inAlbums(qb: SelectQueryBuilder, albumIds: string[]) { return qb.innerJoin( (eb) => @@ -509,7 +534,9 @@ export function searchAssetBuilderLegacy(kysely: Kysely, options: AssetSearc ) .$if(options.withStacked === false, (qb) => qb.where('asset.stackId', 'is', null)) .$if(!!options.withExif, withExifInner) - .$if(!!(options.withFaces || options.withPeople), (qb) => qb.select(withFacesAndPeople)) + .$if(!!(options.withFaces || options.withPeople), (qb) => + qb.select(withFacesAndPeople({ viewingUserId: options.viewingUserId! })), + ) .$if(!options.withDeleted, (qb) => qb.where('asset.deletedAt', 'is', null)); } @@ -567,7 +594,7 @@ function albumIdsPredicates(eb: AssetExpressionBuilder, filter?: IdsFilter) { } function personIdsPredicates(eb: AssetExpressionBuilder, filter?: IdsFilter) { - const matching = (ids: string[]) => visibleFaces(eb).where('asset_face.personId', '=', anyUuid(ids)); + const matching = (ids: string[]) => visibleFaces(eb).where('asset_face.personGroupId', '=', anyUuid(ids)); return idsPredicates(eb, filter, { matchesAny: (ids) => eb.exists(matching(ids)), matchesAll: (ids) => @@ -575,7 +602,7 @@ function personIdsPredicates(eb: AssetExpressionBuilder, filter?: IdsFilter) { matching(ids) .select('asset_face.assetId') .groupBy('asset_face.assetId') - .having((eb) => eb.fn.count('asset_face.personId').distinct(), '=', ids.length), + .having((eb) => eb.fn.count('asset_face.personGroupId').distinct(), '=', ids.length), ), }); } @@ -773,7 +800,9 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .$if(!!options.userIds && options.userIds.length > 0, (qb) => qb.where('asset.ownerId', '=', anyUuid(options.userIds!)), ) - .$if(!!(options.withFaces || options.withPeople), (qb) => qb.select(withFacesAndPeople)) + .$if(!!(options.withFaces || options.withPeople), (qb) => + qb.select(withFacesAndPeople({ viewingUserId: options.viewingUserId! })), + ) .$if(options.withStacked === false, (qb) => qb.where('asset.stackId', 'is', null)) .where((eb) => { const predicates = branchPredicates(eb, filter); diff --git a/server/src/utils/editor.spec.ts b/server/src/utils/editor.spec.ts index 17db0d9da3f43..ed013f9353c0c 100644 --- a/server/src/utils/editor.spec.ts +++ b/server/src/utils/editor.spec.ts @@ -64,7 +64,7 @@ const createFace = (params: Partial = {}): AssetFace => ({ boundingBoxY2: 200, imageWidth: 1000, imageHeight: 1000, - personId: null, + personGroupId: null, sourceType: SourceType.MachineLearning, person: null, updatedAt: new Date(), diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index 656e5bf441050..49877904ec124 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -1,5 +1,5 @@ import { AUDIO_ENCODER, AV1_LEVELS, CodecLevel, H264_LEVELS, HEVC_LEVELS, SUPPORTED_HWA_CODECS } from 'src/constants'; -import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { ConfigFFmpegDto } from 'src/dtos/config.dto'; import { ColorMatrix, ColorPrimaries, @@ -62,18 +62,18 @@ export const getCodecString = (codec: VideoCodec, width: number, height: number, export class BaseConfig implements VideoCodecSWConfig { readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; protected constructor( - protected config: SystemConfigFFmpegDto, + protected config: ConfigFFmpegDto, protected tune: VideoTuning = { strictGop: false, lowLatency: false }, ) {} - static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { + static create(config: ConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { if (config.accel === TranscodeHardwareAcceleration.Disabled) { return BaseConfig.getSWCodecConfig(config, tune); } return BaseConfig.getHWCodecConfig(config, interfaces, tune); } - private static getSWCodecConfig(config: SystemConfigFFmpegDto, tune?: VideoTuning): VideoCodecSWConfig { + private static getSWCodecConfig(config: ConfigFFmpegDto, tune?: VideoTuning): VideoCodecSWConfig { switch (config.targetVideoCodec) { case VideoCodec.H264: { return new H264Config(config, tune); @@ -93,7 +93,7 @@ export class BaseConfig implements VideoCodecSWConfig { } } - private static getHWCodecConfig(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { + private static getHWCodecConfig(config: ConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { if (!SUPPORTED_HWA_CODECS[config.accel].includes(config.targetVideoCodec)) { throw new Error( `${config.accel.toUpperCase()} acceleration does not support codec '${config.targetVideoCodec.toUpperCase()}'. Supported codecs: ${SUPPORTED_HWA_CODECS[config.accel]}`, @@ -424,7 +424,7 @@ export class BaseHWConfig extends BaseConfig { protected device: string; constructor( - protected config: SystemConfigFFmpegDto, + protected config: ConfigFFmpegDto, protected interfaces: VideoInterfaces, tune?: VideoTuning, ) { @@ -471,7 +471,7 @@ export class BaseHWConfig extends BaseConfig { } export class ThumbnailConfig extends BaseConfig { - static create(config: SystemConfigFFmpegDto): VideoCodecSWConfig { + static create(config: ConfigFFmpegDto): VideoCodecSWConfig { return new ThumbnailConfig(config); } diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index a514007987620..84ebc7e980a65 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -13,9 +13,9 @@ import { writeFileSync } from 'node:fs'; import path from 'node:path'; import picomatch from 'picomatch'; import parse from 'picomatch/lib/parse'; -import { SystemConfig } from 'src/config'; import { CLIP_MODEL_INFO, JOBS_ASSET_PAGINATION_SIZE, endpointTags, serverVersion } from 'src/constants'; import { extraModels } from 'src/decorators'; +import { SystemConfig } from 'src/dtos/config.dto'; import { ApiCustomExtension, ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; diff --git a/server/src/utils/profile-image.ts b/server/src/utils/profile-image.ts index ee94dd898625a..ea2c0d3b6b063 100644 --- a/server/src/utils/profile-image.ts +++ b/server/src/utils/profile-image.ts @@ -1,6 +1,6 @@ import { join } from 'node:path'; -import { SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; +import { SystemConfig } from 'src/dtos/config.dto'; import { StorageFolder } from 'src/enum'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { MediaRepository } from 'src/repositories/media.repository'; diff --git a/server/test/factories/asset-face.factory.ts b/server/test/factories/asset-face.factory.ts index b2286cad546c2..c24e543afe49d 100644 --- a/server/test/factories/asset-face.factory.ts +++ b/server/test/factories/asset-face.factory.ts @@ -27,7 +27,7 @@ export class AssetFaceFactory { imageHeight: 500, imageWidth: 400, isVisible: true, - personId: null, + personGroupId: null, sourceType: SourceType.MachineLearning, updatedAt: newDate(), updateId: newUuidV7(), @@ -37,7 +37,7 @@ export class AssetFaceFactory { person(dto: PersonLike = {}, builder?: FactoryBuilder) { this.#person = build(PersonFactory.from(dto), builder); - this.value.personId = this.#person.build().id; + this.value.personGroupId = this.#person.build().personGroupId; return this; } diff --git a/server/test/factories/cluster-group.factory.ts b/server/test/factories/cluster-group.factory.ts new file mode 100644 index 0000000000000..21a39bfd7317d --- /dev/null +++ b/server/test/factories/cluster-group.factory.ts @@ -0,0 +1,27 @@ +import { Selectable } from 'kysely'; +import { ClusterGroupTable } from 'src/schema/tables/cluster-group.table'; +import { ClusterGroupLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class ClusterGroupFactory { + private constructor(private readonly value: Selectable) {} + + static create(dto: ClusterGroupLike = {}) { + return ClusterGroupFactory.from(dto).build(); + } + + static from(dto: ClusterGroupLike = {}) { + return new ClusterGroupFactory({ + id: newUuid(), + name: null, + createdAt: newDate(), + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + build() { + return { ...this.value }; + } +} diff --git a/server/test/factories/person-group.factory.ts b/server/test/factories/person-group.factory.ts new file mode 100644 index 0000000000000..436f777f67e5c --- /dev/null +++ b/server/test/factories/person-group.factory.ts @@ -0,0 +1,28 @@ +import { Selectable } from 'kysely'; +import { PersonGroupTable } from 'src/schema/tables/person-group.table'; +import { PersonGroupLike } from 'test/factories/types'; +import { newDate, newUuid, newUuidV7 } from 'test/small.factory'; + +export class PersonGroupFactory { + private constructor(private readonly value: Selectable) {} + + static create(dto: PersonGroupLike = {}) { + return PersonGroupFactory.from(dto).build(); + } + + static from(dto: PersonGroupLike = {}) { + return new PersonGroupFactory({ + id: newUuid(), + clusterGroupId: newUuid(), + createdAt: newDate(), + createId: newUuidV7(), + updatedAt: newDate(), + updateId: newUuidV7(), + ...dto, + }); + } + + build() { + return { ...this.value }; + } +} diff --git a/server/test/factories/person.factory.ts b/server/test/factories/person.factory.ts index 8e016e539859f..84b0cf5d9723d 100644 --- a/server/test/factories/person.factory.ts +++ b/server/test/factories/person.factory.ts @@ -16,7 +16,7 @@ export class PersonFactory { color: null, createdAt: newDate(), faceAssetId: null, - id: newUuid(), + personGroupId: newUuid(), isFavorite: false, isHidden: false, name: 'person', diff --git a/server/test/factories/types.ts b/server/test/factories/types.ts index 5c8c8ee2c2754..a227deef8f9cb 100644 --- a/server/test/factories/types.ts +++ b/server/test/factories/types.ts @@ -9,8 +9,10 @@ import { AssetExifTable } from 'src/schema/tables/asset-exif.table'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { AssetFileTable } from 'src/schema/tables/asset-file.table'; import { AssetTable } from 'src/schema/tables/asset.table'; +import { ClusterGroupTable } from 'src/schema/tables/cluster-group.table'; import { MemoryTable } from 'src/schema/tables/memory.table'; import { PartnerTable } from 'src/schema/tables/partner.table'; +import { PersonGroupTable } from 'src/schema/tables/person-group.table'; import { PersonTable } from 'src/schema/tables/person.table'; import { SessionTable } from 'src/schema/tables/session.table'; import { SharedLinkTable } from 'src/schema/tables/shared-link.table'; @@ -29,6 +31,8 @@ export type SharedLinkLike = Partial>; export type UserLike = Partial>; export type AssetFaceLike = Partial>; export type PersonLike = Partial>; +export type PersonGroupLike = Partial>; +export type ClusterGroupLike = Partial>; export type StackLike = Partial>; export type MemoryLike = Partial>; export type PartnerLike = Partial>; diff --git a/server/test/factories/user.factory.ts b/server/test/factories/user.factory.ts index 125ce91e86eb1..df5366c9bfb09 100644 --- a/server/test/factories/user.factory.ts +++ b/server/test/factories/user.factory.ts @@ -17,6 +17,7 @@ export class UserFactory { static from(dto: UserLike = {}) { return new UserFactory({ id: newUuid(), + clusterGroupId: newUuid(), email: 'test@immich.cloud', password: '', pinCode: null, diff --git a/server/test/fixtures/system-config.stub.ts b/server/test/fixtures/system-config.stub.ts index 355f2cc1a39c3..6ba65569210e0 100644 --- a/server/test/fixtures/system-config.stub.ts +++ b/server/test/fixtures/system-config.stub.ts @@ -1,4 +1,4 @@ -import { SystemConfig } from 'src/config'; +import { SystemConfig } from 'src/dtos/config.dto'; import { DeepPartial } from 'src/types'; export const systemConfigStub = { diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index 21b49ab8991f1..f6d3a42bde569 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -6,6 +6,7 @@ export const userStub = { admin: { ...authStub.admin.user, status: UserStatus.Active, + clusterGroupId: 'cluster-group-id', profileChangedAt: new Date('2021-01-01'), name: 'admin_name', id: 'admin_id', @@ -24,6 +25,7 @@ export const userStub = { user1: { ...authStub.user1.user, status: UserStatus.Active, + clusterGroupId: 'cluster-group-id', profileChangedAt: new Date('2021-01-01'), name: 'immich_name', storageLabel: null, diff --git a/server/test/mappers.ts b/server/test/mappers.ts index 40ae78fe26fe6..f2694f184f63f 100644 --- a/server/test/mappers.ts +++ b/server/test/mappers.ts @@ -1,6 +1,7 @@ import { Selectable, ShallowDehydrateObject } from 'kysely'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; +import { FaceSearchResult } from 'src/repositories/search.repository'; import { ActivityTable } from 'src/schema/tables/activity.table'; import { AssetTable } from 'src/schema/tables/asset.table'; import { PartnerTable } from 'src/schema/tables/partner.table'; @@ -12,6 +13,7 @@ import { MemoryFactory } from 'test/factories/memory.factory'; import { SharedLinkFactory } from 'test/factories/shared-link.factory'; import { StackFactory } from 'test/factories/stack.factory'; import { UserFactory } from 'test/factories/user.factory'; +import { newUuid } from 'test/small.factory'; export const getForStorageTemplate = (asset: ReturnType) => { return { @@ -54,15 +56,27 @@ export const getAsDetectedFace = (face: ReturnType) = export const getForFacialRecognitionJob = ( face: ReturnType, - asset: Pick, 'ownerId' | 'visibility' | 'fileCreatedAt'> | null, + asset: + (Pick, 'ownerId' | 'visibility' | 'fileCreatedAt'> & { clusterGroupId?: string }) | null, ) => ({ ...face, asset: asset - ? { ownerId: asset.ownerId, visibility: asset.visibility, fileCreatedAt: asset.fileCreatedAt.toISOString() } + ? { + ownerId: asset.ownerId, + clusterGroupId: asset.clusterGroupId ?? newUuid(), + visibility: asset.visibility, + fileCreatedAt: asset.fileCreatedAt.toISOString(), + } : null, faceSearch: { faceId: face.id, embedding: '[1, 2, 3, 4]' }, }); +export const getForFaceSearch = (face: ReturnType, distance: number): FaceSearchResult => ({ + id: face.id, + personGroupId: face.personGroupId, + distance, +}); + export const getDehydrated = >(entity: T) => { const copiedEntity = structuredClone(entity); for (const [key, value] of Object.entries(copiedEntity)) { @@ -122,8 +136,12 @@ export const getForMemory = (memory: ReturnType) => ({ assets: memory.assets.map((asset) => getDehydrated(asset)), }); -export const getForMetadataExtraction = (asset: ReturnType) => ({ +export const getForMetadataExtraction = ( + asset: ReturnType, + { clusterGroupId }: { clusterGroupId?: string } = {}, +) => ({ id: asset.id, + clusterGroupId: clusterGroupId ?? newUuid(), checksum: asset.checksum, checksumAlgorithm: asset.checksumAlgorithm, fileCreatedAt: asset.fileCreatedAt, diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 1fbe41d964e69..c66e9d4a792b7 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -4,9 +4,9 @@ import { createHash, randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { resolve } from 'node:path'; import { Writable } from 'node:stream'; -import { SystemConfig } from 'src/config'; import { AssetFace } from 'src/database'; import { AuthDto, LoginResponseDto } from 'src/dtos/auth.dto'; +import { SystemConfig } from 'src/dtos/config.dto'; import { AssetEditActionItem, AssetEditsCreateDto } from 'src/dtos/editing.dto'; import { AlbumUserRole, @@ -26,6 +26,7 @@ import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; +import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -166,7 +167,8 @@ export class MediumTestContext = } async newUser(dto: Partial> = {}) { - const user = mediumFactory.userInsert(dto); + const clusterGroup = dto.clusterGroupId ? undefined : await this.get(ClusterGroupRepository).create(); + const user = mediumFactory.userInsert({ ...dto, clusterGroupId: dto.clusterGroupId ?? clusterGroup!.id }); const result = await this.get(UserRepository).create(user); return { user, result }; } @@ -266,8 +268,14 @@ export class MediumTestContext = } async newPerson(dto: Partial> & { ownerId: string }) { - const person = mediumFactory.personInsert(dto); - const result = await this.get(PersonRepository).create(person); + const repository = this.get(PersonRepository); + let personGroupId = dto.personGroupId; + if (!personGroupId) { + const group = await repository.createGroup(dto.ownerId); + personGroupId = group.id; + } + const person = mediumFactory.personInsert({ ...dto, personGroupId }); + const result = await repository.create(person); return { person, result }; } @@ -449,6 +457,7 @@ const newRealRepository = (key: T, db: Kysely case AssetRepository: case AssetEditRepository: case AssetJobRepository: + case ClusterGroupRepository: case DuplicateRepository: case IntegrityRepository: case MemoryRepository: @@ -655,7 +664,7 @@ const assetFaceInsert = (assetFace: Partial & { assetId: string }) => id: assetFace.id ?? newUuid(), imageHeight: assetFace.imageHeight ?? 10, imageWidth: assetFace.imageWidth ?? 10, - personId: assetFace.personId ?? null, + personGroupId: assetFace.personGroupId ?? null, sourceType: assetFace.sourceType ?? SourceType.MachineLearning, isVisible: assetFace.isVisible ?? true, }; @@ -682,13 +691,12 @@ const assetJobStatusInsert = ( }; }; -const personInsert = (person: Partial> & { ownerId: string }) => { +const personInsert = (person: Partial> & { ownerId: string; personGroupId: string }) => { const defaults = { birthDate: person.birthDate || null, color: person.color || null, createdAt: person.createdAt || newDate(), faceAssetId: person.faceAssetId || null, - id: person.id || newUuid(), isFavorite: person.isFavorite || false, isHidden: person.isHidden || false, name: person.name || 'Test Name', @@ -722,7 +730,7 @@ const sessionInsert = ({ }; }; -const userInsert = (user: Partial> = {}) => { +const userInsert = (user: Partial> & { clusterGroupId: string }) => { const id = user.id || newUuid(); const defaults = { @@ -811,7 +819,7 @@ const loginDetails = () => { }; const loginResponse = (): LoginResponseDto => { - const user = userInsert({}); + const user = userInsert({ clusterGroupId: newUuid() }); return { accessToken: 'access-token', userId: user.id, diff --git a/server/test/medium/specs/repositories/person.repository.spec.ts b/server/test/medium/specs/repositories/person.repository.spec.ts index 1ff9ade1d115f..4d1604b6018fb 100644 --- a/server/test/medium/specs/repositories/person.repository.spec.ts +++ b/server/test/medium/specs/repositories/person.repository.spec.ts @@ -23,6 +23,161 @@ beforeAll(async () => { }); describe(PersonRepository.name, () => { + describe('createAll', () => { + it('should create people in the groups they were given', async () => { + const { ctx, sut } = setup(); + const [{ user: user1 }, { user: user2 }] = [await ctx.newUser(), await ctx.newUser()]; + + const [group1, group2] = await sut.createGroups([ + { clusterGroupId: user1.clusterGroupId }, + { clusterGroupId: user1.clusterGroupId }, + ]); + const group3 = await sut.createGroup(user2.id); + + const people = await sut.createAll([ + { ownerId: user1.id, name: 'Alice', personGroupId: group1.id }, + { ownerId: user1.id, name: 'Bob', personGroupId: group2.id }, + { ownerId: user2.id, name: 'Carol', personGroupId: group3.id }, + ]); + + expect(people.map(({ personGroupId }) => personGroupId)).toEqual([group1.id, group2.id, group3.id]); + + const groups = await ctx.database + .selectFrom('person') + .innerJoin('person_group', 'person_group.id', 'person.personGroupId') + .innerJoin('user', 'user.id', 'person.ownerId') + .select(['person.name', 'person_group.clusterGroupId', 'user.clusterGroupId as ownerClusterGroupId']) + .where( + 'person.personGroupId', + 'in', + people.map(({ personGroupId }) => personGroupId), + ) + .execute(); + + expect(groups).toHaveLength(3); + for (const group of groups) { + expect(group.clusterGroupId).toBe(group.ownerClusterGroupId); + } + }); + }); + + describe('createGroup', () => { + it('should create a group in the owner cluster group', async () => { + const { ctx, sut } = setup(); + const { user } = await ctx.newUser(); + + const group = await sut.createGroup(user.id); + + const owner = await ctx.database + .selectFrom('person_group') + .innerJoin('user', 'user.clusterGroupId', 'person_group.clusterGroupId') + .select('user.id') + .where('person_group.id', '=', group.id) + .executeTakeFirstOrThrow(); + + expect(owner.id).toBe(user.id); + }); + + it('should put people created with the same group into that group', async () => { + const { ctx, sut } = setup(await getKyselyDB()); + const [{ user: user1 }, { user: user2 }] = [await ctx.newUser(), await ctx.newUser()]; + + const group = await sut.createGroup(user1.id); + const person1 = await sut.create({ ownerId: user1.id, name: 'Alice', personGroupId: group.id }); + const person2 = await sut.create({ ownerId: user2.id, name: 'Alice', personGroupId: group.id }); + + expect(person1.personGroupId).toBe(group.id); + expect(person2.personGroupId).toBe(group.id); + + const groups = await ctx.database.selectFrom('person_group').select('person_group.id').execute(); + expect(groups.map(({ id }) => id)).toEqual([group.id]); + }); + }); + + describe('getByGroupId', () => { + it('should not return a person owned by another user', async () => { + const { ctx, sut } = setup(); + const [{ user: user1 }, { user: user2 }] = [await ctx.newUser(), await ctx.newUser()]; + const group = await sut.createGroup(user1.id); + + const person1 = await sut.create({ ownerId: user1.id, name: 'Alice', personGroupId: group.id }); + const person2 = await ctx.database + .insertInto('person') + .values({ ownerId: user2.id, name: 'Alice', personGroupId: person1.personGroupId }) + .returningAll() + .executeTakeFirstOrThrow(); + + await expect(sut.getByGroupId({ ownerId: user1.id, personGroupId: person1.personGroupId })).resolves.toEqual( + expect.objectContaining({ personGroupId: person1.personGroupId, ownerId: user1.id }), + ); + await expect(sut.getByGroupId({ ownerId: user2.id, personGroupId: person1.personGroupId })).resolves.toEqual( + expect.objectContaining({ personGroupId: person2.personGroupId, ownerId: user2.id }), + ); + }); + + it('should return nothing when the group belongs to another user', async () => { + const { ctx, sut } = setup(); + const [{ user: user1 }, { user: user2 }] = [await ctx.newUser(), await ctx.newUser()]; + const group = await sut.createGroup(user1.id); + + const person = await sut.create({ ownerId: user1.id, name: 'Alice', personGroupId: group.id }); + + await expect( + sut.getByGroupId({ ownerId: user2.id, personGroupId: person.personGroupId }), + ).resolves.toBeUndefined(); + }); + }); + + describe('deleteEmptyGroups', () => { + it('should delete groups that no longer have any people', async () => { + const { ctx, sut } = setup(await getKyselyDB()); + const { user } = await ctx.newUser(); + const [keptGroup, emptiedGroup] = await sut.createGroups([ + { clusterGroupId: user.clusterGroupId }, + { clusterGroupId: user.clusterGroupId }, + ]); + + const kept = await sut.create({ ownerId: user.id, name: 'Alice', personGroupId: keptGroup.id }); + const emptied = await sut.create({ ownerId: user.id, name: 'Bob', personGroupId: emptiedGroup.id }); + await ctx.database + .deleteFrom('person') + .where('person.ownerId', '=', emptied.ownerId) + .where('person.personGroupId', '=', emptied.personGroupId) + .execute(); + + await expect(sut.deleteEmptyGroups()).resolves.toBe(1); + + const groups = await ctx.database.selectFrom('person_group').select('person_group.id').execute(); + expect(groups.map(({ id }) => id)).toEqual([kept.personGroupId]); + }); + }); + + describe('deleteOrphanedClusterGroups', () => { + it('should delete cluster groups that no longer belong to a user, along with their people', async () => { + const { ctx, sut } = setup(await getKyselyDB()); + const [{ user: kept }, { user: removed }] = [await ctx.newUser(), await ctx.newUser()]; + const keptGroup = await sut.createGroup(kept.id); + const removedGroup = await sut.createGroup(removed.id); + + const keptPerson = await sut.create({ ownerId: kept.id, name: 'Alice', personGroupId: keptGroup.id }); + await sut.create({ ownerId: removed.id, name: 'Bob', personGroupId: removedGroup.id }); + const { clusterGroupId } = await ctx.database + .selectFrom('user') + .select('user.clusterGroupId') + .where('user.id', '=', kept.id) + .executeTakeFirstOrThrow(); + await ctx.database.deleteFrom('user').where('user.id', '=', removed.id).execute(); + + await expect(sut.deleteOrphanedClusterGroups()).resolves.toBe(1); + + const clusterGroups = await ctx.database.selectFrom('cluster_group').select('cluster_group.id').execute(); + expect(clusterGroups.map(({ id }) => id)).toEqual([clusterGroupId]); + + const groups = await ctx.database.selectFrom('person_group').select('person_group.id').execute(); + expect(groups.map(({ id }) => id)).toEqual([keptPerson.personGroupId]); + }); + }); + describe('getDataForThumbnailGenerationJob', () => { it('should not return the edited preview path', async () => { const { ctx, sut } = setup(); @@ -33,7 +188,7 @@ describe(PersonRepository.name, () => { const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, - personId: person.id, + personGroupId: person.personGroupId, boundingBoxX1: 10, boundingBoxY1: 10, boundingBoxX2: 90, @@ -41,7 +196,12 @@ describe(PersonRepository.name, () => { }); // there's a circular dependency between assetFace and person, so we need to update the person after creating the assetFace - await ctx.database.updateTable('person').set({ faceAssetId: assetFace.id }).where('id', '=', person.id).execute(); + await ctx.database + .updateTable('person') + .set({ faceAssetId: assetFace.id }) + .where('ownerId', '=', person.ownerId) + .where('personGroupId', '=', person.personGroupId) + .execute(); await ctx.newAssetFile({ assetId: asset.id, @@ -56,7 +216,10 @@ describe(PersonRepository.name, () => { isEdited: false, }); - const result = await sut.getDataForThumbnailGenerationJob(person.id); + const result = await sut.getDataForThumbnailGenerationJob({ + ownerId: person.ownerId, + personGroupId: person.personGroupId, + }); expect(result).toEqual( expect.objectContaining({ diff --git a/server/test/medium/specs/services/auth.service.spec.ts b/server/test/medium/specs/services/auth.service.spec.ts index 5e5c880955b68..af969bae9ab13 100644 --- a/server/test/medium/specs/services/auth.service.spec.ts +++ b/server/test/medium/specs/services/auth.service.spec.ts @@ -3,6 +3,7 @@ import { hash } from 'bcrypt'; import { Kysely } from 'kysely'; import { AuthType } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; +import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; @@ -26,6 +27,7 @@ const setup = (db?: Kysely) => { database: db || defaultDatabase, real: [ AccessRepository, + ClusterGroupRepository, ConfigRepository, CryptoRepository, DatabaseRepository, diff --git a/server/test/medium/specs/services/cluster-group.service.spec.ts b/server/test/medium/specs/services/cluster-group.service.spec.ts new file mode 100644 index 0000000000000..2192e89e6083b --- /dev/null +++ b/server/test/medium/specs/services/cluster-group.service.spec.ts @@ -0,0 +1,475 @@ +import { Kysely } from 'kysely'; +import { AccessRepository } from 'src/repositories/access.repository'; +import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository'; +import { EventRepository } from 'src/repositories/event.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { PersonRepository } from 'src/repositories/person.repository'; +import { UserRepository } from 'src/repositories/user.repository'; +import { DB } from 'src/schema'; +import { ClusterGroupService } from 'src/services/cluster-group.service'; +import { newMediumService } from 'test/medium.factory'; +import { factory } from 'test/small.factory'; +import { getKyselyDB } from 'test/utils'; + +let defaultDatabase: Kysely; + +const setup = (db?: Kysely) => { + const ctx = newMediumService(ClusterGroupService, { + database: db || defaultDatabase, + real: [AccessRepository, ClusterGroupRepository, PersonRepository, UserRepository], + mock: [LoggingRepository, EventRepository], + }); + + ctx.ctx.getMock(EventRepository).emit.mockResolvedValue(); + + return ctx; +}; + +const getClusterGroupId = async (ctx: ReturnType['ctx'], userId: string) => { + const { clusterGroupId } = await ctx.database + .selectFrom('user') + .select('user.clusterGroupId') + .where('user.id', '=', userId) + .executeTakeFirstOrThrow(); + + return clusterGroupId; +}; + +const getPeople = (ctx: ReturnType['ctx'], ownerId: string) => + ctx.database.selectFrom('person').selectAll('person').where('person.ownerId', '=', ownerId).execute(); + +beforeAll(async () => { + defaultDatabase = await getKyselyDB(); +}); + +describe(ClusterGroupService.name, () => { + describe('createRequest', () => { + it('should create a request for another user', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const auth = factory.auth({ user: owner }); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + const { value: request } = await sut.createRequest(auth, clusterGroupId, { userId: invitee.id }); + + expect(request).toEqual( + expect.objectContaining({ clusterGroupId, userId: invitee.id, createdAt: expect.any(Date) }), + ); + expect(ctx.getMock(EventRepository).emit).toHaveBeenCalledWith('ClusterGroupRequest', { + clusterGroupId, + userId: invitee.id, + senderName: owner.name, + }); + }); + + it('should reject a cluster group the user is not a member of', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: other } = await ctx.newUser(); + const auth = factory.auth({ user: owner }); + const otherClusterGroupId = await getClusterGroupId(ctx, other.id); + + await expect(sut.createRequest(auth, otherClusterGroupId, { userId: other.id })).rejects.toThrow( + 'Not found or no clusterGroupRequest.create access', + ); + }); + + it('should return the existing request when it was already created', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const auth = factory.auth({ user: owner }); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + const created = await sut.createRequest(auth, clusterGroupId, { userId: invitee.id }); + expect(created.duplicate).toBe(false); + + const again = await sut.createRequest(auth, clusterGroupId, { userId: invitee.id }); + expect(again.duplicate).toBe(true); + expect(again.value).toEqual(created.value); + + await expect(sut.getRequests(factory.auth({ user: invitee }))).resolves.toEqual([created.value]); + }); + + it('should reject an unknown user', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const auth = factory.auth({ user: owner }); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + await expect(sut.createRequest(auth, clusterGroupId, { userId: factory.uuid() })).rejects.toThrow( + 'User not found', + ); + }); + }); + + describe('getRequests', () => { + it('should only return the requests for the current user', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const { user: other } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + const { value: request } = await sut.createRequest(factory.auth({ user: owner }), clusterGroupId, { + userId: invitee.id, + }); + await sut.createRequest(factory.auth({ user: owner }), clusterGroupId, { userId: other.id }); + + await expect(sut.getRequests(factory.auth({ user: invitee }))).resolves.toEqual([request]); + }); + }); + + describe('getRequestsForGroup', () => { + it('should return the requests sent by the cluster group', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const { user: other } = await ctx.newUser(); + const auth = factory.auth({ user: owner }); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + const { value: request } = await sut.createRequest(auth, clusterGroupId, { userId: invitee.id }); + + await expect(sut.getRequestsForGroup(auth, clusterGroupId)).resolves.toEqual([request]); + await expect(sut.getRequestsForGroup(factory.auth({ user: other }), clusterGroupId)).rejects.toThrow( + 'Not found or no clusterGroup.read access', + ); + }); + }); + + describe('getUsers', () => { + it('should return the members of the cluster group', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: member } = await ctx.newUser(); + const { user: other } = await ctx.newUser(); + const auth = factory.auth({ user: owner }); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + const { value: request } = await sut.createRequest(auth, clusterGroupId, { userId: member.id }); + await sut.acceptRequest(factory.auth({ user: member }), request.id); + + const users = await sut.getUsers(auth, clusterGroupId); + expect(users.map(({ id }) => id)).toEqual(expect.arrayContaining([owner.id, member.id])); + expect(users.map(({ id }) => id)).not.toContain(other.id); + + await expect(sut.getUsers(factory.auth({ user: other }), clusterGroupId)).rejects.toThrow( + 'Not found or no clusterGroup.read access', + ); + }); + + it('should let a user with a pending request see the members', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const auth = factory.auth({ user: owner }); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + await expect(sut.getUsers(factory.auth({ user: invitee }), clusterGroupId)).rejects.toThrow( + 'Not found or no clusterGroup.read access', + ); + + await sut.createRequest(auth, clusterGroupId, { userId: invitee.id }); + + const users = await sut.getUsers(factory.auth({ user: invitee }), clusterGroupId); + expect(users.map(({ id }) => id)).toContain(owner.id); + }); + }); + + describe('acceptRequest', () => { + it('should move the user into the cluster group and delete the request', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + const { value: request } = await sut.createRequest(factory.auth({ user: owner }), clusterGroupId, { + userId: invitee.id, + }); + await sut.acceptRequest(factory.auth({ user: invitee }), request.id); + + await expect(getClusterGroupId(ctx, invitee.id)).resolves.toBe(clusterGroupId); + await expect(sut.getRequests(factory.auth({ user: invitee }))).resolves.toEqual([]); + }); + + it('should not accept a request belonging to someone else', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const { user: other } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + const otherClusterGroupId = await getClusterGroupId(ctx, other.id); + + const { value: request } = await sut.createRequest(factory.auth({ user: owner }), clusterGroupId, { + userId: invitee.id, + }); + + await expect(sut.acceptRequest(factory.auth({ user: other }), request.id)).rejects.toThrow( + 'Not found or no clusterGroupRequest.read access', + ); + await expect(getClusterGroupId(ctx, other.id)).resolves.toBe(otherClusterGroupId); + }); + }); + + describe('leave', () => { + it('should move the user into a new cluster group', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + const auth = factory.auth({ user }); + + const { value: request } = await sut.createRequest(factory.auth({ user: owner }), clusterGroupId, { + userId: user.id, + }); + await sut.acceptRequest(auth, request.id); + + await sut.leave(auth, clusterGroupId); + + const newClusterGroupId = await getClusterGroupId(ctx, user.id); + expect(newClusterGroupId).not.toBe(clusterGroupId); + await expect( + ctx.database + .selectFrom('cluster_group') + .select('cluster_group.id') + .where('cluster_group.id', '=', newClusterGroupId) + .executeTakeFirst(), + ).resolves.toBeDefined(); + }); + + it('should reject a cluster group the user is not a member of', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const { user: other } = await ctx.newUser(); + const auth = factory.auth({ user }); + const otherClusterGroupId = await getClusterGroupId(ctx, other.id); + + await expect(sut.leave(auth, otherClusterGroupId)).rejects.toThrow('Not found or no clusterGroup.leave access'); + }); + + it('should not let the last member leave', async () => { + const { sut, ctx } = setup(); + const { user } = await ctx.newUser(); + const auth = factory.auth({ user }); + const clusterGroupId = await getClusterGroupId(ctx, user.id); + + await expect(sut.leave(auth, clusterGroupId)).rejects.toThrow( + 'Cannot leave a cluster group without any other members', + ); + await expect(getClusterGroupId(ctx, user.id)).resolves.toBe(clusterGroupId); + }); + }); + + describe('joining a cluster group', () => { + it('should take the groups of the user along', async () => { + const { sut, ctx } = setup(await getKyselyDB()); + const personRepo = ctx.get(PersonRepository); + const { user: owner } = await ctx.newUser(); + const { user } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + const { person } = await ctx.newPerson({ ownerId: user.id }); + const { asset } = await ctx.newAsset({ ownerId: user.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId }); + + const { value: request } = await sut.createRequest(factory.auth({ user: owner }), clusterGroupId, { + userId: user.id, + }); + await sut.acceptRequest(factory.auth({ user }), request.id); + + // the group keeps its id, it only changes cluster group + await expect(personRepo.getByGroupId(person)).resolves.toEqual( + expect.objectContaining({ personGroupId: person.personGroupId }), + ); + await expect( + ctx.database + .selectFrom('person_group') + .select('person_group.clusterGroupId') + .where('person_group.id', '=', person.personGroupId) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ clusterGroupId }); + await expect( + ctx.database + .selectFrom('asset_face') + .select('asset_face.personGroupId') + .where('asset_face.id', '=', assetFace.id) + .executeTakeFirstOrThrow(), + ).resolves.toEqual({ personGroupId: person.personGroupId }); + await expect(getClusterGroupId(ctx, user.id)).resolves.toBe(clusterGroupId); + }); + }); + + describe('leaving a shared cluster group', () => { + it('should recreate the shared groups and take the rest of its own along', async () => { + const { sut, ctx } = setup(await getKyselyDB()); + const personRepo = ctx.get(PersonRepository); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, user1.id); + + const { value: request } = await sut.createRequest(factory.auth({ user: user1 }), clusterGroupId, { + userId: user2.id, + }); + await sut.acceptRequest(factory.auth({ user: user2 }), request.id); + + // a group both of them have a person in + const { person: shared1 } = await ctx.newPerson({ ownerId: user1.id }); + await ctx.newPerson({ ownerId: user2.id, personGroupId: shared1.personGroupId }); + // a group only the leaving user has a person in + const { person: only2 } = await ctx.newPerson({ ownerId: user2.id }); + // a group only the remaining user has a person in + const { person: only1 } = await ctx.newPerson({ ownerId: user1.id }); + + const { asset: asset2 } = await ctx.newAsset({ ownerId: user2.id }); + const { assetFace: sharedFace2 } = await ctx.newAssetFace({ + assetId: asset2.id, + personGroupId: shared1.personGroupId, + }); + const { asset: asset1 } = await ctx.newAsset({ ownerId: user1.id }); + const { assetFace: sharedFace1 } = await ctx.newAssetFace({ + assetId: asset1.id, + personGroupId: shared1.personGroupId, + }); + + await sut.leave(factory.auth({ user: user2 }), clusterGroupId); + + const newClusterGroupId = await getClusterGroupId(ctx, user2.id); + expect(newClusterGroupId).not.toBe(clusterGroupId); + + // the shared group is recreated for the leaving user, the one only they had comes along as it is + const leaverPeople = await getPeople(ctx, user2.id); + const movedShared = leaverPeople.find(({ personGroupId }) => personGroupId !== only2.personGroupId); + expect(movedShared).toBeDefined(); + expect(movedShared!.personGroupId).not.toBe(shared1.personGroupId); + await expect(personRepo.getByGroupId(only2)).resolves.toBeDefined(); + + // the remaining user is untouched + await expect(personRepo.getByGroupId(shared1)).resolves.toBeDefined(); + await expect(personRepo.getByGroupId(only1)).resolves.toBeDefined(); + + const groups = await ctx.database + .selectFrom('person_group') + .select(['person_group.id', 'person_group.clusterGroupId']) + .execute(); + expect(groups).toEqual( + expect.arrayContaining([ + { id: shared1.personGroupId, clusterGroupId }, + { id: only1.personGroupId, clusterGroupId }, + { id: only2.personGroupId, clusterGroupId: newClusterGroupId }, + { id: movedShared!.personGroupId, clusterGroupId: newClusterGroupId }, + ]), + ); + + // only the faces on the assets of the leaving user follow the recreated group + const faces = await ctx.database + .selectFrom('asset_face') + .select(['asset_face.id', 'asset_face.personGroupId']) + .where('asset_face.id', 'in', [sharedFace1.id, sharedFace2.id]) + .execute(); + expect(faces).toEqual( + expect.arrayContaining([ + { id: sharedFace1.id, personGroupId: shared1.personGroupId }, + { id: sharedFace2.id, personGroupId: movedShared!.personGroupId }, + ]), + ); + }); + + it('should give each shared group its own new group when a user leaves the group', async () => { + const { sut, ctx } = setup(await getKyselyDB()); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, user1.id); + + const { value: request } = await sut.createRequest(factory.auth({ user: user1 }), clusterGroupId, { + userId: user2.id, + }); + await sut.acceptRequest(factory.auth({ user: user2 }), request.id); + + // two groups both users have a person in + const { person: sharedA } = await ctx.newPerson({ ownerId: user1.id }); + const { person: sharedB } = await ctx.newPerson({ ownerId: user1.id }); + await ctx.newPerson({ ownerId: user2.id, personGroupId: sharedA.personGroupId, name: 'Alice' }); + await ctx.newPerson({ ownerId: user2.id, personGroupId: sharedB.personGroupId, name: 'Bob' }); + + const { asset } = await ctx.newAsset({ ownerId: user2.id }); + const { assetFace: faceA } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: sharedA.personGroupId }); + const { assetFace: faceB } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: sharedB.personGroupId }); + + await sut.leave(factory.auth({ user: user2 }), clusterGroupId); + + const moved = await getPeople(ctx, user2.id); + const movedGroupIds = moved.map(({ personGroupId }) => personGroupId); + expect(movedGroupIds).toHaveLength(2); + expect(movedGroupIds).not.toContain(sharedA.personGroupId); + expect(movedGroupIds).not.toContain(sharedB.personGroupId); + expect(new Set(movedGroupIds).size).toBe(2); + + // the group id changes on the way out, so the name is what identifies each person + const movedA = moved.find(({ name }) => name === 'Alice'); + const movedB = moved.find(({ name }) => name === 'Bob'); + + const faces = await ctx.database + .selectFrom('asset_face') + .select(['asset_face.id', 'asset_face.personGroupId']) + .where('asset_face.id', 'in', [faceA.id, faceB.id]) + .execute(); + expect(faces).toEqual( + expect.arrayContaining([ + { id: faceA.id, personGroupId: movedA!.personGroupId }, + { id: faceB.id, personGroupId: movedB!.personGroupId }, + ]), + ); + }); + }); + + describe('deleteRequest', () => { + it('should let the user it was created for decline it', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + const inviteeClusterGroupId = await getClusterGroupId(ctx, invitee.id); + + const { value: request } = await sut.createRequest(factory.auth({ user: owner }), clusterGroupId, { + userId: invitee.id, + }); + await sut.deleteRequest(factory.auth({ user: invitee }), request.id); + + await expect(sut.getRequests(factory.auth({ user: invitee }))).resolves.toEqual([]); + await expect(getClusterGroupId(ctx, invitee.id)).resolves.toBe(inviteeClusterGroupId); + }); + + it('should let the cluster group it was created by revoke it', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + const { value: request } = await sut.createRequest(factory.auth({ user: owner }), clusterGroupId, { + userId: invitee.id, + }); + await sut.deleteRequest(factory.auth({ user: owner }), request.id); + + await expect(sut.getRequests(factory.auth({ user: invitee }))).resolves.toEqual([]); + }); + + it('should not let an unrelated user delete it', async () => { + const { sut, ctx } = setup(); + const { user: owner } = await ctx.newUser(); + const { user: invitee } = await ctx.newUser(); + const { user: other } = await ctx.newUser(); + const clusterGroupId = await getClusterGroupId(ctx, owner.id); + + const { value: request } = await sut.createRequest(factory.auth({ user: owner }), clusterGroupId, { + userId: invitee.id, + }); + + await expect(sut.deleteRequest(factory.auth({ user: other }), request.id)).rejects.toThrow( + 'Not found or no clusterGroupRequest.delete access', + ); + await expect(sut.getRequests(factory.auth({ user: invitee }))).resolves.toEqual([request]); + }); + }); +}); diff --git a/server/test/medium/specs/services/person.service.spec.ts b/server/test/medium/specs/services/person.service.spec.ts index 39805580f6ca2..579012d04ca15 100644 --- a/server/test/medium/specs/services/person.service.spec.ts +++ b/server/test/medium/specs/services/person.service.spec.ts @@ -1,4 +1,5 @@ import { Kysely } from 'kysely'; +import { DateTime } from 'luxon'; import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto'; import { AssetFaceCreateDto } from 'src/dtos/person.dto'; import { AccessRepository } from 'src/repositories/access.repository'; @@ -47,9 +48,11 @@ describe(PersonService.name, () => { const auth = factory.auth({ user }); storageMock.unlink.mockResolvedValue(); - await expect(personRepo.getById(person.id)).resolves.toEqual(expect.objectContaining({ id: person.id })); - await expect(sut.delete(auth, person.id)).resolves.toBeUndefined(); - await expect(personRepo.getById(person.id)).resolves.toBeUndefined(); + await expect(personRepo.getByGroupId(person)).resolves.toEqual( + expect.objectContaining({ personGroupId: person.personGroupId }), + ); + await expect(sut.delete(auth, person.personGroupId)).resolves.toBeUndefined(); + await expect(personRepo.getByGroupId(person)).resolves.toBeUndefined(); expect(storageMock.unlink).toHaveBeenCalledWith(person.thumbnailPath); }); @@ -73,9 +76,11 @@ describe(PersonService.name, () => { const auth = factory.auth({ user }); storageMock.unlink.mockResolvedValue(); - await expect(sut.deleteAll(auth, { ids: [person1.id, person2.id] })).resolves.toBeUndefined(); - await expect(personRepo.getById(person1.id)).resolves.toBeUndefined(); - await expect(personRepo.getById(person2.id)).resolves.toBeUndefined(); + await expect( + sut.deleteAll(auth, { ids: [person1.personGroupId, person2.personGroupId] }), + ).resolves.toBeUndefined(); + await expect(personRepo.getByGroupId(person1)).resolves.toBeUndefined(); + await expect(personRepo.getByGroupId(person2)).resolves.toBeUndefined(); expect(storageMock.unlink).toHaveBeenCalledTimes(2); expect(storageMock.unlink).toHaveBeenCalledWith(person1.thumbnailPath); @@ -83,6 +88,100 @@ describe(PersonService.name, () => { }); }); + describe('mergePerson', () => { + it('should merge people of multiple users', async () => { + const { sut, ctx } = setup(); + const storageMock = ctx.getMock(StorageRepository); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser({ clusterGroupId: user1.clusterGroupId }); + const { person: person1 } = await ctx.newPerson({ ownerId: user1.id }); + const { person: person2 } = await ctx.newPerson({ ownerId: user1.id }); + await ctx.newPerson({ + ownerId: user2.id, + personGroupId: person1.personGroupId, + }); + await ctx.newPerson({ + ownerId: user2.id, + personGroupId: person2.personGroupId, + }); + storageMock.unlink.mockResolvedValue(); + + const auth = factory.auth({ user: user1 }); + + await sut.mergePerson(auth, person1.personGroupId, { ids: [person2.personGroupId] }); + const user1People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user1.id })); + const user2People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user2.id })); + expect(user1People).toEqual([expect.objectContaining({ personGroupId: person1.personGroupId })]); + expect(user2People).toEqual([expect.objectContaining({ personGroupId: person1.personGroupId })]); + }); + + it('should skip people with a different name', async () => { + const { sut, ctx } = setup(); + const storageMock = ctx.getMock(StorageRepository); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser({ clusterGroupId: user1.clusterGroupId }); + const { person: person1 } = await ctx.newPerson({ ownerId: user1.id }); + const { person: person2 } = await ctx.newPerson({ ownerId: user1.id }); + await ctx.newPerson({ + ownerId: user2.id, + personGroupId: person1.personGroupId, + name: 'Person 1', + }); + await ctx.newPerson({ + ownerId: user2.id, + personGroupId: person2.personGroupId, + name: 'Person 2', + }); + storageMock.unlink.mockResolvedValue(); + + const auth = factory.auth({ user: user1 }); + + await sut.mergePerson(auth, person1.personGroupId, { ids: [person2.personGroupId] }); + const user1People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user1.id })); + const user2People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user2.id })); + expect(user1People).toEqual([expect.objectContaining({ personGroupId: person1.personGroupId })]); + expect(user2People).toEqual( + expect.arrayContaining([ + expect.objectContaining({ personGroupId: person1.personGroupId }), + expect.objectContaining({ personGroupId: person2.personGroupId }), + ]), + ); + }); + + it('should skip people with a different birthdate', async () => { + const { sut, ctx } = setup(); + const storageMock = ctx.getMock(StorageRepository); + const { user: user1 } = await ctx.newUser(); + const { user: user2 } = await ctx.newUser({ clusterGroupId: user1.clusterGroupId }); + const { person: person1 } = await ctx.newPerson({ ownerId: user1.id }); + const { person: person2 } = await ctx.newPerson({ ownerId: user1.id }); + await ctx.newPerson({ + ownerId: user2.id, + personGroupId: person1.personGroupId, + birthDate: DateTime.now().minus({ years: 1 }).toJSDate(), + }); + await ctx.newPerson({ + ownerId: user2.id, + personGroupId: person2.personGroupId, + birthDate: DateTime.now().minus({ years: 2 }).toJSDate(), + }); + storageMock.unlink.mockResolvedValue(); + + const auth = factory.auth({ user: user1 }); + + await sut.mergePerson(auth, person1.personGroupId, { ids: [person2.personGroupId] }); + const user1People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user1.id })); + const user2People = await Array.fromAsync(ctx.get(PersonRepository).getAll({ ownerId: user2.id })); + expect(user1People).toEqual([expect.objectContaining({ personGroupId: person1.personGroupId })]); + expect(user2People).toEqual( + expect.arrayContaining([ + expect.objectContaining({ personGroupId: person1.personGroupId }), + expect.objectContaining({ personGroupId: person2.personGroupId }), + ]), + ); + }); + }); + describe('createFace', () => { it('should store and retrieve the face as-is when there are no edits', async () => { const { sut, ctx } = setup(); @@ -101,7 +200,7 @@ describe(PersonService.name, () => { y: 50, width: 150, height: 150, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -114,7 +213,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 50, boundingBoxY1: 50, boundingBoxX2: 200, @@ -155,7 +254,7 @@ describe(PersonService.name, () => { y: 0, width: 100, height: 100, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -168,7 +267,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 0, boundingBoxY1: 0, boundingBoxX2: 100, @@ -186,7 +285,7 @@ describe(PersonService.name, () => { await expect(facesAfterRemovingEdits).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 50, boundingBoxY1: 50, boundingBoxX2: 150, @@ -224,7 +323,7 @@ describe(PersonService.name, () => { y: 50, width: 10, height: 10, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -235,7 +334,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: expect.closeTo(25, 1), boundingBoxY1: expect.closeTo(50, 1), boundingBoxX2: expect.closeTo(35, 1), @@ -251,7 +350,7 @@ describe(PersonService.name, () => { await expect(facesAfterRemovingEdits).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 50, boundingBoxY1: 65, boundingBoxX2: 60, @@ -289,7 +388,7 @@ describe(PersonService.name, () => { y: 25, width: 100, height: 50, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -300,7 +399,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 50, boundingBoxY1: 25, boundingBoxX2: 150, @@ -316,7 +415,7 @@ describe(PersonService.name, () => { await expect(facesAfterRemovingEdits).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 50, boundingBoxY1: 25, boundingBoxX2: 150, @@ -363,7 +462,7 @@ describe(PersonService.name, () => { y: 25, width: 10, height: 20, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -374,7 +473,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: expect.closeTo(50, 1), boundingBoxY1: expect.closeTo(25, 1), boundingBoxX2: expect.closeTo(60, 1), @@ -390,7 +489,7 @@ describe(PersonService.name, () => { await expect(facesAfterRemovingEdits).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 75, boundingBoxY1: 140, boundingBoxX2: 95, @@ -437,7 +536,7 @@ describe(PersonService.name, () => { y: 25, width: 75, height: 50, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -448,7 +547,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 25, boundingBoxY1: 25, boundingBoxX2: 100, @@ -464,7 +563,7 @@ describe(PersonService.name, () => { await expect(facesAfterRemovingEdits).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 100, boundingBoxY1: 25, boundingBoxX2: 175, @@ -508,7 +607,7 @@ describe(PersonService.name, () => { y: 25, width: 15, height: 20, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -519,7 +618,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: expect.closeTo(50, 1), boundingBoxY1: expect.closeTo(25, 1), boundingBoxX2: expect.closeTo(65, 1), @@ -535,7 +634,7 @@ describe(PersonService.name, () => { await expect(facesAfterRemovingEdits).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 25, boundingBoxY1: 50, boundingBoxX2: 45, @@ -588,7 +687,7 @@ describe(PersonService.name, () => { y: 50, width: 75, height: 50, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -599,7 +698,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 25, boundingBoxY1: 49, boundingBoxX2: 99, @@ -615,7 +714,7 @@ describe(PersonService.name, () => { await expect(facesAfterRemovingEdits).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 50, boundingBoxY1: 75, boundingBoxX2: 100, @@ -659,7 +758,7 @@ describe(PersonService.name, () => { y: 10, width: 80, height: 80, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -670,7 +769,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 10, boundingBoxY1: 10, boundingBoxX2: 90, @@ -686,7 +785,7 @@ describe(PersonService.name, () => { await expect(facesAfterRemovingEdits).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 10, boundingBoxY1: 10, boundingBoxX2: 90, @@ -730,7 +829,7 @@ describe(PersonService.name, () => { y: 10, width: 80, height: 80, - personId: person.id, + personId: person.personGroupId, assetId: asset.id, }; @@ -741,7 +840,7 @@ describe(PersonService.name, () => { await expect(faces).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 110, boundingBoxY1: 10, boundingBoxX2: 190, @@ -757,7 +856,7 @@ describe(PersonService.name, () => { await expect(facesAfterRemovingEdits).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ - person: expect.objectContaining({ id: person.id }), + person: expect.objectContaining({ id: person.personGroupId }), boundingBoxX1: 10, boundingBoxY1: 10, boundingBoxX2: 90, diff --git a/server/test/medium/specs/services/search.service.spec.ts b/server/test/medium/specs/services/search.service.spec.ts index 044cd8d4f5c7e..c612f45e47440 100644 --- a/server/test/medium/specs/services/search.service.spec.ts +++ b/server/test/medium/specs/services/search.service.spec.ts @@ -69,11 +69,11 @@ describe(SearchService.name, () => { const { user } = await ctx.newUser(); const { asset } = await ctx.newAsset({ ownerId: user.id }); const { person } = await ctx.newPerson({ ownerId: user.id }); - await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId }); const auth = factory.auth({ user: { id: user.id } }); - const result = await sut.searchStatistics(auth, { personIds: [person.id] }); + const result = await sut.searchStatistics(auth, { personIds: [person.personGroupId] }); expect(result).toEqual({ total: 1 }); }); @@ -85,7 +85,7 @@ describe(SearchService.name, () => { const auth = factory.auth({ user: { id: user.id } }); - const result = await sut.searchStatistics(auth, { personIds: [person.id] }); + const result = await sut.searchStatistics(auth, { personIds: [person.personGroupId] }); expect(result).toEqual({ total: 0 }); }); diff --git a/server/test/medium/specs/services/user.service.spec.ts b/server/test/medium/specs/services/user.service.spec.ts index 85ab9a8c0daa4..311a5cb846d33 100644 --- a/server/test/medium/specs/services/user.service.spec.ts +++ b/server/test/medium/specs/services/user.service.spec.ts @@ -1,6 +1,7 @@ import { Kysely } from 'kysely'; import { DateTime } from 'luxon'; import { ImmichEnvironment, JobName, JobStatus, UserAvatarColor } from 'src/enum'; +import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { EventRepository } from 'src/repositories/event.repository'; @@ -12,7 +13,7 @@ import { DB } from 'src/schema'; import { UserService } from 'src/services/user.service'; import { HumanReadableSize } from 'src/utils/bytes'; import { mediumFactory, newMediumService } from 'test/medium.factory'; -import { factory } from 'test/small.factory'; +import { factory, newUuid } from 'test/small.factory'; import { getKyselyDB } from 'test/utils'; const userLicense = { @@ -28,7 +29,7 @@ const setup = (db?: Kysely) => { return newMediumService(UserService, { database: db || defaultDatabase, - real: [CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository], + real: [ClusterGroupRepository, CryptoRepository, ConfigRepository, SystemMetadataRepository, UserRepository], mock: [LoggingRepository, JobRepository, EventRepository], }); }; @@ -44,7 +45,7 @@ describe(UserService.name, () => { it('should create a user', async () => { const { sut, ctx } = setup(); ctx.getMock(EventRepository).emit.mockResolvedValue(); - const user = mediumFactory.userInsert(); + const user = mediumFactory.userInsert({ clusterGroupId: newUuid() }); const created = await sut.createUser({ name: user.name, email: user.email }); expect(created).toEqual(expect.objectContaining({ name: user.name, email: user.email })); @@ -54,7 +55,7 @@ describe(UserService.name, () => { it('should reject user with duplicate email', async () => { const { sut, ctx } = setup(); ctx.getMock(EventRepository).emit.mockResolvedValue(); - const user = mediumFactory.userInsert(); + const user = mediumFactory.userInsert({ clusterGroupId: newUuid() }); await expect(sut.createUser({ name: 'Test', email: user.email })).resolves.toMatchObject({ email: user.email }); await expect(sut.createUser({ name: 'Test', email: user.email })).rejects.toThrow('Email is not available'); }); @@ -62,7 +63,7 @@ describe(UserService.name, () => { it('should not return password', async () => { const { sut, ctx } = setup(); ctx.getMock(EventRepository).emit.mockResolvedValue(); - const dto = mediumFactory.userInsert({ password: 'password' }); + const dto = mediumFactory.userInsert({ clusterGroupId: newUuid(), password: 'password' }); const user = await sut.createUser({ name: 'Test', email: dto.email, password: 'password' }); expect((user as any).password).toBeUndefined(); }); diff --git a/server/test/medium/specs/sync/sync-asset-face.spec.ts b/server/test/medium/specs/sync/sync-asset-face.spec.ts index 74d4c536f114e..6e8e4d7039f1d 100644 --- a/server/test/medium/specs/sync/sync-asset-face.spec.ts +++ b/server/test/medium/specs/sync/sync-asset-face.spec.ts @@ -23,7 +23,7 @@ describe(SyncEntityType.AssetFaceV2, () => { const { auth, ctx } = await setup(); const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); const { person } = await ctx.newPerson({ ownerId: auth.user.id }); - const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId }); const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); expect(response).toEqual([ @@ -32,7 +32,7 @@ describe(SyncEntityType.AssetFaceV2, () => { data: expect.objectContaining({ id: assetFace.id, assetId: asset.id, - personId: person.id, + personId: person.personGroupId, imageWidth: assetFace.imageWidth, imageHeight: assetFace.imageHeight, boundingBoxX1: assetFace.boundingBoxX1, @@ -103,7 +103,7 @@ describe(SyncEntityType.AssetFaceV2, () => { const { auth, ctx } = await setup(); const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); const { person } = await ctx.newPerson({ ownerId: auth.user.id }); - const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId }); const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); expect(response).toEqual([ @@ -112,7 +112,7 @@ describe(SyncEntityType.AssetFaceV2, () => { data: expect.objectContaining({ id: assetFace.id, assetId: asset.id, - personId: person.id, + personId: person.personGroupId, imageWidth: assetFace.imageWidth, imageHeight: assetFace.imageHeight, boundingBoxX1: assetFace.boundingBoxX1, @@ -182,7 +182,7 @@ describe(SyncEntityType.AssetFaceV2, () => { const personRepo = ctx.get(PersonRepository); const { asset } = await ctx.newAsset({ ownerId: auth.user.id }); const { person } = await ctx.newPerson({ ownerId: auth.user.id }); - const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id }); + const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personGroupId: person.personGroupId }); let response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]); expect(response).toEqual([ @@ -191,7 +191,7 @@ describe(SyncEntityType.AssetFaceV2, () => { data: expect.objectContaining({ id: assetFace.id, assetId: asset.id, - personId: person.id, + personId: person.personGroupId, imageWidth: assetFace.imageWidth, imageHeight: assetFace.imageHeight, boundingBoxX1: assetFace.boundingBoxX1, diff --git a/server/test/medium/specs/sync/sync-person.spec.ts b/server/test/medium/specs/sync/sync-person.spec.ts index 6fdb5a58f2d26..bb752344ad6e8 100644 --- a/server/test/medium/specs/sync/sync-person.spec.ts +++ b/server/test/medium/specs/sync/sync-person.spec.ts @@ -28,7 +28,7 @@ describe(SyncEntityType.PersonV1, () => { { ack: expect.any(String), data: expect.objectContaining({ - id: person.id, + id: person.personGroupId, name: person.name, isHidden: person.isHidden, birthDate: person.birthDate, @@ -50,14 +50,14 @@ describe(SyncEntityType.PersonV1, () => { const { auth, ctx } = await setup(); const personRepo = ctx.get(PersonRepository); const { person } = await ctx.newPerson({ ownerId: auth.user.id }); - await personRepo.delete([person.id]); + await personRepo.delete([person.personGroupId], person.ownerId); const response = await ctx.syncStream(auth, [SyncRequestType.PeopleV1]); expect(response).toEqual([ { ack: expect.any(String), data: { - personId: person.id, + personId: person.personGroupId, }, type: 'PersonDeleteV1', }, @@ -82,7 +82,7 @@ describe(SyncEntityType.PersonV1, () => { ]); await ctx.assertSyncIsComplete(auth, [SyncRequestType.PeopleV1]); - await personRepo.delete([person.id]); + await personRepo.delete([person.personGroupId], person.ownerId); expect(await ctx.syncStream(auth2, [SyncRequestType.PeopleV1])).toEqual([ expect.objectContaining({ type: SyncEntityType.PersonDeleteV1 }), diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index f723113bd1785..a928b4796de81 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -16,6 +16,16 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => { checkCreateAccess: vitest.fn().mockResolvedValue(new Set()), }, + clusterGroup: { + checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), + checkInviteAccess: vitest.fn().mockResolvedValue(new Set()), + }, + + clusterGroupRequest: { + checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), + checkGroupAccess: vitest.fn().mockResolvedValue(new Set()), + }, + asset: { checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), checkAlbumAccess: vitest.fn().mockResolvedValue(new Set()), diff --git a/server/test/utils.ts b/server/test/utils.ts index 3bdbe1f5d511f..e7392949ad4b6 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -26,6 +26,7 @@ import { AppRepository } from 'src/repositories/app.repository'; import { AssetEditRepository } from 'src/repositories/asset-edit.repository'; import { AssetJobRepository } from 'src/repositories/asset-job.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; +import { ClusterGroupRepository } from 'src/repositories/cluster-group.repository'; import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; @@ -75,6 +76,7 @@ import { AuthService } from 'src/services/auth.service'; import { BaseService } from 'src/services/base.service'; import { RepositoryInterface } from 'src/types'; import { getKyselyConfig } from 'src/utils/database'; +import { ClusterGroupFactory } from 'test/factories/cluster-group.factory'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; @@ -95,7 +97,9 @@ export type ControllerContext = { close: () => Promise; }; -export const controllerSetup = async (controller: new (...args: any[]) => unknown, providers: Provider[]) => { +type ControllerClass = new (...args: any[]) => unknown; + +export const controllerSetup = async (controller: ControllerClass | ControllerClass[], providers: Provider[]) => { const noopInterceptor = { intercept: (ctx: never, next: CallHandler) => next.handle() }; const upload = multer({ storage: multer.memoryStorage() }); const memoryFileInterceptor = { @@ -117,7 +121,7 @@ export const controllerSetup = async (controller: new (...args: any[]) => unknow }, }; const moduleRef = await Test.createTestingModule({ - controllers: [controller], + controllers: Array.isArray(controller) ? controller : [controller], providers: [ { provide: APP_FILTER, useClass: GlobalExceptionFilter }, { provide: APP_PIPE, useClass: ZodValidationPipe }, @@ -236,6 +240,7 @@ export type ServiceOverrides = { asset: AssetRepository; assetEdit: AssetEditRepository; assetJob: AssetJobRepository; + clusterGroup: ClusterGroupRepository; config: ConfigRepository; cron: CronRepository; crypto: CryptoRepository; @@ -319,6 +324,7 @@ export const getMocks = () => { asset: newAssetRepositoryMock(), assetEdit: automock(AssetEditRepository), assetJob: automock(AssetJobRepository), + clusterGroup: automock(ClusterGroupRepository), app: automock(AppRepository, { strict: false }), config: newConfigRepositoryMock(), database: databaseMock, @@ -369,6 +375,9 @@ export const getMocks = () => { workflow: automock(WorkflowRepository, { strict: true }), }; + // every new user gets a cluster group, which is incidental to most tests + mocks.clusterGroup.create.mockResolvedValue(ClusterGroupFactory.create()); + return mocks; }; @@ -389,6 +398,7 @@ export const newTestService = ( overrides.asset || (mocks.asset as As), overrides.assetEdit || (mocks.assetEdit as As), overrides.assetJob || (mocks.assetJob as As), + overrides.clusterGroup || (mocks.clusterGroup as As), overrides.config || (mocks.config as As as ConfigRepository), overrides.cron || (mocks.cron as As), overrides.crypto || (mocks.crypto as As), diff --git a/web/src/lib/components/asset-viewer/DetailPanelPeople.svelte b/web/src/lib/components/asset-viewer/DetailPanelPeople.svelte index 6290209e28936..fea7946c207b7 100644 --- a/web/src/lib/components/asset-viewer/DetailPanelPeople.svelte +++ b/web/src/lib/components/asset-viewer/DetailPanelPeople.svelte @@ -57,45 +57,49 @@ ); -{#if !authManager.isSharedLink && isOwner} +{#if !authManager.isSharedLink}
-
- {$t('people')} -
- {#if people.some((person) => person.isHidden)} - assetViewerManager.toggleHiddenPeople()} - /> - {/if} - assetViewerManager.toggleFaceEditMode()} - /> + {#if isOwner || visiblePeople.length > 0} +
+ {$t('people')} +
+ {#if isOwner} + {#if people.some((person) => person.isHidden)} + assetViewerManager.toggleHiddenPeople()} + /> + {/if} + assetViewerManager.toggleFaceEditMode()} + /> - {#if faceManager.data.length > 0} - assetViewerManager.openEditFacesPanel()} - /> - {/if} + {#if faceManager.data.length > 0} + assetViewerManager.openEditFacesPanel()} + /> + {/if} + {/if} +
-
+ {/if}
{#each visiblePeople as person (person.id)} diff --git a/web/src/lib/components/shared-components/navigation-bar/NotificationPanel.svelte b/web/src/lib/components/shared-components/navigation-bar/NotificationPanel.svelte index 79cfff858a5bf..4e6929a87ef52 100644 --- a/web/src/lib/components/shared-components/navigation-bar/NotificationPanel.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/NotificationPanel.svelte @@ -2,6 +2,8 @@ import { goto } from '$app/navigation'; import { focusTrap } from '$lib/actions/focus-trap'; import NotificationItem from '$lib/components/shared-components/navigation-bar/NotificationItem.svelte'; + import { OpenQueryParam } from '$lib/constants'; + import { Route } from '$lib/route'; import { notificationManager } from '$lib/stores/notification-manager.svelte'; import { handleError } from '$lib/utils/handle-error'; import { NotificationType, type NotificationDto } from '@immich/sdk'; @@ -50,6 +52,11 @@ break; } + case NotificationType.ClusterGroupRequest: { + await goto(Route.userSettings({ isOpen: OpenQueryParam.SHARING })); + break; + } + default: { break; } diff --git a/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte b/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte index da58d779e9768..3948b6bbc2d85 100644 --- a/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte +++ b/web/src/lib/components/shared-components/settings/SystemConfigButtonRow.svelte @@ -1,15 +1,15 @@ + + + + {#await loadUsers()} +
+ +
+ {:then _} + {#if availableUsers.length > 0} +
+ {#each availableUsers as user (user.id)} + selectUser(user)} selected={selectedUsers.some(({ id }) => id === user.id)}> + +
+ {user.name} + {user.email} +
+
+ {/each} +
+ + + + + {:else} + {$t('partner_page_no_more_users')} + {/if} + {/await} +
+
diff --git a/web/src/lib/modals/ClusterGroupUsersModal.svelte b/web/src/lib/modals/ClusterGroupUsersModal.svelte new file mode 100644 index 0000000000000..2bfb61257c17d --- /dev/null +++ b/web/src/lib/modals/ClusterGroupUsersModal.svelte @@ -0,0 +1,54 @@ + + + + + {#await loadUsers()} +
+ +
+ {:then _} + {$t('cluster_group_invite_description')} + +
+ {$t('users')} +
+ +
+ {#each users as user (user.id)} +
+ +
+ {user.name} + {user.email} +
+
+ {/each} +
+ {/await} +
+ + + + + + + +
diff --git a/web/src/lib/services/api-key.service.ts b/web/src/lib/services/api-key.service.ts index e5346627ff130..5dc990af9699f 100644 --- a/web/src/lib/services/api-key.service.ts +++ b/web/src/lib/services/api-key.service.ts @@ -64,7 +64,7 @@ export const handleCreateApiKey = async (dto: ApiKeyCreateDto) => { } const response = await createApiKey({ apiKeyCreateDto: dto }); - eventManager.emit('ApiKeyCreate', response.apiKey); + eventManager.emit('ApiKeyCreate', response); return response; } catch (error) { @@ -105,7 +105,7 @@ export const handleRotateApiKey = async (apiKey: ApiKeyResponseDto) => { try { const response = await rotateApiKey({ id: apiKey.id }); - eventManager.emit('ApiKeyUpdate', response.apiKey); + eventManager.emit('ApiKeyUpdate', response); await modalManager.show(ApiKeySecretModal, { secret: response.secret }); } catch (error) { handleError(error, $t('errors.something_went_wrong')); diff --git a/web/src/lib/services/system-config.service.ts b/web/src/lib/services/system-config.service.ts index b8d7a0f1a7bfb..6fa8bf0f17772 100644 --- a/web/src/lib/services/system-config.service.ts +++ b/web/src/lib/services/system-config.service.ts @@ -1,4 +1,4 @@ -import { getConfig, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk'; +import { getConfig, updateConfig, type ServerFeaturesDto, type AdminConfigDto } from '@immich/sdk'; import { toastManager, type ActionItem } from '@immich/ui'; import { mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js'; import { isEqual } from 'lodash-es'; @@ -11,7 +11,7 @@ import { getFormatter } from '$lib/utils/i18n'; export const getSystemConfigActions = ( $t: MessageFormatter, featureFlags: ServerFeaturesDto, - config: SystemConfigDto, + config: AdminConfigDto, ) => { const CopyToClipboard: ActionItem = { title: $t('copy_to_clipboard'), @@ -44,17 +44,17 @@ export const getSystemConfigActions = ( return { CopyToClipboard, Download, Upload }; }; -export const handleSystemConfigSave = async (update: Partial) => { +export const handleSystemConfigSave = async (update: Partial) => { const $t = await getFormatter(); const config = await getConfig(); - const systemConfigDto = { ...config, ...update }; + const adminConfigDto = { ...config, ...update }; - if (isEqual(config, systemConfigDto)) { + if (isEqual(config, adminConfigDto)) { return; } try { - const newConfig = await updateConfig({ systemConfigDto }); + const newConfig = await updateConfig({ adminConfigDto }); eventManager.emit('SystemConfigUpdate', newConfig); toastManager.primary($t('settings_saved')); diff --git a/web/src/routes/(user)/user-settings/PartnerSettings.svelte b/web/src/routes/(user)/user-settings/PartnerSettings.svelte deleted file mode 100644 index af1eff38bfb64..0000000000000 --- a/web/src/routes/(user)/user-settings/PartnerSettings.svelte +++ /dev/null @@ -1,194 +0,0 @@ - - -
- {#if partners.length > 0} - {#each partners as partner (partner.user.id)} -
-
-
- -
-

- {partner.user.name} -

-

- {partner.user.email} -

-
-
- - {#if partner.sharedByMe} - handleRemovePartner(partner.user)} - icon={mdiClose} - size="small" - aria-label={$t('stop_sharing_photos_with_user')} - /> - {/if} -
- -
- - {#if partner.sharedByMe} -
- - {$t('shared_with_partner', { values: { partner: partner.user.name } })} - - {$t('partner_can_access', { values: { partner: partner.user.name } })} -
    -
  • - - {$t('partner_can_access_assets')} -
  • -
  • - - {$t('partner_can_access_location')} -
  • -
- {/if} - - - {#if partner.sharedWithMe} -
- - {$t('shared_from_partner', { values: { partner: partner.user.name } })} - - - handleShowOnTimelineChanged(partner, isChecked)} - /> - {/if} -
-
- {/each} - {/if} - -
- -
-
diff --git a/web/src/routes/(user)/user-settings/SharingSettings.svelte b/web/src/routes/(user)/user-settings/SharingSettings.svelte new file mode 100644 index 0000000000000..8e0ea59938548 --- /dev/null +++ b/web/src/routes/(user)/user-settings/SharingSettings.svelte @@ -0,0 +1,379 @@ + + +
+ {$t('cluster_group')} + {$t('cluster_group_description')} + + + + {#each users as user, index (user.id)} +
0}> +
+ +
+

+ {user.name} + {#if user.id === authManager.user.id} + ({$t('you')}) + {/if} +

+

{user.email}

+
+
+ + {#if user.id === authManager.user.id && canLeave} + + {/if} +
+ {/each} +
+
+ + {#if sentRequests.length > 0 || receivedRequests.length > 0} +
+ {$t('pending')} +
+ + + + {#each receivedRequests as request, index (request.id)} +
0}> + {$t('request_received_description')} +
+ +
+
+ {/each} + + {#each sentRequests as request, index (request.id)} + {@const user = candidates[request.userId]} +
0 || receivedRequests.length > 0}> +
+ {#if user} + + {/if} +
+

{user?.name ?? request.userId}

+

{user?.email ?? ''}

+
+
+ + +
+ {/each} +
+
+ {/if} + +
+ +
+
+ +
+ {$t('partners')} + + {#if partners.length > 0} + {#each partners as partner (partner.user.id)} +
+
+
+ +
+

+ {partner.user.name} +

+

+ {partner.user.email} +

+
+
+ + {#if partner.sharedByMe} + handleRemovePartner(partner.user)} + icon={mdiClose} + size="small" + aria-label={$t('stop_sharing_photos_with_user')} + /> + {/if} +
+ +
+ + {#if partner.sharedByMe} +
+ + {$t('shared_with_partner', { values: { partner: partner.user.name } })} + + {$t('partner_can_access', { values: { partner: partner.user.name } })} +
    +
  • + + {$t('partner_can_access_assets')} +
  • +
  • + + {$t('partner_can_access_location')} +
  • +
+ {/if} + + + {#if partner.sharedWithMe} +
+ + {$t('shared_from_partner', { values: { partner: partner.user.name } })} + + + handleShowOnTimelineChanged(partner, isChecked)} + /> + {/if} +
+
+ {/each} + {/if} + +
+ +
+
diff --git a/web/src/routes/(user)/user-settings/UserSettingsList.svelte b/web/src/routes/(user)/user-settings/UserSettingsList.svelte index 91c32560d8479..b6595ed60f8a8 100644 --- a/web/src/routes/(user)/user-settings/UserSettingsList.svelte +++ b/web/src/routes/(user)/user-settings/UserSettingsList.svelte @@ -31,7 +31,7 @@ import ChangePasswordSettings from './ChangePasswordSettings.svelte'; import DeviceList from './DeviceList.svelte'; import OauthSettings from './OauthSettings.svelte'; - import PartnerSettings from './PartnerSettings.svelte'; + import SharingSettings from './SharingSettings.svelte'; import UserApiKeyList from './UserApiKeyList.svelte'; import UserProfileSettings from './UserProfileSettings.svelte'; @@ -129,15 +129,6 @@ - - - - + + + + diff --git a/web/src/routes/admin/system-settings/JobSettings.svelte b/web/src/routes/admin/system-settings/JobSettings.svelte index c229a788b4232..b38c3c2612657 100644 --- a/web/src/routes/admin/system-settings/JobSettings.svelte +++ b/web/src/routes/admin/system-settings/JobSettings.svelte @@ -4,7 +4,7 @@ import { SettingInputFieldType } from '$lib/constants'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { systemConfigManager } from '$lib/managers/system-config-manager.svelte'; - import { QueueName, type SystemConfigJobDto } from '@immich/sdk'; + import { QueueName, type AdminConfigJobDto } from '@immich/sdk'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; @@ -26,7 +26,7 @@ QueueName.Ocr, ]; - function isSystemConfigJobDto(jobName: string): jobName is keyof SystemConfigJobDto { + function isSystemConfigJobDto(jobName: string): jobName is keyof AdminConfigJobDto { return Object.hasOwn(configToEdit.job, jobName); } diff --git a/web/src/routes/admin/system-settings/NotificationSettings.svelte b/web/src/routes/admin/system-settings/NotificationSettings.svelte index d0ec7d8f3ab3a..9de2a2d7d45df 100644 --- a/web/src/routes/admin/system-settings/NotificationSettings.svelte +++ b/web/src/routes/admin/system-settings/NotificationSettings.svelte @@ -30,7 +30,7 @@ try { await sendTestEmailAdmin({ - systemConfigSmtpDto: { + adminConfigSmtpDto: { enabled: configToEdit.notifications.smtp.enabled, transport: { host: configToEdit.notifications.smtp.transport.host, diff --git a/web/src/routes/admin/system-settings/TemplateSettings.svelte b/web/src/routes/admin/system-settings/TemplateSettings.svelte index 55da30e266f78..bcc9cc90cd051 100644 --- a/web/src/routes/admin/system-settings/TemplateSettings.svelte +++ b/web/src/routes/admin/system-settings/TemplateSettings.svelte @@ -5,14 +5,14 @@ import { systemConfigManager } from '$lib/managers/system-config-manager.svelte'; import EmailTemplatePreviewModal from '$lib/modals/EmailTemplatePreviewModal.svelte'; import { handleError } from '$lib/utils/handle-error'; - import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplateAdmin } from '@immich/sdk'; + import { type AdminConfigDto, type AdminConfigTemplateEmailsDto, getNotificationTemplateAdmin } from '@immich/sdk'; import { Button, Icon, LoadingSpinner, modalManager } from '@immich/ui'; import { mdiEyeOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; interface Props { - config: SystemConfigDto; + config: AdminConfigDto; } let { config = $bindable() }: Props = $props(); @@ -52,7 +52,7 @@ }, ]; - const isEdited = (templateKey: keyof SystemConfigTemplateEmailsDto) => + const isEdited = (templateKey: keyof AdminConfigTemplateEmailsDto) => config.templates.email[templateKey] !== systemConfigManager.value.templates.email[templateKey]; const onsubmit = (event: Event) => { diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 78cd5fdba366f..b505ef1f9f980 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -2,7 +2,6 @@ import { goto } from '$app/navigation'; import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte'; - import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; import { Route } from '$lib/route'; import { oauth } from '$lib/utils'; @@ -27,6 +26,7 @@ let oauthLoading = $state(true); const serverConfig = $derived(serverConfigManager.value); + const publicConfig = $derived(data.publicConfig); const onSuccess = async (user: LoginResponseDto) => { await goto(data.continueUrl, { invalidateAll: true }); @@ -37,7 +37,7 @@ const onOnboarding = () => goto(Route.onboarding()); onMount(async () => { - if (!featureFlagsManager.value.oauth) { + if (!publicConfig.oauth.enabled) { oauthLoading = false; return; } @@ -63,7 +63,7 @@ try { if ( - (featureFlagsManager.value.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(location)) || + (publicConfig.oauth.autoLaunch && !oauth.isAutoLaunchDisabled(location)) || oauth.isAutoLaunchEnabled(location) ) { await goto(Route.login({ autoLaunch: 0 }), { replaceState: true }); @@ -128,14 +128,14 @@ - {#if serverConfig.loginPageMessage} + {#if publicConfig.server.loginPageMessage} - {@html serverConfig.loginPageMessage} + {@html publicConfig.server.loginPageMessage} {/if} - {#if !oauthLoading && featureFlagsManager.value.passwordLogin} + {#if !oauthLoading && publicConfig.passwordLogin.enabled}
{#if errorMessage} @@ -153,8 +153,8 @@ {/if} - {#if featureFlagsManager.value.oauth} - {#if featureFlagsManager.value.passwordLogin} + {#if publicConfig.oauth.enabled} + {#if publicConfig.passwordLogin.enabled}

- {serverConfig.oauthButtonText} + {publicConfig.oauth.buttonText} {/if} - {#if !featureFlagsManager.value.passwordLogin && !featureFlagsManager.value.oauth} + {#if !publicConfig.passwordLogin.enabled && !publicConfig.oauth.enabled} {/if} diff --git a/web/src/routes/auth/login/+page.ts b/web/src/routes/auth/login/+page.ts index dceb340505403..a1daaf74953ef 100644 --- a/web/src/routes/auth/login/+page.ts +++ b/web/src/routes/auth/login/+page.ts @@ -1,3 +1,4 @@ +import { getPublicConfig } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; import { authManager } from '$lib/managers/auth-manager.svelte'; import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; @@ -19,11 +20,14 @@ export const load = (async ({ parent, url }) => { redirect(307, Route.register()); } + const publicConfig = await getPublicConfig(); + const $t = await getFormatter(); return { meta: { title: $t('login'), }, continueUrl, + publicConfig, }; }) satisfies PageLoad; diff --git a/web/src/test-data/factories/user-factory.ts b/web/src/test-data/factories/user-factory.ts index 7b56c275e25f3..edc1ead1ce910 100644 --- a/web/src/test-data/factories/user-factory.ts +++ b/web/src/test-data/factories/user-factory.ts @@ -4,6 +4,7 @@ import { Sync } from 'factory.ts'; export const userAdminFactory = Sync.makeFactory({ id: Sync.each(() => faker.string.uuid()), + clusterGroupId: Sync.each(() => faker.string.uuid()), email: Sync.each(() => faker.internet.email()), name: Sync.each(() => faker.person.fullName()), profileImagePath: '',