mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
Merge remote-tracking branch 'origin/main' into improve_focus
This commit is contained in:
commit
1e77f420d9
78
.vscode/settings.json
vendored
78
.vscode/settings.json
vendored
@ -1,45 +1,63 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.tabSize": 2,
|
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"[css]": {
|
"[css]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.tabSize": 2,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnSave": true
|
|
||||||
},
|
|
||||||
"[svelte]": {
|
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2
|
||||||
},
|
},
|
||||||
"svelte.enable-ts-plugin": true,
|
|
||||||
"eslint.validate": [
|
|
||||||
"javascript",
|
|
||||||
"svelte"
|
|
||||||
],
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
|
||||||
"[dart]": {
|
"[dart]": {
|
||||||
|
"editor.defaultFormatter": "Dart-Code.dart-code",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.selectionHighlight": false,
|
"editor.selectionHighlight": false,
|
||||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||||
"editor.suggestSelection": "first",
|
"editor.suggestSelection": "first",
|
||||||
"editor.tabCompletion": "onlySnippets",
|
"editor.tabCompletion": "onlySnippets",
|
||||||
"editor.wordBasedSuggestions": "off",
|
"editor.wordBasedSuggestions": "off"
|
||||||
"editor.defaultFormatter": "Dart-Code.dart-code"
|
|
||||||
},
|
},
|
||||||
"cSpell.words": [
|
"[javascript]": {
|
||||||
"immich"
|
"editor.codeActionsOnSave": {
|
||||||
],
|
"source.organizeImports": "explicit",
|
||||||
|
"source.removeUnusedImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit",
|
||||||
|
"source.removeUnusedImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit",
|
||||||
|
"source.removeUnusedImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 2
|
||||||
|
},
|
||||||
|
"cSpell.words": ["immich"],
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"eslint.validate": ["javascript", "svelte"],
|
||||||
"explorer.fileNesting.enabled": true,
|
"explorer.fileNesting.enabled": true,
|
||||||
"explorer.fileNesting.patterns": {
|
"explorer.fileNesting.patterns": {
|
||||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
||||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart"
|
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
|
||||||
}
|
},
|
||||||
|
"svelte.enable-ts-plugin": true,
|
||||||
|
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||||
}
|
}
|
3
Makefile
3
Makefile
@ -17,6 +17,9 @@ e2e:
|
|||||||
prod:
|
prod:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
|
||||||
|
|
||||||
|
prod-down:
|
||||||
|
docker compose -f ./docker/docker-compose.prod.yml down --remove-orphans
|
||||||
|
|
||||||
prod-scale:
|
prod-scale:
|
||||||
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
docker compose -f ./docker/docker-compose.prod.yml up --build -V --scale immich-server=3 --scale immich-microservices=3 --remove-orphans
|
||||||
|
|
||||||
|
@ -215,6 +215,19 @@ describe('/admin/users', () => {
|
|||||||
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
|
const user = await getMyUser({ headers: asBearerAuth(token.accessToken) });
|
||||||
expect(user).toMatchObject({ email: nonAdmin.userEmail });
|
expect(user).toMatchObject({ email: nonAdmin.userEmail });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update the avatar color', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/admin/users/${admin.userId}`)
|
||||||
|
.send({ avatarColor: 'orange' })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ avatarColor: 'orange' });
|
||||||
|
|
||||||
|
const after = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(after).toMatchObject({ avatarColor: 'orange' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /admin/users/:id/preferences', () => {
|
describe('PUT /admin/users/:id/preferences', () => {
|
||||||
@ -240,19 +253,6 @@ describe('/admin/users', () => {
|
|||||||
expect(after).toMatchObject({ memories: { enabled: false } });
|
expect(after).toMatchObject({ memories: { enabled: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update the avatar color', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/admin/users/${admin.userId}/preferences`)
|
|
||||||
.send({ avatar: { color: 'orange' } })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({ avatar: { color: 'orange' } });
|
|
||||||
|
|
||||||
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(after).toMatchObject({ avatar: { color: 'orange' } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update download archive size', async () => {
|
it('should update download archive size', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/admin/users/${admin.userId}/preferences`)
|
.put(`/admin/users/${admin.userId}/preferences`)
|
||||||
|
@ -139,6 +139,19 @@ describe('/users', () => {
|
|||||||
profileChangedAt: expect.anything(),
|
profileChangedAt: expect.anything(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update avatar color', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/users/me`)
|
||||||
|
.send({ avatarColor: 'blue' })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ avatarColor: 'blue' });
|
||||||
|
|
||||||
|
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(after).toMatchObject({ avatarColor: 'blue' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /users/me/preferences', () => {
|
describe('PUT /users/me/preferences', () => {
|
||||||
@ -158,19 +171,6 @@ describe('/users', () => {
|
|||||||
expect(after).toMatchObject({ memories: { enabled: false } });
|
expect(after).toMatchObject({ memories: { enabled: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update avatar color', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.put(`/users/me/preferences`)
|
|
||||||
.send({ avatar: { color: 'blue' } })
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toMatchObject({ avatar: { color: 'blue' } });
|
|
||||||
|
|
||||||
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(after).toMatchObject({ avatar: { color: 'blue' } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should require an integer for download archive size', async () => {
|
it('should require an integer for download archive size', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/users/me/preferences`)
|
.put(`/users/me/preferences`)
|
||||||
|
@ -21,23 +21,9 @@ test.describe('Photo Viewer', () => {
|
|||||||
test.beforeEach(async ({ context, page }) => {
|
test.beforeEach(async ({ context, page }) => {
|
||||||
// before each test, login as user
|
// before each test, login as user
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
await page.goto('/photos');
|
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially shows a loading spinner', async ({ page }) => {
|
|
||||||
await page.route(`/api/assets/${asset.id}/thumbnail**`, async (route) => {
|
|
||||||
// slow down the request for thumbnail, so spinner has chance to show up
|
|
||||||
await new Promise((f) => setTimeout(f, 2000));
|
|
||||||
await route.continue();
|
|
||||||
});
|
|
||||||
await page.goto(`/photos/${asset.id}`);
|
|
||||||
await page.waitForLoadState('load');
|
|
||||||
// this is the spinner
|
|
||||||
await page.waitForSelector('svg[role=status]');
|
|
||||||
await expect(page.getByTestId('loading-spinner')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('loads original photo when zoomed', async ({ page }) => {
|
test('loads original photo when zoomed', async ({ page }) => {
|
||||||
await page.goto(`/photos/${asset.id}`);
|
await page.goto(`/photos/${asset.id}`);
|
||||||
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
|
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
|
||||||
|
@ -853,10 +853,12 @@
|
|||||||
"failed_to_keep_this_delete_others": "Failed to keep this asset and delete the other assets",
|
"failed_to_keep_this_delete_others": "Failed to keep this asset and delete the other assets",
|
||||||
"failed_to_load_asset": "Failed to load asset",
|
"failed_to_load_asset": "Failed to load asset",
|
||||||
"failed_to_load_assets": "Failed to load assets",
|
"failed_to_load_assets": "Failed to load assets",
|
||||||
|
"failed_to_load_notifications": "Failed to load notifications",
|
||||||
"failed_to_load_people": "Failed to load people",
|
"failed_to_load_people": "Failed to load people",
|
||||||
"failed_to_remove_product_key": "Failed to remove product key",
|
"failed_to_remove_product_key": "Failed to remove product key",
|
||||||
"failed_to_stack_assets": "Failed to stack assets",
|
"failed_to_stack_assets": "Failed to stack assets",
|
||||||
"failed_to_unstack_assets": "Failed to un-stack assets",
|
"failed_to_unstack_assets": "Failed to un-stack assets",
|
||||||
|
"failed_to_update_notification_status": "Failed to update notification status",
|
||||||
"import_path_already_exists": "This import path already exists.",
|
"import_path_already_exists": "This import path already exists.",
|
||||||
"incorrect_email_or_password": "Incorrect email or password",
|
"incorrect_email_or_password": "Incorrect email or password",
|
||||||
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
|
"paths_validation_failed": "{paths, plural, one {# path} other {# paths}} failed validation",
|
||||||
@ -1199,6 +1201,9 @@
|
|||||||
"map_settings_only_show_favorites": "Show Favorite Only",
|
"map_settings_only_show_favorites": "Show Favorite Only",
|
||||||
"map_settings_theme_settings": "Map Theme",
|
"map_settings_theme_settings": "Map Theme",
|
||||||
"map_zoom_to_see_photos": "Zoom out to see photos",
|
"map_zoom_to_see_photos": "Zoom out to see photos",
|
||||||
|
"mark_as_read": "Mark as read",
|
||||||
|
"mark_all_as_read": "Mark all as read",
|
||||||
|
"marked_all_as_read": "Marked all as read",
|
||||||
"matches": "Matches",
|
"matches": "Matches",
|
||||||
"media_type": "Media type",
|
"media_type": "Media type",
|
||||||
"memories": "Memories",
|
"memories": "Memories",
|
||||||
@ -1260,6 +1265,7 @@
|
|||||||
"no_places": "No places",
|
"no_places": "No places",
|
||||||
"no_results": "No results",
|
"no_results": "No results",
|
||||||
"no_results_description": "Try a synonym or more general keyword",
|
"no_results_description": "Try a synonym or more general keyword",
|
||||||
|
"no_notifications": "No notifications",
|
||||||
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
"no_shared_albums_message": "Create an album to share photos and videos with people in your network",
|
||||||
"not_in_any_album": "Not in any album",
|
"not_in_any_album": "Not in any album",
|
||||||
"not_selected": "Not selected",
|
"not_selected": "Not selected",
|
||||||
|
@ -97,6 +97,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
if (7 - scaleFactor.value.toInt() != perRow.value) {
|
if (7 - scaleFactor.value.toInt() != perRow.value) {
|
||||||
perRow.value = 7 - scaleFactor.value.toInt();
|
perRow.value = 7 - scaleFactor.value.toInt();
|
||||||
|
settings.setSetting(AppSettingsEnum.tilesPerRow, perRow.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -755,7 +755,7 @@ class _MonthTitle extends StatelessWidget {
|
|||||||
key: Key("month-$title"),
|
key: Key("month-$title"),
|
||||||
padding: const EdgeInsets.only(left: 12.0, top: 24.0),
|
padding: const EdgeInsets.only(left: 12.0, top: 24.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
title,
|
toBeginningOfSentenceCase(title, context.locale.languageCode),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 26,
|
fontSize: 26,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@ -786,7 +786,7 @@ class _Title extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GroupDividerTitle(
|
return GroupDividerTitle(
|
||||||
text: title,
|
text: toBeginningOfSentenceCase(title, context.locale.languageCode),
|
||||||
multiselectEnabled: selectionActive,
|
multiselectEnabled: selectionActive,
|
||||||
onSelect: () => selectAssets(assets),
|
onSelect: () => selectAssets(assets),
|
||||||
onDeselect: () => deselectAssets(assets),
|
onDeselect: () => deselectAssets(assets),
|
||||||
|
19
mobile/openapi/README.md
generated
19
mobile/openapi/README.md
generated
@ -145,8 +145,15 @@ Class | Method | HTTP request | Description
|
|||||||
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
|
*MemoriesApi* | [**removeMemoryAssets**](doc//MemoriesApi.md#removememoryassets) | **DELETE** /memories/{id}/assets |
|
||||||
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
|
*MemoriesApi* | [**searchMemories**](doc//MemoriesApi.md#searchmemories) | **GET** /memories |
|
||||||
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
|
*MemoriesApi* | [**updateMemory**](doc//MemoriesApi.md#updatememory) | **PUT** /memories/{id} |
|
||||||
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /notifications/admin/templates/{name} |
|
*NotificationsApi* | [**deleteNotification**](doc//NotificationsApi.md#deletenotification) | **DELETE** /notifications/{id} |
|
||||||
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /notifications/admin/test-email |
|
*NotificationsApi* | [**deleteNotifications**](doc//NotificationsApi.md#deletenotifications) | **DELETE** /notifications |
|
||||||
|
*NotificationsApi* | [**getNotification**](doc//NotificationsApi.md#getnotification) | **GET** /notifications/{id} |
|
||||||
|
*NotificationsApi* | [**getNotifications**](doc//NotificationsApi.md#getnotifications) | **GET** /notifications |
|
||||||
|
*NotificationsApi* | [**updateNotification**](doc//NotificationsApi.md#updatenotification) | **PUT** /notifications/{id} |
|
||||||
|
*NotificationsApi* | [**updateNotifications**](doc//NotificationsApi.md#updatenotifications) | **PUT** /notifications |
|
||||||
|
*NotificationsAdminApi* | [**createNotification**](doc//NotificationsAdminApi.md#createnotification) | **POST** /admin/notifications |
|
||||||
|
*NotificationsAdminApi* | [**getNotificationTemplateAdmin**](doc//NotificationsAdminApi.md#getnotificationtemplateadmin) | **POST** /admin/notifications/templates/{name} |
|
||||||
|
*NotificationsAdminApi* | [**sendTestEmailAdmin**](doc//NotificationsAdminApi.md#sendtestemailadmin) | **POST** /admin/notifications/test-email |
|
||||||
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
|
*OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback |
|
||||||
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
|
*OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link |
|
||||||
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
|
*OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect |
|
||||||
@ -300,7 +307,6 @@ Class | Method | HTTP request | Description
|
|||||||
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
|
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
|
||||||
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
- [AssetTypeEnum](doc//AssetTypeEnum.md)
|
||||||
- [AudioCodec](doc//AudioCodec.md)
|
- [AudioCodec](doc//AudioCodec.md)
|
||||||
- [AvatarResponse](doc//AvatarResponse.md)
|
|
||||||
- [AvatarUpdate](doc//AvatarUpdate.md)
|
- [AvatarUpdate](doc//AvatarUpdate.md)
|
||||||
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
|
||||||
- [BulkIdsDto](doc//BulkIdsDto.md)
|
- [BulkIdsDto](doc//BulkIdsDto.md)
|
||||||
@ -361,6 +367,13 @@ Class | Method | HTTP request | Description
|
|||||||
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
|
||||||
- [MergePersonDto](doc//MergePersonDto.md)
|
- [MergePersonDto](doc//MergePersonDto.md)
|
||||||
- [MetadataSearchDto](doc//MetadataSearchDto.md)
|
- [MetadataSearchDto](doc//MetadataSearchDto.md)
|
||||||
|
- [NotificationCreateDto](doc//NotificationCreateDto.md)
|
||||||
|
- [NotificationDeleteAllDto](doc//NotificationDeleteAllDto.md)
|
||||||
|
- [NotificationDto](doc//NotificationDto.md)
|
||||||
|
- [NotificationLevel](doc//NotificationLevel.md)
|
||||||
|
- [NotificationType](doc//NotificationType.md)
|
||||||
|
- [NotificationUpdateAllDto](doc//NotificationUpdateAllDto.md)
|
||||||
|
- [NotificationUpdateDto](doc//NotificationUpdateDto.md)
|
||||||
- [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md)
|
- [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md)
|
||||||
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
|
||||||
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
||||||
|
9
mobile/openapi/lib/api.dart
generated
9
mobile/openapi/lib/api.dart
generated
@ -44,6 +44,7 @@ part 'api/jobs_api.dart';
|
|||||||
part 'api/libraries_api.dart';
|
part 'api/libraries_api.dart';
|
||||||
part 'api/map_api.dart';
|
part 'api/map_api.dart';
|
||||||
part 'api/memories_api.dart';
|
part 'api/memories_api.dart';
|
||||||
|
part 'api/notifications_api.dart';
|
||||||
part 'api/notifications_admin_api.dart';
|
part 'api/notifications_admin_api.dart';
|
||||||
part 'api/o_auth_api.dart';
|
part 'api/o_auth_api.dart';
|
||||||
part 'api/partners_api.dart';
|
part 'api/partners_api.dart';
|
||||||
@ -107,7 +108,6 @@ part 'model/asset_stack_response_dto.dart';
|
|||||||
part 'model/asset_stats_response_dto.dart';
|
part 'model/asset_stats_response_dto.dart';
|
||||||
part 'model/asset_type_enum.dart';
|
part 'model/asset_type_enum.dart';
|
||||||
part 'model/audio_codec.dart';
|
part 'model/audio_codec.dart';
|
||||||
part 'model/avatar_response.dart';
|
|
||||||
part 'model/avatar_update.dart';
|
part 'model/avatar_update.dart';
|
||||||
part 'model/bulk_id_response_dto.dart';
|
part 'model/bulk_id_response_dto.dart';
|
||||||
part 'model/bulk_ids_dto.dart';
|
part 'model/bulk_ids_dto.dart';
|
||||||
@ -168,6 +168,13 @@ part 'model/memory_type.dart';
|
|||||||
part 'model/memory_update_dto.dart';
|
part 'model/memory_update_dto.dart';
|
||||||
part 'model/merge_person_dto.dart';
|
part 'model/merge_person_dto.dart';
|
||||||
part 'model/metadata_search_dto.dart';
|
part 'model/metadata_search_dto.dart';
|
||||||
|
part 'model/notification_create_dto.dart';
|
||||||
|
part 'model/notification_delete_all_dto.dart';
|
||||||
|
part 'model/notification_dto.dart';
|
||||||
|
part 'model/notification_level.dart';
|
||||||
|
part 'model/notification_type.dart';
|
||||||
|
part 'model/notification_update_all_dto.dart';
|
||||||
|
part 'model/notification_update_dto.dart';
|
||||||
part 'model/o_auth_authorize_response_dto.dart';
|
part 'model/o_auth_authorize_response_dto.dart';
|
||||||
part 'model/o_auth_callback_dto.dart';
|
part 'model/o_auth_callback_dto.dart';
|
||||||
part 'model/o_auth_config_dto.dart';
|
part 'model/o_auth_config_dto.dart';
|
||||||
|
55
mobile/openapi/lib/api/notifications_admin_api.dart
generated
55
mobile/openapi/lib/api/notifications_admin_api.dart
generated
@ -16,7 +16,54 @@ class NotificationsAdminApi {
|
|||||||
|
|
||||||
final ApiClient apiClient;
|
final ApiClient apiClient;
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /notifications/admin/templates/{name}' operation and returns the [Response].
|
/// Performs an HTTP 'POST /admin/notifications' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationCreateDto] notificationCreateDto (required):
|
||||||
|
Future<Response> createNotificationWithHttpInfo(NotificationCreateDto notificationCreateDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/admin/notifications';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = notificationCreateDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationCreateDto] notificationCreateDto (required):
|
||||||
|
Future<NotificationDto?> createNotification(NotificationCreateDto notificationCreateDto,) async {
|
||||||
|
final response = await createNotificationWithHttpInfo(notificationCreateDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'NotificationDto',) as NotificationDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /admin/notifications/templates/{name}' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [String] name (required):
|
/// * [String] name (required):
|
||||||
@ -24,7 +71,7 @@ class NotificationsAdminApi {
|
|||||||
/// * [TemplateDto] templateDto (required):
|
/// * [TemplateDto] templateDto (required):
|
||||||
Future<Response> getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async {
|
Future<Response> getNotificationTemplateAdminWithHttpInfo(String name, TemplateDto templateDto,) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/notifications/admin/templates/{name}'
|
final apiPath = r'/admin/notifications/templates/{name}'
|
||||||
.replaceAll('{name}', name);
|
.replaceAll('{name}', name);
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
// ignore: prefer_final_locals
|
||||||
@ -68,13 +115,13 @@ class NotificationsAdminApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /notifications/admin/test-email' operation and returns the [Response].
|
/// Performs an HTTP 'POST /admin/notifications/test-email' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [SystemConfigSmtpDto] systemConfigSmtpDto (required):
|
/// * [SystemConfigSmtpDto] systemConfigSmtpDto (required):
|
||||||
Future<Response> sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async {
|
Future<Response> sendTestEmailAdminWithHttpInfo(SystemConfigSmtpDto systemConfigSmtpDto,) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final apiPath = r'/notifications/admin/test-email';
|
final apiPath = r'/admin/notifications/test-email';
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
// ignore: prefer_final_locals
|
||||||
Object? postBody = systemConfigSmtpDto;
|
Object? postBody = systemConfigSmtpDto;
|
||||||
|
311
mobile/openapi/lib/api/notifications_api.dart
generated
Normal file
311
mobile/openapi/lib/api/notifications_api.dart
generated
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationsApi {
|
||||||
|
NotificationsApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||||
|
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Performs an HTTP 'DELETE /notifications/{id}' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<Response> deleteNotificationWithHttpInfo(String id,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications/{id}'
|
||||||
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<void> deleteNotification(String id,) async {
|
||||||
|
final response = await deleteNotificationWithHttpInfo(id,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'DELETE /notifications' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationDeleteAllDto] notificationDeleteAllDto (required):
|
||||||
|
Future<Response> deleteNotificationsWithHttpInfo(NotificationDeleteAllDto notificationDeleteAllDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = notificationDeleteAllDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'DELETE',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationDeleteAllDto] notificationDeleteAllDto (required):
|
||||||
|
Future<void> deleteNotifications(NotificationDeleteAllDto notificationDeleteAllDto,) async {
|
||||||
|
final response = await deleteNotificationsWithHttpInfo(notificationDeleteAllDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /notifications/{id}' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<Response> getNotificationWithHttpInfo(String id,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications/{id}'
|
||||||
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
Future<NotificationDto?> getNotification(String id,) async {
|
||||||
|
final response = await getNotificationWithHttpInfo(id,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'NotificationDto',) as NotificationDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /notifications' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id:
|
||||||
|
///
|
||||||
|
/// * [NotificationLevel] level:
|
||||||
|
///
|
||||||
|
/// * [NotificationType] type:
|
||||||
|
///
|
||||||
|
/// * [bool] unread:
|
||||||
|
Future<Response> getNotificationsWithHttpInfo({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (id != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'id', id));
|
||||||
|
}
|
||||||
|
if (level != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'level', level));
|
||||||
|
}
|
||||||
|
if (type != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'type', type));
|
||||||
|
}
|
||||||
|
if (unread != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'unread', unread));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id:
|
||||||
|
///
|
||||||
|
/// * [NotificationLevel] level:
|
||||||
|
///
|
||||||
|
/// * [NotificationType] type:
|
||||||
|
///
|
||||||
|
/// * [bool] unread:
|
||||||
|
Future<List<NotificationDto>?> getNotifications({ String? id, NotificationLevel? level, NotificationType? type, bool? unread, }) async {
|
||||||
|
final response = await getNotificationsWithHttpInfo( id: id, level: level, type: type, unread: unread, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<NotificationDto>') as List)
|
||||||
|
.cast<NotificationDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'PUT /notifications/{id}' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
///
|
||||||
|
/// * [NotificationUpdateDto] notificationUpdateDto (required):
|
||||||
|
Future<Response> updateNotificationWithHttpInfo(String id, NotificationUpdateDto notificationUpdateDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications/{id}'
|
||||||
|
.replaceAll('{id}', id);
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = notificationUpdateDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'PUT',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
///
|
||||||
|
/// * [NotificationUpdateDto] notificationUpdateDto (required):
|
||||||
|
Future<NotificationDto?> updateNotification(String id, NotificationUpdateDto notificationUpdateDto,) async {
|
||||||
|
final response = await updateNotificationWithHttpInfo(id, notificationUpdateDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'NotificationDto',) as NotificationDto;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'PUT /notifications' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationUpdateAllDto] notificationUpdateAllDto (required):
|
||||||
|
Future<Response> updateNotificationsWithHttpInfo(NotificationUpdateAllDto notificationUpdateAllDto,) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/notifications';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody = notificationUpdateAllDto;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
const contentTypes = <String>['application/json'];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'PUT',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [NotificationUpdateAllDto] notificationUpdateAllDto (required):
|
||||||
|
Future<void> updateNotifications(NotificationUpdateAllDto notificationUpdateAllDto,) async {
|
||||||
|
final response = await updateNotificationsWithHttpInfo(notificationUpdateAllDto,);
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
mobile/openapi/lib/api_client.dart
generated
16
mobile/openapi/lib/api_client.dart
generated
@ -270,8 +270,6 @@ class ApiClient {
|
|||||||
return AssetTypeEnumTypeTransformer().decode(value);
|
return AssetTypeEnumTypeTransformer().decode(value);
|
||||||
case 'AudioCodec':
|
case 'AudioCodec':
|
||||||
return AudioCodecTypeTransformer().decode(value);
|
return AudioCodecTypeTransformer().decode(value);
|
||||||
case 'AvatarResponse':
|
|
||||||
return AvatarResponse.fromJson(value);
|
|
||||||
case 'AvatarUpdate':
|
case 'AvatarUpdate':
|
||||||
return AvatarUpdate.fromJson(value);
|
return AvatarUpdate.fromJson(value);
|
||||||
case 'BulkIdResponseDto':
|
case 'BulkIdResponseDto':
|
||||||
@ -392,6 +390,20 @@ class ApiClient {
|
|||||||
return MergePersonDto.fromJson(value);
|
return MergePersonDto.fromJson(value);
|
||||||
case 'MetadataSearchDto':
|
case 'MetadataSearchDto':
|
||||||
return MetadataSearchDto.fromJson(value);
|
return MetadataSearchDto.fromJson(value);
|
||||||
|
case 'NotificationCreateDto':
|
||||||
|
return NotificationCreateDto.fromJson(value);
|
||||||
|
case 'NotificationDeleteAllDto':
|
||||||
|
return NotificationDeleteAllDto.fromJson(value);
|
||||||
|
case 'NotificationDto':
|
||||||
|
return NotificationDto.fromJson(value);
|
||||||
|
case 'NotificationLevel':
|
||||||
|
return NotificationLevelTypeTransformer().decode(value);
|
||||||
|
case 'NotificationType':
|
||||||
|
return NotificationTypeTypeTransformer().decode(value);
|
||||||
|
case 'NotificationUpdateAllDto':
|
||||||
|
return NotificationUpdateAllDto.fromJson(value);
|
||||||
|
case 'NotificationUpdateDto':
|
||||||
|
return NotificationUpdateDto.fromJson(value);
|
||||||
case 'OAuthAuthorizeResponseDto':
|
case 'OAuthAuthorizeResponseDto':
|
||||||
return OAuthAuthorizeResponseDto.fromJson(value);
|
return OAuthAuthorizeResponseDto.fromJson(value);
|
||||||
case 'OAuthCallbackDto':
|
case 'OAuthCallbackDto':
|
||||||
|
6
mobile/openapi/lib/api_helper.dart
generated
6
mobile/openapi/lib/api_helper.dart
generated
@ -100,6 +100,12 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is MemoryType) {
|
if (value is MemoryType) {
|
||||||
return MemoryTypeTypeTransformer().encode(value).toString();
|
return MemoryTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
if (value is NotificationLevel) {
|
||||||
|
return NotificationLevelTypeTransformer().encode(value).toString();
|
||||||
|
}
|
||||||
|
if (value is NotificationType) {
|
||||||
|
return NotificationTypeTypeTransformer().encode(value).toString();
|
||||||
|
}
|
||||||
if (value is PartnerDirection) {
|
if (value is PartnerDirection) {
|
||||||
return PartnerDirectionTypeTransformer().encode(value).toString();
|
return PartnerDirectionTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
180
mobile/openapi/lib/model/notification_create_dto.dart
generated
Normal file
180
mobile/openapi/lib/model/notification_create_dto.dart
generated
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class NotificationCreateDto {
|
||||||
|
/// Returns a new [NotificationCreateDto] instance.
|
||||||
|
NotificationCreateDto({
|
||||||
|
this.data,
|
||||||
|
this.description,
|
||||||
|
this.level,
|
||||||
|
this.readAt,
|
||||||
|
required this.title,
|
||||||
|
this.type,
|
||||||
|
required this.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
Object? data;
|
||||||
|
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
NotificationLevel? level;
|
||||||
|
|
||||||
|
DateTime? readAt;
|
||||||
|
|
||||||
|
String title;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
NotificationType? type;
|
||||||
|
|
||||||
|
String userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is NotificationCreateDto &&
|
||||||
|
other.data == data &&
|
||||||
|
other.description == description &&
|
||||||
|
other.level == level &&
|
||||||
|
other.readAt == readAt &&
|
||||||
|
other.title == title &&
|
||||||
|
other.type == type &&
|
||||||
|
other.userId == userId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(data == null ? 0 : data!.hashCode) +
|
||||||
|
(description == null ? 0 : description!.hashCode) +
|
||||||
|
(level == null ? 0 : level!.hashCode) +
|
||||||
|
(readAt == null ? 0 : readAt!.hashCode) +
|
||||||
|
(title.hashCode) +
|
||||||
|
(type == null ? 0 : type!.hashCode) +
|
||||||
|
(userId.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NotificationCreateDto[data=$data, description=$description, level=$level, readAt=$readAt, title=$title, type=$type, userId=$userId]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
if (this.data != null) {
|
||||||
|
json[r'data'] = this.data;
|
||||||
|
} else {
|
||||||
|
// json[r'data'] = null;
|
||||||
|
}
|
||||||
|
if (this.description != null) {
|
||||||
|
json[r'description'] = this.description;
|
||||||
|
} else {
|
||||||
|
// json[r'description'] = null;
|
||||||
|
}
|
||||||
|
if (this.level != null) {
|
||||||
|
json[r'level'] = this.level;
|
||||||
|
} else {
|
||||||
|
// json[r'level'] = null;
|
||||||
|
}
|
||||||
|
if (this.readAt != null) {
|
||||||
|
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'readAt'] = null;
|
||||||
|
}
|
||||||
|
json[r'title'] = this.title;
|
||||||
|
if (this.type != null) {
|
||||||
|
json[r'type'] = this.type;
|
||||||
|
} else {
|
||||||
|
// json[r'type'] = null;
|
||||||
|
}
|
||||||
|
json[r'userId'] = this.userId;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [NotificationCreateDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static NotificationCreateDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "NotificationCreateDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return NotificationCreateDto(
|
||||||
|
data: mapValueOfType<Object>(json, r'data'),
|
||||||
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
|
level: NotificationLevel.fromJson(json[r'level']),
|
||||||
|
readAt: mapDateTime(json, r'readAt', r''),
|
||||||
|
title: mapValueOfType<String>(json, r'title')!,
|
||||||
|
type: NotificationType.fromJson(json[r'type']),
|
||||||
|
userId: mapValueOfType<String>(json, r'userId')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NotificationCreateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationCreateDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationCreateDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, NotificationCreateDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, NotificationCreateDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = NotificationCreateDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of NotificationCreateDto-objects as value to a dart map
|
||||||
|
static Map<String, List<NotificationCreateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<NotificationCreateDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = NotificationCreateDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'title',
|
||||||
|
'userId',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
101
mobile/openapi/lib/model/notification_delete_all_dto.dart
generated
Normal file
101
mobile/openapi/lib/model/notification_delete_all_dto.dart
generated
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class NotificationDeleteAllDto {
|
||||||
|
/// Returns a new [NotificationDeleteAllDto] instance.
|
||||||
|
NotificationDeleteAllDto({
|
||||||
|
this.ids = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> ids;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is NotificationDeleteAllDto &&
|
||||||
|
_deepEquality.equals(other.ids, ids);
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(ids.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NotificationDeleteAllDto[ids=$ids]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'ids'] = this.ids;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [NotificationDeleteAllDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static NotificationDeleteAllDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "NotificationDeleteAllDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return NotificationDeleteAllDto(
|
||||||
|
ids: json[r'ids'] is Iterable
|
||||||
|
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NotificationDeleteAllDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationDeleteAllDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationDeleteAllDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, NotificationDeleteAllDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, NotificationDeleteAllDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = NotificationDeleteAllDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of NotificationDeleteAllDto-objects as value to a dart map
|
||||||
|
static Map<String, List<NotificationDeleteAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<NotificationDeleteAllDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = NotificationDeleteAllDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'ids',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
182
mobile/openapi/lib/model/notification_dto.dart
generated
Normal file
182
mobile/openapi/lib/model/notification_dto.dart
generated
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class NotificationDto {
|
||||||
|
/// Returns a new [NotificationDto] instance.
|
||||||
|
NotificationDto({
|
||||||
|
required this.createdAt,
|
||||||
|
this.data,
|
||||||
|
this.description,
|
||||||
|
required this.id,
|
||||||
|
required this.level,
|
||||||
|
this.readAt,
|
||||||
|
required this.title,
|
||||||
|
required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
DateTime createdAt;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
Object? data;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
String? description;
|
||||||
|
|
||||||
|
String id;
|
||||||
|
|
||||||
|
NotificationLevel level;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
DateTime? readAt;
|
||||||
|
|
||||||
|
String title;
|
||||||
|
|
||||||
|
NotificationType type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is NotificationDto &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.data == data &&
|
||||||
|
other.description == description &&
|
||||||
|
other.id == id &&
|
||||||
|
other.level == level &&
|
||||||
|
other.readAt == readAt &&
|
||||||
|
other.title == title &&
|
||||||
|
other.type == type;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(createdAt.hashCode) +
|
||||||
|
(data == null ? 0 : data!.hashCode) +
|
||||||
|
(description == null ? 0 : description!.hashCode) +
|
||||||
|
(id.hashCode) +
|
||||||
|
(level.hashCode) +
|
||||||
|
(readAt == null ? 0 : readAt!.hashCode) +
|
||||||
|
(title.hashCode) +
|
||||||
|
(type.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NotificationDto[createdAt=$createdAt, data=$data, description=$description, id=$id, level=$level, readAt=$readAt, title=$title, type=$type]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'createdAt'] = this.createdAt.toUtc().toIso8601String();
|
||||||
|
if (this.data != null) {
|
||||||
|
json[r'data'] = this.data;
|
||||||
|
} else {
|
||||||
|
// json[r'data'] = null;
|
||||||
|
}
|
||||||
|
if (this.description != null) {
|
||||||
|
json[r'description'] = this.description;
|
||||||
|
} else {
|
||||||
|
// json[r'description'] = null;
|
||||||
|
}
|
||||||
|
json[r'id'] = this.id;
|
||||||
|
json[r'level'] = this.level;
|
||||||
|
if (this.readAt != null) {
|
||||||
|
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'readAt'] = null;
|
||||||
|
}
|
||||||
|
json[r'title'] = this.title;
|
||||||
|
json[r'type'] = this.type;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [NotificationDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static NotificationDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "NotificationDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return NotificationDto(
|
||||||
|
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||||
|
data: mapValueOfType<Object>(json, r'data'),
|
||||||
|
description: mapValueOfType<String>(json, r'description'),
|
||||||
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
|
level: NotificationLevel.fromJson(json[r'level'])!,
|
||||||
|
readAt: mapDateTime(json, r'readAt', r''),
|
||||||
|
title: mapValueOfType<String>(json, r'title')!,
|
||||||
|
type: NotificationType.fromJson(json[r'type'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NotificationDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, NotificationDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, NotificationDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = NotificationDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of NotificationDto-objects as value to a dart map
|
||||||
|
static Map<String, List<NotificationDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<NotificationDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = NotificationDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'createdAt',
|
||||||
|
'id',
|
||||||
|
'level',
|
||||||
|
'title',
|
||||||
|
'type',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
91
mobile/openapi/lib/model/notification_level.dart
generated
Normal file
91
mobile/openapi/lib/model/notification_level.dart
generated
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationLevel {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const NotificationLevel._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const success = NotificationLevel._(r'success');
|
||||||
|
static const error = NotificationLevel._(r'error');
|
||||||
|
static const warning = NotificationLevel._(r'warning');
|
||||||
|
static const info = NotificationLevel._(r'info');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][NotificationLevel].
|
||||||
|
static const values = <NotificationLevel>[
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
warning,
|
||||||
|
info,
|
||||||
|
];
|
||||||
|
|
||||||
|
static NotificationLevel? fromJson(dynamic value) => NotificationLevelTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<NotificationLevel> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationLevel>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationLevel.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [NotificationLevel] to String,
|
||||||
|
/// and [decode] dynamic data back to [NotificationLevel].
|
||||||
|
class NotificationLevelTypeTransformer {
|
||||||
|
factory NotificationLevelTypeTransformer() => _instance ??= const NotificationLevelTypeTransformer._();
|
||||||
|
|
||||||
|
const NotificationLevelTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(NotificationLevel data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a NotificationLevel.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
NotificationLevel? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'success': return NotificationLevel.success;
|
||||||
|
case r'error': return NotificationLevel.error;
|
||||||
|
case r'warning': return NotificationLevel.warning;
|
||||||
|
case r'info': return NotificationLevel.info;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [NotificationLevelTypeTransformer] instance.
|
||||||
|
static NotificationLevelTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
91
mobile/openapi/lib/model/notification_type.dart
generated
Normal file
91
mobile/openapi/lib/model/notification_type.dart
generated
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationType {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const NotificationType._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const jobFailed = NotificationType._(r'JobFailed');
|
||||||
|
static const backupFailed = NotificationType._(r'BackupFailed');
|
||||||
|
static const systemMessage = NotificationType._(r'SystemMessage');
|
||||||
|
static const custom = NotificationType._(r'Custom');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][NotificationType].
|
||||||
|
static const values = <NotificationType>[
|
||||||
|
jobFailed,
|
||||||
|
backupFailed,
|
||||||
|
systemMessage,
|
||||||
|
custom,
|
||||||
|
];
|
||||||
|
|
||||||
|
static NotificationType? fromJson(dynamic value) => NotificationTypeTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<NotificationType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationType>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationType.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [NotificationType] to String,
|
||||||
|
/// and [decode] dynamic data back to [NotificationType].
|
||||||
|
class NotificationTypeTypeTransformer {
|
||||||
|
factory NotificationTypeTypeTransformer() => _instance ??= const NotificationTypeTypeTransformer._();
|
||||||
|
|
||||||
|
const NotificationTypeTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(NotificationType data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a NotificationType.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
NotificationType? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'JobFailed': return NotificationType.jobFailed;
|
||||||
|
case r'BackupFailed': return NotificationType.backupFailed;
|
||||||
|
case r'SystemMessage': return NotificationType.systemMessage;
|
||||||
|
case r'Custom': return NotificationType.custom;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [NotificationTypeTypeTransformer] instance.
|
||||||
|
static NotificationTypeTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
112
mobile/openapi/lib/model/notification_update_all_dto.dart
generated
Normal file
112
mobile/openapi/lib/model/notification_update_all_dto.dart
generated
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class NotificationUpdateAllDto {
|
||||||
|
/// Returns a new [NotificationUpdateAllDto] instance.
|
||||||
|
NotificationUpdateAllDto({
|
||||||
|
this.ids = const [],
|
||||||
|
this.readAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
List<String> ids;
|
||||||
|
|
||||||
|
DateTime? readAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateAllDto &&
|
||||||
|
_deepEquality.equals(other.ids, ids) &&
|
||||||
|
other.readAt == readAt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(ids.hashCode) +
|
||||||
|
(readAt == null ? 0 : readAt!.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'NotificationUpdateAllDto[ids=$ids, readAt=$readAt]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'ids'] = this.ids;
|
||||||
|
if (this.readAt != null) {
|
||||||
|
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'readAt'] = null;
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [NotificationUpdateAllDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static NotificationUpdateAllDto? fromJson(dynamic value) {
|
||||||
|
upgradeDto(value, "NotificationUpdateAllDto");
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return NotificationUpdateAllDto(
|
||||||
|
ids: json[r'ids'] is Iterable
|
||||||
|
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
|
||||||
|
: const [],
|
||||||
|
readAt: mapDateTime(json, r'readAt', r''),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<NotificationUpdateAllDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <NotificationUpdateAllDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = NotificationUpdateAllDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, NotificationUpdateAllDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, NotificationUpdateAllDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = NotificationUpdateAllDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of NotificationUpdateAllDto-objects as value to a dart map
|
||||||
|
static Map<String, List<NotificationUpdateAllDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<NotificationUpdateAllDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = NotificationUpdateAllDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'ids',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -10,52 +10,56 @@
|
|||||||
|
|
||||||
part of openapi.api;
|
part of openapi.api;
|
||||||
|
|
||||||
class AvatarResponse {
|
class NotificationUpdateDto {
|
||||||
/// Returns a new [AvatarResponse] instance.
|
/// Returns a new [NotificationUpdateDto] instance.
|
||||||
AvatarResponse({
|
NotificationUpdateDto({
|
||||||
required this.color,
|
this.readAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
UserAvatarColor color;
|
DateTime? readAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AvatarResponse &&
|
bool operator ==(Object other) => identical(this, other) || other is NotificationUpdateDto &&
|
||||||
other.color == color;
|
other.readAt == readAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(color.hashCode);
|
(readAt == null ? 0 : readAt!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AvatarResponse[color=$color]';
|
String toString() => 'NotificationUpdateDto[readAt=$readAt]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'color'] = this.color;
|
if (this.readAt != null) {
|
||||||
|
json[r'readAt'] = this.readAt!.toUtc().toIso8601String();
|
||||||
|
} else {
|
||||||
|
// json[r'readAt'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new [AvatarResponse] instance and imports its values from
|
/// Returns a new [NotificationUpdateDto] instance and imports its values from
|
||||||
/// [value] if it's a [Map], null otherwise.
|
/// [value] if it's a [Map], null otherwise.
|
||||||
// ignore: prefer_constructors_over_static_methods
|
// ignore: prefer_constructors_over_static_methods
|
||||||
static AvatarResponse? fromJson(dynamic value) {
|
static NotificationUpdateDto? fromJson(dynamic value) {
|
||||||
upgradeDto(value, "AvatarResponse");
|
upgradeDto(value, "NotificationUpdateDto");
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return AvatarResponse(
|
return NotificationUpdateDto(
|
||||||
color: UserAvatarColor.fromJson(json[r'color'])!,
|
readAt: mapDateTime(json, r'readAt', r''),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<AvatarResponse> listFromJson(dynamic json, {bool growable = false,}) {
|
static List<NotificationUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final result = <AvatarResponse>[];
|
final result = <NotificationUpdateDto>[];
|
||||||
if (json is List && json.isNotEmpty) {
|
if (json is List && json.isNotEmpty) {
|
||||||
for (final row in json) {
|
for (final row in json) {
|
||||||
final value = AvatarResponse.fromJson(row);
|
final value = NotificationUpdateDto.fromJson(row);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
result.add(value);
|
result.add(value);
|
||||||
}
|
}
|
||||||
@ -64,12 +68,12 @@ class AvatarResponse {
|
|||||||
return result.toList(growable: growable);
|
return result.toList(growable: growable);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, AvatarResponse> mapFromJson(dynamic json) {
|
static Map<String, NotificationUpdateDto> mapFromJson(dynamic json) {
|
||||||
final map = <String, AvatarResponse>{};
|
final map = <String, NotificationUpdateDto>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
final value = AvatarResponse.fromJson(entry.value);
|
final value = NotificationUpdateDto.fromJson(entry.value);
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
map[entry.key] = value;
|
map[entry.key] = value;
|
||||||
}
|
}
|
||||||
@ -78,14 +82,14 @@ class AvatarResponse {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
// maps a json object with a list of AvatarResponse-objects as value to a dart map
|
// maps a json object with a list of NotificationUpdateDto-objects as value to a dart map
|
||||||
static Map<String, List<AvatarResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
static Map<String, List<NotificationUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
final map = <String, List<AvatarResponse>>{};
|
final map = <String, List<NotificationUpdateDto>>{};
|
||||||
if (json is Map && json.isNotEmpty) {
|
if (json is Map && json.isNotEmpty) {
|
||||||
// ignore: parameter_assignments
|
// ignore: parameter_assignments
|
||||||
json = json.cast<String, dynamic>();
|
json = json.cast<String, dynamic>();
|
||||||
for (final entry in json.entries) {
|
for (final entry in json.entries) {
|
||||||
map[entry.key] = AvatarResponse.listFromJson(entry.value, growable: growable,);
|
map[entry.key] = NotificationUpdateDto.listFromJson(entry.value, growable: growable,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
@ -93,7 +97,6 @@ class AvatarResponse {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'color',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
12
mobile/openapi/lib/model/permission.dart
generated
12
mobile/openapi/lib/model/permission.dart
generated
@ -66,6 +66,10 @@ class Permission {
|
|||||||
static const memoryPeriodRead = Permission._(r'memory.read');
|
static const memoryPeriodRead = Permission._(r'memory.read');
|
||||||
static const memoryPeriodUpdate = Permission._(r'memory.update');
|
static const memoryPeriodUpdate = Permission._(r'memory.update');
|
||||||
static const memoryPeriodDelete = Permission._(r'memory.delete');
|
static const memoryPeriodDelete = Permission._(r'memory.delete');
|
||||||
|
static const notificationPeriodCreate = Permission._(r'notification.create');
|
||||||
|
static const notificationPeriodRead = Permission._(r'notification.read');
|
||||||
|
static const notificationPeriodUpdate = Permission._(r'notification.update');
|
||||||
|
static const notificationPeriodDelete = Permission._(r'notification.delete');
|
||||||
static const partnerPeriodCreate = Permission._(r'partner.create');
|
static const partnerPeriodCreate = Permission._(r'partner.create');
|
||||||
static const partnerPeriodRead = Permission._(r'partner.read');
|
static const partnerPeriodRead = Permission._(r'partner.read');
|
||||||
static const partnerPeriodUpdate = Permission._(r'partner.update');
|
static const partnerPeriodUpdate = Permission._(r'partner.update');
|
||||||
@ -147,6 +151,10 @@ class Permission {
|
|||||||
memoryPeriodRead,
|
memoryPeriodRead,
|
||||||
memoryPeriodUpdate,
|
memoryPeriodUpdate,
|
||||||
memoryPeriodDelete,
|
memoryPeriodDelete,
|
||||||
|
notificationPeriodCreate,
|
||||||
|
notificationPeriodRead,
|
||||||
|
notificationPeriodUpdate,
|
||||||
|
notificationPeriodDelete,
|
||||||
partnerPeriodCreate,
|
partnerPeriodCreate,
|
||||||
partnerPeriodRead,
|
partnerPeriodRead,
|
||||||
partnerPeriodUpdate,
|
partnerPeriodUpdate,
|
||||||
@ -263,6 +271,10 @@ class PermissionTypeTransformer {
|
|||||||
case r'memory.read': return Permission.memoryPeriodRead;
|
case r'memory.read': return Permission.memoryPeriodRead;
|
||||||
case r'memory.update': return Permission.memoryPeriodUpdate;
|
case r'memory.update': return Permission.memoryPeriodUpdate;
|
||||||
case r'memory.delete': return Permission.memoryPeriodDelete;
|
case r'memory.delete': return Permission.memoryPeriodDelete;
|
||||||
|
case r'notification.create': return Permission.notificationPeriodCreate;
|
||||||
|
case r'notification.read': return Permission.notificationPeriodRead;
|
||||||
|
case r'notification.update': return Permission.notificationPeriodUpdate;
|
||||||
|
case r'notification.delete': return Permission.notificationPeriodDelete;
|
||||||
case r'partner.create': return Permission.partnerPeriodCreate;
|
case r'partner.create': return Permission.partnerPeriodCreate;
|
||||||
case r'partner.read': return Permission.partnerPeriodRead;
|
case r'partner.read': return Permission.partnerPeriodRead;
|
||||||
case r'partner.update': return Permission.partnerPeriodUpdate;
|
case r'partner.update': return Permission.partnerPeriodUpdate;
|
||||||
|
13
mobile/openapi/lib/model/user_admin_create_dto.dart
generated
13
mobile/openapi/lib/model/user_admin_create_dto.dart
generated
@ -13,6 +13,7 @@ part of openapi.api;
|
|||||||
class UserAdminCreateDto {
|
class UserAdminCreateDto {
|
||||||
/// Returns a new [UserAdminCreateDto] instance.
|
/// Returns a new [UserAdminCreateDto] instance.
|
||||||
UserAdminCreateDto({
|
UserAdminCreateDto({
|
||||||
|
this.avatarColor,
|
||||||
required this.email,
|
required this.email,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.notify,
|
this.notify,
|
||||||
@ -22,6 +23,8 @@ class UserAdminCreateDto {
|
|||||||
this.storageLabel,
|
this.storageLabel,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UserAvatarColor? avatarColor;
|
||||||
|
|
||||||
String email;
|
String email;
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
@ -51,6 +54,7 @@ class UserAdminCreateDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserAdminCreateDto &&
|
||||||
|
other.avatarColor == avatarColor &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.notify == notify &&
|
other.notify == notify &&
|
||||||
@ -62,6 +66,7 @@ class UserAdminCreateDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
||||||
(email.hashCode) +
|
(email.hashCode) +
|
||||||
(name.hashCode) +
|
(name.hashCode) +
|
||||||
(notify == null ? 0 : notify!.hashCode) +
|
(notify == null ? 0 : notify!.hashCode) +
|
||||||
@ -71,10 +76,15 @@ class UserAdminCreateDto {
|
|||||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserAdminCreateDto[email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
String toString() => 'UserAdminCreateDto[avatarColor=$avatarColor, email=$email, name=$name, notify=$notify, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.avatarColor != null) {
|
||||||
|
json[r'avatarColor'] = this.avatarColor;
|
||||||
|
} else {
|
||||||
|
// json[r'avatarColor'] = null;
|
||||||
|
}
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
json[r'name'] = this.name;
|
json[r'name'] = this.name;
|
||||||
if (this.notify != null) {
|
if (this.notify != null) {
|
||||||
@ -110,6 +120,7 @@ class UserAdminCreateDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UserAdminCreateDto(
|
return UserAdminCreateDto(
|
||||||
|
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
||||||
email: mapValueOfType<String>(json, r'email')!,
|
email: mapValueOfType<String>(json, r'email')!,
|
||||||
name: mapValueOfType<String>(json, r'name')!,
|
name: mapValueOfType<String>(json, r'name')!,
|
||||||
notify: mapValueOfType<bool>(json, r'notify'),
|
notify: mapValueOfType<bool>(json, r'notify'),
|
||||||
|
13
mobile/openapi/lib/model/user_admin_update_dto.dart
generated
13
mobile/openapi/lib/model/user_admin_update_dto.dart
generated
@ -13,6 +13,7 @@ part of openapi.api;
|
|||||||
class UserAdminUpdateDto {
|
class UserAdminUpdateDto {
|
||||||
/// Returns a new [UserAdminUpdateDto] instance.
|
/// Returns a new [UserAdminUpdateDto] instance.
|
||||||
UserAdminUpdateDto({
|
UserAdminUpdateDto({
|
||||||
|
this.avatarColor,
|
||||||
this.email,
|
this.email,
|
||||||
this.name,
|
this.name,
|
||||||
this.password,
|
this.password,
|
||||||
@ -21,6 +22,8 @@ class UserAdminUpdateDto {
|
|||||||
this.storageLabel,
|
this.storageLabel,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UserAvatarColor? avatarColor;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@ -60,6 +63,7 @@ class UserAdminUpdateDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserAdminUpdateDto &&
|
||||||
|
other.avatarColor == avatarColor &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.password == password &&
|
other.password == password &&
|
||||||
@ -70,6 +74,7 @@ class UserAdminUpdateDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
||||||
(email == null ? 0 : email!.hashCode) +
|
(email == null ? 0 : email!.hashCode) +
|
||||||
(name == null ? 0 : name!.hashCode) +
|
(name == null ? 0 : name!.hashCode) +
|
||||||
(password == null ? 0 : password!.hashCode) +
|
(password == null ? 0 : password!.hashCode) +
|
||||||
@ -78,10 +83,15 @@ class UserAdminUpdateDto {
|
|||||||
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
(storageLabel == null ? 0 : storageLabel!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserAdminUpdateDto[email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
String toString() => 'UserAdminUpdateDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.avatarColor != null) {
|
||||||
|
json[r'avatarColor'] = this.avatarColor;
|
||||||
|
} else {
|
||||||
|
// json[r'avatarColor'] = null;
|
||||||
|
}
|
||||||
if (this.email != null) {
|
if (this.email != null) {
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
} else {
|
} else {
|
||||||
@ -124,6 +134,7 @@ class UserAdminUpdateDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UserAdminUpdateDto(
|
return UserAdminUpdateDto(
|
||||||
|
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
||||||
email: mapValueOfType<String>(json, r'email'),
|
email: mapValueOfType<String>(json, r'email'),
|
||||||
name: mapValueOfType<String>(json, r'name'),
|
name: mapValueOfType<String>(json, r'name'),
|
||||||
password: mapValueOfType<String>(json, r'password'),
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
|
@ -13,7 +13,6 @@ part of openapi.api;
|
|||||||
class UserPreferencesResponseDto {
|
class UserPreferencesResponseDto {
|
||||||
/// Returns a new [UserPreferencesResponseDto] instance.
|
/// Returns a new [UserPreferencesResponseDto] instance.
|
||||||
UserPreferencesResponseDto({
|
UserPreferencesResponseDto({
|
||||||
required this.avatar,
|
|
||||||
required this.download,
|
required this.download,
|
||||||
required this.emailNotifications,
|
required this.emailNotifications,
|
||||||
required this.folders,
|
required this.folders,
|
||||||
@ -25,8 +24,6 @@ class UserPreferencesResponseDto {
|
|||||||
required this.tags,
|
required this.tags,
|
||||||
});
|
});
|
||||||
|
|
||||||
AvatarResponse avatar;
|
|
||||||
|
|
||||||
DownloadResponse download;
|
DownloadResponse download;
|
||||||
|
|
||||||
EmailNotificationsResponse emailNotifications;
|
EmailNotificationsResponse emailNotifications;
|
||||||
@ -47,7 +44,6 @@ class UserPreferencesResponseDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto &&
|
||||||
other.avatar == avatar &&
|
|
||||||
other.download == download &&
|
other.download == download &&
|
||||||
other.emailNotifications == emailNotifications &&
|
other.emailNotifications == emailNotifications &&
|
||||||
other.folders == folders &&
|
other.folders == folders &&
|
||||||
@ -61,7 +57,6 @@ class UserPreferencesResponseDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(avatar.hashCode) +
|
|
||||||
(download.hashCode) +
|
(download.hashCode) +
|
||||||
(emailNotifications.hashCode) +
|
(emailNotifications.hashCode) +
|
||||||
(folders.hashCode) +
|
(folders.hashCode) +
|
||||||
@ -73,11 +68,10 @@ class UserPreferencesResponseDto {
|
|||||||
(tags.hashCode);
|
(tags.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
|
String toString() => 'UserPreferencesResponseDto[download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'avatar'] = this.avatar;
|
|
||||||
json[r'download'] = this.download;
|
json[r'download'] = this.download;
|
||||||
json[r'emailNotifications'] = this.emailNotifications;
|
json[r'emailNotifications'] = this.emailNotifications;
|
||||||
json[r'folders'] = this.folders;
|
json[r'folders'] = this.folders;
|
||||||
@ -99,7 +93,6 @@ class UserPreferencesResponseDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UserPreferencesResponseDto(
|
return UserPreferencesResponseDto(
|
||||||
avatar: AvatarResponse.fromJson(json[r'avatar'])!,
|
|
||||||
download: DownloadResponse.fromJson(json[r'download'])!,
|
download: DownloadResponse.fromJson(json[r'download'])!,
|
||||||
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
|
emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!,
|
||||||
folders: FoldersResponse.fromJson(json[r'folders'])!,
|
folders: FoldersResponse.fromJson(json[r'folders'])!,
|
||||||
@ -156,7 +149,6 @@ class UserPreferencesResponseDto {
|
|||||||
|
|
||||||
/// The list of required keys that must be present in a JSON.
|
/// The list of required keys that must be present in a JSON.
|
||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'avatar',
|
|
||||||
'download',
|
'download',
|
||||||
'emailNotifications',
|
'emailNotifications',
|
||||||
'folders',
|
'folders',
|
||||||
|
13
mobile/openapi/lib/model/user_update_me_dto.dart
generated
13
mobile/openapi/lib/model/user_update_me_dto.dart
generated
@ -13,11 +13,14 @@ part of openapi.api;
|
|||||||
class UserUpdateMeDto {
|
class UserUpdateMeDto {
|
||||||
/// Returns a new [UserUpdateMeDto] instance.
|
/// Returns a new [UserUpdateMeDto] instance.
|
||||||
UserUpdateMeDto({
|
UserUpdateMeDto({
|
||||||
|
this.avatarColor,
|
||||||
this.email,
|
this.email,
|
||||||
this.name,
|
this.name,
|
||||||
this.password,
|
this.password,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
UserAvatarColor? avatarColor;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
@ -44,6 +47,7 @@ class UserUpdateMeDto {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UserUpdateMeDto &&
|
||||||
|
other.avatarColor == avatarColor &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.name == name &&
|
other.name == name &&
|
||||||
other.password == password;
|
other.password == password;
|
||||||
@ -51,15 +55,21 @@ class UserUpdateMeDto {
|
|||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
|
(avatarColor == null ? 0 : avatarColor!.hashCode) +
|
||||||
(email == null ? 0 : email!.hashCode) +
|
(email == null ? 0 : email!.hashCode) +
|
||||||
(name == null ? 0 : name!.hashCode) +
|
(name == null ? 0 : name!.hashCode) +
|
||||||
(password == null ? 0 : password!.hashCode);
|
(password == null ? 0 : password!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UserUpdateMeDto[email=$email, name=$name, password=$password]';
|
String toString() => 'UserUpdateMeDto[avatarColor=$avatarColor, email=$email, name=$name, password=$password]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
if (this.avatarColor != null) {
|
||||||
|
json[r'avatarColor'] = this.avatarColor;
|
||||||
|
} else {
|
||||||
|
// json[r'avatarColor'] = null;
|
||||||
|
}
|
||||||
if (this.email != null) {
|
if (this.email != null) {
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
} else {
|
} else {
|
||||||
@ -87,6 +97,7 @@ class UserUpdateMeDto {
|
|||||||
final json = value.cast<String, dynamic>();
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
return UserUpdateMeDto(
|
return UserUpdateMeDto(
|
||||||
|
avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']),
|
||||||
email: mapValueOfType<String>(json, r'email'),
|
email: mapValueOfType<String>(json, r'email'),
|
||||||
name: mapValueOfType<String>(json, r'name'),
|
name: mapValueOfType<String>(json, r'name'),
|
||||||
password: mapValueOfType<String>(json, r'password'),
|
password: mapValueOfType<String>(json, r'password'),
|
||||||
|
@ -206,6 +206,141 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/admin/notifications": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "createNotification",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationCreateDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications (Admin)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/notifications/templates/{name}": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "getNotificationTemplateAdmin",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TemplateDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TemplateResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications (Admin)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/admin/notifications/test-email": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "sendTestEmailAdmin",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SystemConfigSmtpDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TestEmailResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications (Admin)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/admin/users": {
|
"/admin/users": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "searchUsersAdmin",
|
"operationId": "searchUsersAdmin",
|
||||||
@ -3485,15 +3620,224 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/notifications/admin/templates/{name}": {
|
"/notifications": {
|
||||||
"post": {
|
"delete": {
|
||||||
"operationId": "getNotificationTemplateAdmin",
|
"operationId": "deleteNotifications",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationDeleteAllDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "getNotifications",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "id",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/NotificationDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "updateNotifications",
|
||||||
|
"parameters": [],
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationUpdateAllDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/notifications/{id}": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "deleteNotification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
"required": true,
|
"required": true,
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"operationId": "getNotification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/NotificationDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Notifications"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"operationId": "updateNotification",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3502,7 +3846,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/TemplateDto"
|
"$ref": "#/components/schemas/NotificationUpdateDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -3513,7 +3857,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/TemplateResponseDto"
|
"$ref": "#/components/schemas/NotificationDto"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -3532,49 +3876,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"Notifications (Admin)"
|
"Notifications"
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/notifications/admin/test-email": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "sendTestEmailAdmin",
|
|
||||||
"parameters": [],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SystemConfigSmtpDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TestEmailResponseDto"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Notifications (Admin)"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -8884,21 +9186,6 @@
|
|||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"AvatarResponse": {
|
|
||||||
"properties": {
|
|
||||||
"color": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/UserAvatarColor"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"color"
|
|
||||||
],
|
|
||||||
"type": "object"
|
|
||||||
},
|
|
||||||
"AvatarUpdate": {
|
"AvatarUpdate": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"color": {
|
"color": {
|
||||||
@ -10341,6 +10628,157 @@
|
|||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"NotificationCreateDto": {
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NotificationLevel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"readAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NotificationType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"userId": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"title",
|
||||||
|
"userId"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"NotificationDeleteAllDto": {
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"items": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"ids"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"NotificationDto": {
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NotificationLevel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"readAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/NotificationType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"createdAt",
|
||||||
|
"id",
|
||||||
|
"level",
|
||||||
|
"title",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"NotificationLevel": {
|
||||||
|
"enum": [
|
||||||
|
"success",
|
||||||
|
"error",
|
||||||
|
"warning",
|
||||||
|
"info"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"NotificationType": {
|
||||||
|
"enum": [
|
||||||
|
"JobFailed",
|
||||||
|
"BackupFailed",
|
||||||
|
"SystemMessage",
|
||||||
|
"Custom"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"NotificationUpdateAllDto": {
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"items": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"readAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"ids"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"NotificationUpdateDto": {
|
||||||
|
"properties": {
|
||||||
|
"readAt": {
|
||||||
|
"format": "date-time",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"OAuthAuthorizeResponseDto": {
|
"OAuthAuthorizeResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"url": {
|
"url": {
|
||||||
@ -10615,6 +11053,10 @@
|
|||||||
"memory.read",
|
"memory.read",
|
||||||
"memory.update",
|
"memory.update",
|
||||||
"memory.delete",
|
"memory.delete",
|
||||||
|
"notification.create",
|
||||||
|
"notification.read",
|
||||||
|
"notification.update",
|
||||||
|
"notification.delete",
|
||||||
"partner.create",
|
"partner.create",
|
||||||
"partner.read",
|
"partner.read",
|
||||||
"partner.update",
|
"partner.update",
|
||||||
@ -13621,6 +14063,14 @@
|
|||||||
},
|
},
|
||||||
"UserAdminCreateDto": {
|
"UserAdminCreateDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"format": "email",
|
"format": "email",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -13763,6 +14213,14 @@
|
|||||||
},
|
},
|
||||||
"UserAdminUpdateDto": {
|
"UserAdminUpdateDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"format": "email",
|
"format": "email",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -13826,9 +14284,6 @@
|
|||||||
},
|
},
|
||||||
"UserPreferencesResponseDto": {
|
"UserPreferencesResponseDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"avatar": {
|
|
||||||
"$ref": "#/components/schemas/AvatarResponse"
|
|
||||||
},
|
|
||||||
"download": {
|
"download": {
|
||||||
"$ref": "#/components/schemas/DownloadResponse"
|
"$ref": "#/components/schemas/DownloadResponse"
|
||||||
},
|
},
|
||||||
@ -13858,7 +14313,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"avatar",
|
|
||||||
"download",
|
"download",
|
||||||
"emailNotifications",
|
"emailNotifications",
|
||||||
"folders",
|
"folders",
|
||||||
@ -13952,6 +14406,14 @@
|
|||||||
},
|
},
|
||||||
"UserUpdateMeDto": {
|
"UserUpdateMeDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"avatarColor": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/UserAvatarColor"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"format": "email",
|
"format": "email",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -39,6 +39,48 @@ export type ActivityCreateDto = {
|
|||||||
export type ActivityStatisticsResponseDto = {
|
export type ActivityStatisticsResponseDto = {
|
||||||
comments: number;
|
comments: number;
|
||||||
};
|
};
|
||||||
|
export type NotificationCreateDto = {
|
||||||
|
data?: object;
|
||||||
|
description?: string | null;
|
||||||
|
level?: NotificationLevel;
|
||||||
|
readAt?: string | null;
|
||||||
|
title: string;
|
||||||
|
"type"?: NotificationType;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
export type NotificationDto = {
|
||||||
|
createdAt: string;
|
||||||
|
data?: object;
|
||||||
|
description?: string;
|
||||||
|
id: string;
|
||||||
|
level: NotificationLevel;
|
||||||
|
readAt?: string;
|
||||||
|
title: string;
|
||||||
|
"type": NotificationType;
|
||||||
|
};
|
||||||
|
export type TemplateDto = {
|
||||||
|
template: string;
|
||||||
|
};
|
||||||
|
export type TemplateResponseDto = {
|
||||||
|
html: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
export type SystemConfigSmtpTransportDto = {
|
||||||
|
host: string;
|
||||||
|
ignoreCert: boolean;
|
||||||
|
password: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
export type SystemConfigSmtpDto = {
|
||||||
|
enabled: boolean;
|
||||||
|
"from": string;
|
||||||
|
replyTo: string;
|
||||||
|
transport: SystemConfigSmtpTransportDto;
|
||||||
|
};
|
||||||
|
export type TestEmailResponseDto = {
|
||||||
|
messageId: string;
|
||||||
|
};
|
||||||
export type UserLicense = {
|
export type UserLicense = {
|
||||||
activatedAt: string;
|
activatedAt: string;
|
||||||
activationKey: string;
|
activationKey: string;
|
||||||
@ -64,6 +106,7 @@ export type UserAdminResponseDto = {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
export type UserAdminCreateDto = {
|
export type UserAdminCreateDto = {
|
||||||
|
avatarColor?: (UserAvatarColor) | null;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
notify?: boolean;
|
notify?: boolean;
|
||||||
@ -76,6 +119,7 @@ export type UserAdminDeleteDto = {
|
|||||||
force?: boolean;
|
force?: boolean;
|
||||||
};
|
};
|
||||||
export type UserAdminUpdateDto = {
|
export type UserAdminUpdateDto = {
|
||||||
|
avatarColor?: (UserAvatarColor) | null;
|
||||||
email?: string;
|
email?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
@ -83,9 +127,6 @@ export type UserAdminUpdateDto = {
|
|||||||
shouldChangePassword?: boolean;
|
shouldChangePassword?: boolean;
|
||||||
storageLabel?: string | null;
|
storageLabel?: string | null;
|
||||||
};
|
};
|
||||||
export type AvatarResponse = {
|
|
||||||
color: UserAvatarColor;
|
|
||||||
};
|
|
||||||
export type DownloadResponse = {
|
export type DownloadResponse = {
|
||||||
archiveSize: number;
|
archiveSize: number;
|
||||||
includeEmbeddedVideos: boolean;
|
includeEmbeddedVideos: boolean;
|
||||||
@ -122,7 +163,6 @@ export type TagsResponse = {
|
|||||||
sidebarWeb: boolean;
|
sidebarWeb: boolean;
|
||||||
};
|
};
|
||||||
export type UserPreferencesResponseDto = {
|
export type UserPreferencesResponseDto = {
|
||||||
avatar: AvatarResponse;
|
|
||||||
download: DownloadResponse;
|
download: DownloadResponse;
|
||||||
emailNotifications: EmailNotificationsResponse;
|
emailNotifications: EmailNotificationsResponse;
|
||||||
folders: FoldersResponse;
|
folders: FoldersResponse;
|
||||||
@ -663,28 +703,15 @@ export type MemoryUpdateDto = {
|
|||||||
memoryAt?: string;
|
memoryAt?: string;
|
||||||
seenAt?: string;
|
seenAt?: string;
|
||||||
};
|
};
|
||||||
export type TemplateDto = {
|
export type NotificationDeleteAllDto = {
|
||||||
template: string;
|
ids: string[];
|
||||||
};
|
};
|
||||||
export type TemplateResponseDto = {
|
export type NotificationUpdateAllDto = {
|
||||||
html: string;
|
ids: string[];
|
||||||
name: string;
|
readAt?: string | null;
|
||||||
};
|
};
|
||||||
export type SystemConfigSmtpTransportDto = {
|
export type NotificationUpdateDto = {
|
||||||
host: string;
|
readAt?: string | null;
|
||||||
ignoreCert: boolean;
|
|
||||||
password: string;
|
|
||||||
port: number;
|
|
||||||
username: string;
|
|
||||||
};
|
|
||||||
export type SystemConfigSmtpDto = {
|
|
||||||
enabled: boolean;
|
|
||||||
"from": string;
|
|
||||||
replyTo: string;
|
|
||||||
transport: SystemConfigSmtpTransportDto;
|
|
||||||
};
|
|
||||||
export type TestEmailResponseDto = {
|
|
||||||
messageId: string;
|
|
||||||
};
|
};
|
||||||
export type OAuthConfigDto = {
|
export type OAuthConfigDto = {
|
||||||
codeChallenge?: string;
|
codeChallenge?: string;
|
||||||
@ -1388,6 +1415,7 @@ export type TrashResponseDto = {
|
|||||||
count: number;
|
count: number;
|
||||||
};
|
};
|
||||||
export type UserUpdateMeDto = {
|
export type UserUpdateMeDto = {
|
||||||
|
avatarColor?: (UserAvatarColor) | null;
|
||||||
email?: string;
|
email?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
@ -1454,6 +1482,43 @@ export function deleteActivity({ id }: {
|
|||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
export function createNotification({ notificationCreateDto }: {
|
||||||
|
notificationCreateDto: NotificationCreateDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 201;
|
||||||
|
data: NotificationDto;
|
||||||
|
}>("/admin/notifications", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: notificationCreateDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function getNotificationTemplateAdmin({ name, templateDto }: {
|
||||||
|
name: string;
|
||||||
|
templateDto: TemplateDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: TemplateResponseDto;
|
||||||
|
}>(`/admin/notifications/templates/${encodeURIComponent(name)}`, oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: templateDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function sendTestEmailAdmin({ systemConfigSmtpDto }: {
|
||||||
|
systemConfigSmtpDto: SystemConfigSmtpDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: TestEmailResponseDto;
|
||||||
|
}>("/admin/notifications/test-email", oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "POST",
|
||||||
|
body: systemConfigSmtpDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
export function searchUsersAdmin({ withDeleted }: {
|
export function searchUsersAdmin({ withDeleted }: {
|
||||||
withDeleted?: boolean;
|
withDeleted?: boolean;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
@ -2322,29 +2387,71 @@ export function addMemoryAssets({ id, bulkIdsDto }: {
|
|||||||
body: bulkIdsDto
|
body: bulkIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function getNotificationTemplateAdmin({ name, templateDto }: {
|
export function deleteNotifications({ notificationDeleteAllDto }: {
|
||||||
name: string;
|
notificationDeleteAllDto: NotificationDeleteAllDto;
|
||||||
templateDto: TemplateDto;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({
|
||||||
status: 200;
|
|
||||||
data: TemplateResponseDto;
|
|
||||||
}>(`/notifications/admin/templates/${encodeURIComponent(name)}`, oazapfts.json({
|
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "DELETE",
|
||||||
body: templateDto
|
body: notificationDeleteAllDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function sendTestEmailAdmin({ systemConfigSmtpDto }: {
|
export function getNotifications({ id, level, $type, unread }: {
|
||||||
systemConfigSmtpDto: SystemConfigSmtpDto;
|
id?: string;
|
||||||
|
level?: NotificationLevel;
|
||||||
|
$type?: NotificationType;
|
||||||
|
unread?: boolean;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: TestEmailResponseDto;
|
data: NotificationDto[];
|
||||||
}>("/notifications/admin/test-email", oazapfts.json({
|
}>(`/notifications${QS.query(QS.explode({
|
||||||
|
id,
|
||||||
|
level,
|
||||||
|
"type": $type,
|
||||||
|
unread
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function updateNotifications({ notificationUpdateAllDto }: {
|
||||||
|
notificationUpdateAllDto: NotificationUpdateAllDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText("/notifications", oazapfts.json({
|
||||||
...opts,
|
...opts,
|
||||||
method: "POST",
|
method: "PUT",
|
||||||
body: systemConfigSmtpDto
|
body: notificationUpdateAllDto
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
export function deleteNotification({ id }: {
|
||||||
|
id: string;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchText(`/notifications/${encodeURIComponent(id)}`, {
|
||||||
|
...opts,
|
||||||
|
method: "DELETE"
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function getNotification({ id }: {
|
||||||
|
id: string;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: NotificationDto;
|
||||||
|
}>(`/notifications/${encodeURIComponent(id)}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function updateNotification({ id, notificationUpdateDto }: {
|
||||||
|
id: string;
|
||||||
|
notificationUpdateDto: NotificationUpdateDto;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: NotificationDto;
|
||||||
|
}>(`/notifications/${encodeURIComponent(id)}`, oazapfts.json({
|
||||||
|
...opts,
|
||||||
|
method: "PUT",
|
||||||
|
body: notificationUpdateDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
export function startOAuth({ oAuthConfigDto }: {
|
export function startOAuth({ oAuthConfigDto }: {
|
||||||
@ -3453,6 +3560,18 @@ export enum UserAvatarColor {
|
|||||||
Gray = "gray",
|
Gray = "gray",
|
||||||
Amber = "amber"
|
Amber = "amber"
|
||||||
}
|
}
|
||||||
|
export enum NotificationLevel {
|
||||||
|
Success = "success",
|
||||||
|
Error = "error",
|
||||||
|
Warning = "warning",
|
||||||
|
Info = "info"
|
||||||
|
}
|
||||||
|
export enum NotificationType {
|
||||||
|
JobFailed = "JobFailed",
|
||||||
|
BackupFailed = "BackupFailed",
|
||||||
|
SystemMessage = "SystemMessage",
|
||||||
|
Custom = "Custom"
|
||||||
|
}
|
||||||
export enum UserStatus {
|
export enum UserStatus {
|
||||||
Active = "active",
|
Active = "active",
|
||||||
Removing = "removing",
|
Removing = "removing",
|
||||||
@ -3527,6 +3646,10 @@ export enum Permission {
|
|||||||
MemoryRead = "memory.read",
|
MemoryRead = "memory.read",
|
||||||
MemoryUpdate = "memory.update",
|
MemoryUpdate = "memory.update",
|
||||||
MemoryDelete = "memory.delete",
|
MemoryDelete = "memory.delete",
|
||||||
|
NotificationCreate = "notification.create",
|
||||||
|
NotificationRead = "notification.read",
|
||||||
|
NotificationUpdate = "notification.update",
|
||||||
|
NotificationDelete = "notification.delete",
|
||||||
PartnerCreate = "partner.create",
|
PartnerCreate = "partner.create",
|
||||||
PartnerRead = "partner.read",
|
PartnerRead = "partner.read",
|
||||||
PartnerUpdate = "partner.update",
|
PartnerUpdate = "partner.update",
|
||||||
|
66
server/package-lock.json
generated
66
server/package-lock.json
generated
@ -32,6 +32,7 @@
|
|||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"compression": "^1.8.0",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
@ -83,6 +84,7 @@
|
|||||||
"@types/archiver": "^6.0.0",
|
"@types/archiver": "^6.0.0",
|
||||||
"@types/async-lock": "^1.4.2",
|
"@types/async-lock": "^1.4.2",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cookie-parser": "^1.4.8",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fluent-ffmpeg": "^2.1.21",
|
"@types/fluent-ffmpeg": "^2.1.21",
|
||||||
@ -5009,6 +5011,16 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/compression": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/connect": {
|
"node_modules/@types/connect": {
|
||||||
"version": "3.4.38",
|
"version": "3.4.38",
|
||||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
@ -7603,6 +7615,60 @@
|
|||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/compressible": {
|
||||||
|
"version": "2.0.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
|
||||||
|
"integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": ">= 1.43.0 < 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"compressible": "~2.0.18",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"negotiator": "~0.6.4",
|
||||||
|
"on-headers": "~1.0.2",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/compression/node_modules/negotiator": {
|
||||||
|
"version": "0.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
|
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
"chokidar": "^3.5.3",
|
"chokidar": "^3.5.3",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.0",
|
"class-validator": "^0.14.0",
|
||||||
|
"compression": "^1.8.0",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
@ -108,6 +109,7 @@
|
|||||||
"@types/archiver": "^6.0.0",
|
"@types/archiver": "^6.0.0",
|
||||||
"@types/async-lock": "^1.4.2",
|
"@types/async-lock": "^1.4.2",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
|
"@types/compression": "^1.7.5",
|
||||||
"@types/cookie-parser": "^1.4.8",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/fluent-ffmpeg": "^2.1.21",
|
"@types/fluent-ffmpeg": "^2.1.21",
|
||||||
|
@ -14,6 +14,7 @@ import { LibraryController } from 'src/controllers/library.controller';
|
|||||||
import { MapController } from 'src/controllers/map.controller';
|
import { MapController } from 'src/controllers/map.controller';
|
||||||
import { MemoryController } from 'src/controllers/memory.controller';
|
import { MemoryController } from 'src/controllers/memory.controller';
|
||||||
import { NotificationAdminController } from 'src/controllers/notification-admin.controller';
|
import { NotificationAdminController } from 'src/controllers/notification-admin.controller';
|
||||||
|
import { NotificationController } from 'src/controllers/notification.controller';
|
||||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
import { PartnerController } from 'src/controllers/partner.controller';
|
import { PartnerController } from 'src/controllers/partner.controller';
|
||||||
import { PersonController } from 'src/controllers/person.controller';
|
import { PersonController } from 'src/controllers/person.controller';
|
||||||
@ -47,6 +48,7 @@ export const controllers = [
|
|||||||
LibraryController,
|
LibraryController,
|
||||||
MapController,
|
MapController,
|
||||||
MemoryController,
|
MemoryController,
|
||||||
|
NotificationController,
|
||||||
NotificationAdminController,
|
NotificationAdminController,
|
||||||
OAuthController,
|
OAuthController,
|
||||||
PartnerController,
|
PartnerController,
|
||||||
|
@ -1,16 +1,28 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
|
import { Body, Controller, HttpCode, HttpStatus, Param, Post } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { TemplateDto, TemplateResponseDto, TestEmailResponseDto } from 'src/dtos/notification.dto';
|
import {
|
||||||
|
NotificationCreateDto,
|
||||||
|
NotificationDto,
|
||||||
|
TemplateDto,
|
||||||
|
TemplateResponseDto,
|
||||||
|
TestEmailResponseDto,
|
||||||
|
} from 'src/dtos/notification.dto';
|
||||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||||
import { NotificationService } from 'src/services/notification.service';
|
import { NotificationAdminService } from 'src/services/notification-admin.service';
|
||||||
|
|
||||||
@ApiTags('Notifications (Admin)')
|
@ApiTags('Notifications (Admin)')
|
||||||
@Controller('notifications/admin')
|
@Controller('admin/notifications')
|
||||||
export class NotificationAdminController {
|
export class NotificationAdminController {
|
||||||
constructor(private service: NotificationService) {}
|
constructor(private service: NotificationAdminService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@Authenticated({ admin: true })
|
||||||
|
createNotification(@Auth() auth: AuthDto, @Body() dto: NotificationCreateDto): Promise<NotificationDto> {
|
||||||
|
return this.service.create(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('test-email')
|
@Post('test-email')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
|
60
server/src/controllers/notification.controller.ts
Normal file
60
server/src/controllers/notification.controller.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Body, Controller, Delete, Get, Param, Put, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import {
|
||||||
|
NotificationDeleteAllDto,
|
||||||
|
NotificationDto,
|
||||||
|
NotificationSearchDto,
|
||||||
|
NotificationUpdateAllDto,
|
||||||
|
NotificationUpdateDto,
|
||||||
|
} from 'src/dtos/notification.dto';
|
||||||
|
import { Permission } from 'src/enum';
|
||||||
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { NotificationService } from 'src/services/notification.service';
|
||||||
|
import { UUIDParamDto } from 'src/validation';
|
||||||
|
|
||||||
|
@ApiTags('Notifications')
|
||||||
|
@Controller('notifications')
|
||||||
|
export class NotificationController {
|
||||||
|
constructor(private service: NotificationService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_READ })
|
||||||
|
getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise<NotificationDto[]> {
|
||||||
|
return this.service.search(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE })
|
||||||
|
updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise<void> {
|
||||||
|
return this.service.updateAll(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete()
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_DELETE })
|
||||||
|
deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise<void> {
|
||||||
|
return this.service.deleteAll(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_READ })
|
||||||
|
getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<NotificationDto> {
|
||||||
|
return this.service.get(auth, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE })
|
||||||
|
updateNotification(
|
||||||
|
@Auth() auth: AuthDto,
|
||||||
|
@Param() { id }: UUIDParamDto,
|
||||||
|
@Body() dto: NotificationUpdateDto,
|
||||||
|
): Promise<NotificationDto> {
|
||||||
|
return this.service.update(auth, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
@Authenticated({ permission: Permission.NOTIFICATION_DELETE })
|
||||||
|
deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
|
||||||
|
return this.service.delete(auth, id);
|
||||||
|
}
|
||||||
|
}
|
@ -90,7 +90,7 @@ export class StorageCore {
|
|||||||
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`);
|
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: ImageFormat) {
|
static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') {
|
||||||
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`);
|
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
Permission,
|
Permission,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
SourceType,
|
SourceType,
|
||||||
|
UserAvatarColor,
|
||||||
UserStatus,
|
UserStatus,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
import { OnThisDayData, UserMetadataItem } from 'src/types';
|
||||||
@ -122,6 +123,7 @@ export type User = {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
avatarColor: UserAvatarColor | null;
|
||||||
profileImagePath: string;
|
profileImagePath: string;
|
||||||
profileChangedAt: Date;
|
profileChangedAt: Date;
|
||||||
};
|
};
|
||||||
@ -264,7 +266,15 @@ export type AssetFace = {
|
|||||||
person?: Person | null;
|
person?: Person | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const userColumns = ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'] as const;
|
const userColumns = ['id', 'name', 'email', 'avatarColor', 'profileImagePath', 'profileChangedAt'] as const;
|
||||||
|
const userWithPrefixColumns = [
|
||||||
|
'users.id',
|
||||||
|
'users.name',
|
||||||
|
'users.email',
|
||||||
|
'users.avatarColor',
|
||||||
|
'users.profileImagePath',
|
||||||
|
'users.profileChangedAt',
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const columns = {
|
export const columns = {
|
||||||
asset: [
|
asset: [
|
||||||
@ -306,7 +316,7 @@ export const columns = {
|
|||||||
'shared_links.password',
|
'shared_links.password',
|
||||||
],
|
],
|
||||||
user: userColumns,
|
user: userColumns,
|
||||||
userWithPrefix: ['users.id', 'users.name', 'users.email', 'users.profileImagePath', 'users.profileChangedAt'],
|
userWithPrefix: userWithPrefixColumns,
|
||||||
userAdmin: [
|
userAdmin: [
|
||||||
...userColumns,
|
...userColumns,
|
||||||
'createdAt',
|
'createdAt',
|
||||||
@ -323,6 +333,7 @@ export const columns = {
|
|||||||
],
|
],
|
||||||
tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'],
|
tag: ['tags.id', 'tags.value', 'tags.createdAt', 'tags.updatedAt', 'tags.color', 'tags.parentId'],
|
||||||
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'],
|
||||||
|
notification: ['id', 'createdAt', 'level', 'type', 'title', 'description', 'data', 'readAt'],
|
||||||
syncAsset: [
|
syncAsset: [
|
||||||
'id',
|
'id',
|
||||||
'ownerId',
|
'ownerId',
|
||||||
|
18
server/src/db.d.ts
vendored
18
server/src/db.d.ts
vendored
@ -11,6 +11,8 @@ import {
|
|||||||
AssetStatus,
|
AssetStatus,
|
||||||
AssetType,
|
AssetType,
|
||||||
MemoryType,
|
MemoryType,
|
||||||
|
NotificationLevel,
|
||||||
|
NotificationType,
|
||||||
Permission,
|
Permission,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
SourceType,
|
SourceType,
|
||||||
@ -263,6 +265,21 @@ export interface Memories {
|
|||||||
updateId: Generated<string>;
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Notifications {
|
||||||
|
id: Generated<string>;
|
||||||
|
createdAt: Generated<Timestamp>;
|
||||||
|
updatedAt: Generated<Timestamp>;
|
||||||
|
deletedAt: Timestamp | null;
|
||||||
|
updateId: Generated<string>;
|
||||||
|
userId: string;
|
||||||
|
level: Generated<NotificationLevel>;
|
||||||
|
type: NotificationType;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
data: any | null;
|
||||||
|
readAt: Timestamp | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface MemoriesAssetsAssets {
|
export interface MemoriesAssetsAssets {
|
||||||
assetsId: string;
|
assetsId: string;
|
||||||
memoriesId: string;
|
memoriesId: string;
|
||||||
@ -463,6 +480,7 @@ export interface DB {
|
|||||||
memories: Memories;
|
memories: Memories;
|
||||||
memories_assets_assets: MemoriesAssetsAssets;
|
memories_assets_assets: MemoriesAssetsAssets;
|
||||||
migrations: Migrations;
|
migrations: Migrations;
|
||||||
|
notifications: Notifications;
|
||||||
move_history: MoveHistory;
|
move_history: MoveHistory;
|
||||||
naturalearth_countries: NaturalearthCountries;
|
naturalearth_countries: NaturalearthCountries;
|
||||||
partners_audit: PartnersAudit;
|
partners_audit: PartnersAudit;
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { IsString } from 'class-validator';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsString } from 'class-validator';
|
||||||
|
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||||
|
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class TestEmailResponseDto {
|
export class TestEmailResponseDto {
|
||||||
messageId!: string;
|
messageId!: string;
|
||||||
@ -11,3 +14,106 @@ export class TemplateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
template!: string;
|
template!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class NotificationDto {
|
||||||
|
id!: string;
|
||||||
|
@ValidateDate()
|
||||||
|
createdAt!: Date;
|
||||||
|
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||||
|
level!: NotificationLevel;
|
||||||
|
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||||
|
type!: NotificationType;
|
||||||
|
title!: string;
|
||||||
|
description?: string;
|
||||||
|
data?: any;
|
||||||
|
readAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationSearchDto {
|
||||||
|
@Optional()
|
||||||
|
@ValidateUUID({ optional: true })
|
||||||
|
id?: string;
|
||||||
|
|
||||||
|
@IsEnum(NotificationLevel)
|
||||||
|
@Optional()
|
||||||
|
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||||
|
level?: NotificationLevel;
|
||||||
|
|
||||||
|
@IsEnum(NotificationType)
|
||||||
|
@Optional()
|
||||||
|
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||||
|
type?: NotificationType;
|
||||||
|
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
unread?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationCreateDto {
|
||||||
|
@Optional()
|
||||||
|
@IsEnum(NotificationLevel)
|
||||||
|
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||||
|
level?: NotificationLevel;
|
||||||
|
|
||||||
|
@IsEnum(NotificationType)
|
||||||
|
@Optional()
|
||||||
|
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||||
|
type?: NotificationType;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
description?: string | null;
|
||||||
|
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
data?: any;
|
||||||
|
|
||||||
|
@ValidateDate({ optional: true, nullable: true })
|
||||||
|
readAt?: Date | null;
|
||||||
|
|
||||||
|
@ValidateUUID()
|
||||||
|
userId!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationUpdateDto {
|
||||||
|
@ValidateDate({ optional: true, nullable: true })
|
||||||
|
readAt?: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationUpdateAllDto {
|
||||||
|
@ValidateUUID({ each: true, optional: true })
|
||||||
|
ids!: string[];
|
||||||
|
|
||||||
|
@ValidateDate({ optional: true, nullable: true })
|
||||||
|
readAt?: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NotificationDeleteAllDto {
|
||||||
|
@ValidateUUID({ each: true })
|
||||||
|
ids!: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MapNotification = {
|
||||||
|
id: string;
|
||||||
|
createdAt: Date;
|
||||||
|
updateId?: string;
|
||||||
|
level: NotificationLevel;
|
||||||
|
type: NotificationType;
|
||||||
|
data: any | null;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
readAt: Date | null;
|
||||||
|
};
|
||||||
|
export const mapNotification = (notification: MapNotification): NotificationDto => {
|
||||||
|
return {
|
||||||
|
id: notification.id,
|
||||||
|
createdAt: notification.createdAt,
|
||||||
|
level: notification.level,
|
||||||
|
type: notification.type,
|
||||||
|
title: notification.title,
|
||||||
|
description: notification.description ?? undefined,
|
||||||
|
data: notification.data ?? undefined,
|
||||||
|
readAt: notification.readAt ?? undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -137,11 +137,6 @@ export class UserPreferencesUpdateDto {
|
|||||||
purchase?: PurchaseUpdate;
|
purchase?: PurchaseUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvatarResponse {
|
|
||||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
|
||||||
color!: UserAvatarColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
class RatingsResponse {
|
class RatingsResponse {
|
||||||
enabled: boolean = false;
|
enabled: boolean = false;
|
||||||
}
|
}
|
||||||
@ -195,7 +190,6 @@ export class UserPreferencesResponseDto implements UserPreferences {
|
|||||||
ratings!: RatingsResponse;
|
ratings!: RatingsResponse;
|
||||||
sharedLinks!: SharedLinksResponse;
|
sharedLinks!: SharedLinksResponse;
|
||||||
tags!: TagsResponse;
|
tags!: TagsResponse;
|
||||||
avatar!: AvatarResponse;
|
|
||||||
emailNotifications!: EmailNotificationsResponse;
|
emailNotifications!: EmailNotificationsResponse;
|
||||||
download!: DownloadResponse;
|
download!: DownloadResponse;
|
||||||
purchase!: PurchaseResponse;
|
purchase!: PurchaseResponse;
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||||
import { User, UserAdmin } from 'src/database';
|
import { User, UserAdmin } from 'src/database';
|
||||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||||
import { UserMetadataItem } from 'src/types';
|
import { UserMetadataItem } from 'src/types';
|
||||||
import { getPreferences } from 'src/utils/preferences';
|
|
||||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
||||||
|
|
||||||
export class UserUpdateMeDto {
|
export class UserUpdateMeDto {
|
||||||
@ -23,6 +22,11 @@ export class UserUpdateMeDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
@IsEnum(UserAvatarColor)
|
||||||
|
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||||
|
avatarColor?: UserAvatarColor | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserResponseDto {
|
export class UserResponseDto {
|
||||||
@ -41,13 +45,21 @@ export class UserLicense {
|
|||||||
activatedAt!: Date;
|
activatedAt!: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emailToAvatarColor = (email: string): UserAvatarColor => {
|
||||||
|
const values = Object.values(UserAvatarColor);
|
||||||
|
const randomIndex = Math.floor(
|
||||||
|
[...email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
|
||||||
|
);
|
||||||
|
return values[randomIndex];
|
||||||
|
};
|
||||||
|
|
||||||
export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
|
export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
|
||||||
return {
|
return {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
email: entity.email,
|
email: entity.email,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
profileImagePath: entity.profileImagePath,
|
profileImagePath: entity.profileImagePath,
|
||||||
avatarColor: getPreferences(entity.email, (entity as UserAdmin).metadata || []).avatar.color,
|
avatarColor: entity.avatarColor ?? emailToAvatarColor(entity.email),
|
||||||
profileChangedAt: entity.profileChangedAt,
|
profileChangedAt: entity.profileChangedAt,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -69,6 +81,11 @@ export class UserAdminCreateDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
name!: string;
|
name!: string;
|
||||||
|
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
@IsEnum(UserAvatarColor)
|
||||||
|
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||||
|
avatarColor?: UserAvatarColor | null;
|
||||||
|
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Transform(toSanitized)
|
@Transform(toSanitized)
|
||||||
@ -104,6 +121,11 @@ export class UserAdminUpdateDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@Optional({ nullable: true })
|
||||||
|
@IsEnum(UserAvatarColor)
|
||||||
|
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||||
|
avatarColor?: UserAvatarColor | null;
|
||||||
|
|
||||||
@Optional({ nullable: true })
|
@Optional({ nullable: true })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Transform(toSanitized)
|
@Transform(toSanitized)
|
||||||
|
@ -126,6 +126,11 @@ export enum Permission {
|
|||||||
MEMORY_UPDATE = 'memory.update',
|
MEMORY_UPDATE = 'memory.update',
|
||||||
MEMORY_DELETE = 'memory.delete',
|
MEMORY_DELETE = 'memory.delete',
|
||||||
|
|
||||||
|
NOTIFICATION_CREATE = 'notification.create',
|
||||||
|
NOTIFICATION_READ = 'notification.read',
|
||||||
|
NOTIFICATION_UPDATE = 'notification.update',
|
||||||
|
NOTIFICATION_DELETE = 'notification.delete',
|
||||||
|
|
||||||
PARTNER_CREATE = 'partner.create',
|
PARTNER_CREATE = 'partner.create',
|
||||||
PARTNER_READ = 'partner.read',
|
PARTNER_READ = 'partner.read',
|
||||||
PARTNER_UPDATE = 'partner.update',
|
PARTNER_UPDATE = 'partner.update',
|
||||||
@ -332,6 +337,11 @@ export enum ImageFormat {
|
|||||||
WEBP = 'webp',
|
WEBP = 'webp',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum RawExtractedFormat {
|
||||||
|
JPEG = 'jpeg',
|
||||||
|
JXL = 'jxl',
|
||||||
|
}
|
||||||
|
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
VERBOSE = 'verbose',
|
VERBOSE = 'verbose',
|
||||||
DEBUG = 'debug',
|
DEBUG = 'debug',
|
||||||
@ -515,6 +525,7 @@ export enum JobName {
|
|||||||
NOTIFY_SIGNUP = 'notify-signup',
|
NOTIFY_SIGNUP = 'notify-signup',
|
||||||
NOTIFY_ALBUM_INVITE = 'notify-album-invite',
|
NOTIFY_ALBUM_INVITE = 'notify-album-invite',
|
||||||
NOTIFY_ALBUM_UPDATE = 'notify-album-update',
|
NOTIFY_ALBUM_UPDATE = 'notify-album-update',
|
||||||
|
NOTIFICATIONS_CLEANUP = 'notifications-cleanup',
|
||||||
SEND_EMAIL = 'notification-send-email',
|
SEND_EMAIL = 'notification-send-email',
|
||||||
|
|
||||||
// Version check
|
// Version check
|
||||||
@ -580,3 +591,17 @@ export enum SyncEntityType {
|
|||||||
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
||||||
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum NotificationLevel {
|
||||||
|
Success = 'success',
|
||||||
|
Error = 'error',
|
||||||
|
Warning = 'warning',
|
||||||
|
Info = 'info',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NotificationType {
|
||||||
|
JobFailed = 'JobFailed',
|
||||||
|
BackupFailed = 'BackupFailed',
|
||||||
|
SystemMessage = 'SystemMessage',
|
||||||
|
Custom = 'Custom',
|
||||||
|
}
|
||||||
|
@ -157,6 +157,15 @@ where
|
|||||||
and "memories"."ownerId" = $2
|
and "memories"."ownerId" = $2
|
||||||
and "memories"."deletedAt" is null
|
and "memories"."deletedAt" is null
|
||||||
|
|
||||||
|
-- AccessRepository.notification.checkOwnerAccess
|
||||||
|
select
|
||||||
|
"notifications"."id"
|
||||||
|
from
|
||||||
|
"notifications"
|
||||||
|
where
|
||||||
|
"notifications"."id" in ($1)
|
||||||
|
and "notifications"."userId" = $2
|
||||||
|
|
||||||
-- AccessRepository.person.checkOwnerAccess
|
-- AccessRepository.person.checkOwnerAccess
|
||||||
select
|
select
|
||||||
"person"."id"
|
"person"."id"
|
||||||
|
@ -13,6 +13,7 @@ from
|
|||||||
"users"."id",
|
"users"."id",
|
||||||
"users"."name",
|
"users"."name",
|
||||||
"users"."email",
|
"users"."email",
|
||||||
|
"users"."avatarColor",
|
||||||
"users"."profileImagePath",
|
"users"."profileImagePath",
|
||||||
"users"."profileChangedAt"
|
"users"."profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -44,6 +45,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
|
@ -12,6 +12,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -36,6 +37,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -100,6 +102,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -124,6 +127,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -191,6 +195,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -215,6 +220,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -269,6 +275,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -292,6 +299,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -353,6 +361,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
|
@ -259,6 +259,130 @@ from
|
|||||||
where
|
where
|
||||||
"assets"."id" = $2
|
"assets"."id" = $2
|
||||||
|
|
||||||
|
-- AssetJobRepository.getForSyncAssets
|
||||||
|
select
|
||||||
|
"assets"."id",
|
||||||
|
"assets"."isOffline",
|
||||||
|
"assets"."libraryId",
|
||||||
|
"assets"."originalPath",
|
||||||
|
"assets"."status",
|
||||||
|
"assets"."fileModifiedAt"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
where
|
||||||
|
"assets"."id" = any ($1::uuid[])
|
||||||
|
|
||||||
|
-- AssetJobRepository.getForAssetDeletion
|
||||||
|
select
|
||||||
|
"assets"."id",
|
||||||
|
"assets"."isVisible",
|
||||||
|
"assets"."libraryId",
|
||||||
|
"assets"."ownerId",
|
||||||
|
"assets"."livePhotoVideoId",
|
||||||
|
"assets"."sidecarPath",
|
||||||
|
"assets"."encodedVideoPath",
|
||||||
|
"assets"."originalPath",
|
||||||
|
to_json("exif") as "exifInfo",
|
||||||
|
(
|
||||||
|
select
|
||||||
|
coalesce(json_agg(agg), '[]')
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"asset_faces".*,
|
||||||
|
"person" as "person"
|
||||||
|
from
|
||||||
|
"asset_faces"
|
||||||
|
left join lateral (
|
||||||
|
select
|
||||||
|
"person".*
|
||||||
|
from
|
||||||
|
"person"
|
||||||
|
where
|
||||||
|
"asset_faces"."personId" = "person"."id"
|
||||||
|
) as "person" on true
|
||||||
|
where
|
||||||
|
"asset_faces"."assetId" = "assets"."id"
|
||||||
|
and "asset_faces"."deletedAt" is null
|
||||||
|
) as agg
|
||||||
|
) as "faces",
|
||||||
|
(
|
||||||
|
select
|
||||||
|
coalesce(json_agg(agg), '[]')
|
||||||
|
from
|
||||||
|
(
|
||||||
|
select
|
||||||
|
"asset_files"."id",
|
||||||
|
"asset_files"."path",
|
||||||
|
"asset_files"."type"
|
||||||
|
from
|
||||||
|
"asset_files"
|
||||||
|
where
|
||||||
|
"asset_files"."assetId" = "assets"."id"
|
||||||
|
) as agg
|
||||||
|
) as "files",
|
||||||
|
to_json("stacked_assets") as "stack"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
left join "exif" on "assets"."id" = "exif"."assetId"
|
||||||
|
left join "asset_stack" on "asset_stack"."id" = "assets"."stackId"
|
||||||
|
left join lateral (
|
||||||
|
select
|
||||||
|
"asset_stack"."id",
|
||||||
|
"asset_stack"."primaryAssetId",
|
||||||
|
array_agg("stacked") as "assets"
|
||||||
|
from
|
||||||
|
"assets" as "stacked"
|
||||||
|
where
|
||||||
|
"stacked"."deletedAt" is not null
|
||||||
|
and "stacked"."isArchived" = $1
|
||||||
|
and "stacked"."stackId" = "asset_stack"."id"
|
||||||
|
group by
|
||||||
|
"asset_stack"."id"
|
||||||
|
) as "stacked_assets" on "asset_stack"."id" is not null
|
||||||
|
where
|
||||||
|
"assets"."id" = $2
|
||||||
|
|
||||||
|
-- AssetJobRepository.streamForVideoConversion
|
||||||
|
select
|
||||||
|
"assets"."id"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
where
|
||||||
|
"assets"."type" = $1
|
||||||
|
and (
|
||||||
|
"assets"."encodedVideoPath" is null
|
||||||
|
or "assets"."encodedVideoPath" = $2
|
||||||
|
)
|
||||||
|
and "assets"."isVisible" = $3
|
||||||
|
and "assets"."deletedAt" is null
|
||||||
|
|
||||||
|
-- AssetJobRepository.getForVideoConversion
|
||||||
|
select
|
||||||
|
"assets"."id",
|
||||||
|
"assets"."ownerId",
|
||||||
|
"assets"."originalPath",
|
||||||
|
"assets"."encodedVideoPath"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
where
|
||||||
|
"assets"."id" = $1
|
||||||
|
and "assets"."type" = $2
|
||||||
|
|
||||||
|
-- AssetJobRepository.streamForMetadataExtraction
|
||||||
|
select
|
||||||
|
"assets"."id"
|
||||||
|
from
|
||||||
|
"assets"
|
||||||
|
left join "asset_job_status" on "asset_job_status"."assetId" = "assets"."id"
|
||||||
|
where
|
||||||
|
(
|
||||||
|
"asset_job_status"."metadataExtractedAt" is null
|
||||||
|
or "asset_job_status"."assetId" is null
|
||||||
|
)
|
||||||
|
and "assets"."isVisible" = $1
|
||||||
|
and "assets"."deletedAt" is null
|
||||||
|
|
||||||
-- AssetJobRepository.getForStorageTemplateJob
|
-- AssetJobRepository.getForStorageTemplateJob
|
||||||
select
|
select
|
||||||
"assets"."id",
|
"assets"."id",
|
||||||
|
58
server/src/queries/notification.repository.sql
Normal file
58
server/src/queries/notification.repository.sql
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
-- NOTE: This file is auto generated by ./sql-generator
|
||||||
|
|
||||||
|
-- NotificationRepository.cleanup
|
||||||
|
delete from "notifications"
|
||||||
|
where
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"deletedAt" is not null
|
||||||
|
and "deletedAt" < $1
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
"readAt" > $2
|
||||||
|
and "createdAt" < $3
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
"readAt" = $4
|
||||||
|
and "createdAt" < $5
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
-- NotificationRepository.search
|
||||||
|
select
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"level",
|
||||||
|
"type",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"data",
|
||||||
|
"readAt"
|
||||||
|
from
|
||||||
|
"notifications"
|
||||||
|
where
|
||||||
|
"userId" = $1
|
||||||
|
and "deletedAt" is null
|
||||||
|
order by
|
||||||
|
"createdAt" desc
|
||||||
|
|
||||||
|
-- NotificationRepository.search (unread)
|
||||||
|
select
|
||||||
|
"id",
|
||||||
|
"createdAt",
|
||||||
|
"level",
|
||||||
|
"type",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"data",
|
||||||
|
"readAt"
|
||||||
|
from
|
||||||
|
"notifications"
|
||||||
|
where
|
||||||
|
(
|
||||||
|
"userId" = $1
|
||||||
|
and "readAt" is null
|
||||||
|
)
|
||||||
|
and "deletedAt" is null
|
||||||
|
order by
|
||||||
|
"createdAt" desc
|
@ -12,6 +12,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -29,6 +30,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -61,6 +63,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -78,6 +81,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -112,6 +116,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -129,6 +134,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -156,6 +162,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
@ -173,6 +180,7 @@ returning
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt"
|
"profileChangedAt"
|
||||||
from
|
from
|
||||||
|
@ -5,6 +5,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -43,6 +44,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -90,6 +92,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -128,6 +131,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -152,6 +156,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -198,6 +203,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@ -235,6 +241,7 @@ select
|
|||||||
"id",
|
"id",
|
||||||
"name",
|
"name",
|
||||||
"email",
|
"email",
|
||||||
|
"avatarColor",
|
||||||
"profileImagePath",
|
"profileImagePath",
|
||||||
"profileChangedAt",
|
"profileChangedAt",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
|
@ -279,6 +279,26 @@ class AuthDeviceAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NotificationAccess {
|
||||||
|
constructor(private db: Kysely<DB>) {}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
|
||||||
|
@ChunkedSet({ paramIndex: 1 })
|
||||||
|
async checkOwnerAccess(userId: string, notificationIds: Set<string>) {
|
||||||
|
if (notificationIds.size === 0) {
|
||||||
|
return new Set<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.db
|
||||||
|
.selectFrom('notifications')
|
||||||
|
.select('notifications.id')
|
||||||
|
.where('notifications.id', 'in', [...notificationIds])
|
||||||
|
.where('notifications.userId', '=', userId)
|
||||||
|
.execute()
|
||||||
|
.then((stacks) => new Set(stacks.map((stack) => stack.id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class StackAccess {
|
class StackAccess {
|
||||||
constructor(private db: Kysely<DB>) {}
|
constructor(private db: Kysely<DB>) {}
|
||||||
|
|
||||||
@ -426,6 +446,7 @@ export class AccessRepository {
|
|||||||
asset: AssetAccess;
|
asset: AssetAccess;
|
||||||
authDevice: AuthDeviceAccess;
|
authDevice: AuthDeviceAccess;
|
||||||
memory: MemoryAccess;
|
memory: MemoryAccess;
|
||||||
|
notification: NotificationAccess;
|
||||||
person: PersonAccess;
|
person: PersonAccess;
|
||||||
partner: PartnerAccess;
|
partner: PartnerAccess;
|
||||||
stack: StackAccess;
|
stack: StackAccess;
|
||||||
@ -438,6 +459,7 @@ export class AccessRepository {
|
|||||||
this.asset = new AssetAccess(db);
|
this.asset = new AssetAccess(db);
|
||||||
this.authDevice = new AuthDeviceAccess(db);
|
this.authDevice = new AuthDeviceAccess(db);
|
||||||
this.memory = new MemoryAccess(db);
|
this.memory = new MemoryAccess(db);
|
||||||
|
this.notification = new NotificationAccess(db);
|
||||||
this.person = new PersonAccess(db);
|
this.person = new PersonAccess(db);
|
||||||
this.partner = new PartnerAccess(db);
|
this.partner = new PartnerAccess(db);
|
||||||
this.stack = new StackAccess(db);
|
this.stack = new StackAccess(db);
|
||||||
|
@ -2,12 +2,21 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Kysely } from 'kysely';
|
import { Kysely } from 'kysely';
|
||||||
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
import { jsonArrayFrom } from 'kysely/helpers/postgres';
|
||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { columns } from 'src/database';
|
import { Asset, columns } from 'src/database';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AssetFileType } from 'src/enum';
|
import { AssetFileType, AssetType } from 'src/enum';
|
||||||
import { StorageAsset } from 'src/types';
|
import { StorageAsset } from 'src/types';
|
||||||
import { anyUuid, asUuid, withExifInner, withFaces, withFiles } from 'src/utils/database';
|
import {
|
||||||
|
anyUuid,
|
||||||
|
asUuid,
|
||||||
|
toJson,
|
||||||
|
withExif,
|
||||||
|
withExifInner,
|
||||||
|
withFaces,
|
||||||
|
withFacesAndPeople,
|
||||||
|
withFiles,
|
||||||
|
} from 'src/utils/database';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetJobRepository {
|
export class AssetJobRepository {
|
||||||
@ -148,6 +157,7 @@ export class AssetJobRepository {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
getForSyncAssets(ids: string[]) {
|
getForSyncAssets(ids: string[]) {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
@ -163,6 +173,84 @@ export class AssetJobRepository {
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
getForAssetDeletion(id: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select([
|
||||||
|
'assets.id',
|
||||||
|
'assets.isVisible',
|
||||||
|
'assets.libraryId',
|
||||||
|
'assets.ownerId',
|
||||||
|
'assets.livePhotoVideoId',
|
||||||
|
'assets.sidecarPath',
|
||||||
|
'assets.encodedVideoPath',
|
||||||
|
'assets.originalPath',
|
||||||
|
])
|
||||||
|
.$call(withExif)
|
||||||
|
.select(withFacesAndPeople)
|
||||||
|
.select(withFiles)
|
||||||
|
.leftJoin('asset_stack', 'asset_stack.id', 'assets.stackId')
|
||||||
|
.leftJoinLateral(
|
||||||
|
(eb) =>
|
||||||
|
eb
|
||||||
|
.selectFrom('assets as stacked')
|
||||||
|
.select(['asset_stack.id', 'asset_stack.primaryAssetId'])
|
||||||
|
.select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets'))
|
||||||
|
.where('stacked.deletedAt', 'is not', null)
|
||||||
|
.where('stacked.isArchived', '=', false)
|
||||||
|
.whereRef('stacked.stackId', '=', 'asset_stack.id')
|
||||||
|
.groupBy('asset_stack.id')
|
||||||
|
.as('stacked_assets'),
|
||||||
|
(join) => join.on('asset_stack.id', 'is not', null),
|
||||||
|
)
|
||||||
|
.select((eb) => toJson(eb, 'stacked_assets').as('stack'))
|
||||||
|
.where('assets.id', '=', id)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [], stream: true })
|
||||||
|
streamForVideoConversion(force?: boolean) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select(['assets.id'])
|
||||||
|
.where('assets.type', '=', AssetType.VIDEO)
|
||||||
|
.$if(!force, (qb) =>
|
||||||
|
qb
|
||||||
|
.where((eb) => eb.or([eb('assets.encodedVideoPath', 'is', null), eb('assets.encodedVideoPath', '=', '')]))
|
||||||
|
.where('assets.isVisible', '=', true),
|
||||||
|
)
|
||||||
|
.where('assets.deletedAt', 'is', null)
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
getForVideoConversion(id: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select(['assets.id', 'assets.ownerId', 'assets.originalPath', 'assets.encodedVideoPath'])
|
||||||
|
.where('assets.id', '=', id)
|
||||||
|
.where('assets.type', '=', AssetType.VIDEO)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [], stream: true })
|
||||||
|
streamForMetadataExtraction(force?: boolean) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('assets')
|
||||||
|
.select(['assets.id'])
|
||||||
|
.$if(!force, (qb) =>
|
||||||
|
qb
|
||||||
|
.leftJoin('asset_job_status', 'asset_job_status.assetId', 'assets.id')
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([eb('asset_job_status.metadataExtractedAt', 'is', null), eb('asset_job_status.assetId', 'is', null)]),
|
||||||
|
)
|
||||||
|
.where('assets.isVisible', '=', true),
|
||||||
|
)
|
||||||
|
.where('assets.deletedAt', 'is', null)
|
||||||
|
.stream();
|
||||||
|
}
|
||||||
|
|
||||||
private storageTemplateAssetQuery() {
|
private storageTemplateAssetQuery() {
|
||||||
return this.db
|
return this.db
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
|
@ -14,6 +14,7 @@ import { SystemConfig } from 'src/config';
|
|||||||
import { EventConfig } from 'src/decorators';
|
import { EventConfig } from 'src/decorators';
|
||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { ImmichWorker, MetadataKey, QueueName } from 'src/enum';
|
import { ImmichWorker, MetadataKey, QueueName } from 'src/enum';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
@ -64,6 +65,7 @@ type EventMap = {
|
|||||||
'assets.restore': [{ assetIds: string[]; userId: string }];
|
'assets.restore': [{ assetIds: string[]; userId: string }];
|
||||||
|
|
||||||
'job.start': [QueueName, JobItem];
|
'job.start': [QueueName, JobItem];
|
||||||
|
'job.failed': [{ job: JobItem; error: Error | any }];
|
||||||
|
|
||||||
// session events
|
// session events
|
||||||
'session.delete': [{ sessionId: string }];
|
'session.delete': [{ sessionId: string }];
|
||||||
@ -104,6 +106,7 @@ export interface ClientEventMap {
|
|||||||
on_server_version: [ServerVersionResponseDto];
|
on_server_version: [ServerVersionResponseDto];
|
||||||
on_config_update: [];
|
on_config_update: [];
|
||||||
on_new_release: [ReleaseNotification];
|
on_new_release: [ReleaseNotification];
|
||||||
|
on_notification: [NotificationDto];
|
||||||
on_session_delete: [string];
|
on_session_delete: [string];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ import { MediaRepository } from 'src/repositories/media.repository';
|
|||||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||||
import { MoveRepository } from 'src/repositories/move.repository';
|
import { MoveRepository } from 'src/repositories/move.repository';
|
||||||
|
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
@ -55,6 +56,7 @@ export const repositories = [
|
|||||||
CryptoRepository,
|
CryptoRepository,
|
||||||
DatabaseRepository,
|
DatabaseRepository,
|
||||||
DownloadRepository,
|
DownloadRepository,
|
||||||
|
EmailRepository,
|
||||||
EventRepository,
|
EventRepository,
|
||||||
JobRepository,
|
JobRepository,
|
||||||
LibraryRepository,
|
LibraryRepository,
|
||||||
@ -65,7 +67,7 @@ export const repositories = [
|
|||||||
MemoryRepository,
|
MemoryRepository,
|
||||||
MetadataRepository,
|
MetadataRepository,
|
||||||
MoveRepository,
|
MoveRepository,
|
||||||
EmailRepository,
|
NotificationRepository,
|
||||||
OAuthRepository,
|
OAuthRepository,
|
||||||
PartnerRepository,
|
PartnerRepository,
|
||||||
PersonRepository,
|
PersonRepository,
|
||||||
|
@ -7,7 +7,7 @@ import { Writable } from 'node:stream';
|
|||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants';
|
import { ORIENTATION_TO_SHARP_ROTATION } from 'src/constants';
|
||||||
import { Exif } from 'src/database';
|
import { Exif } from 'src/database';
|
||||||
import { Colorspace, LogLevel } from 'src/enum';
|
import { Colorspace, LogLevel, RawExtractedFormat } from 'src/enum';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import {
|
import {
|
||||||
DecodeToBufferOptions,
|
DecodeToBufferOptions,
|
||||||
@ -36,34 +36,51 @@ type ProgressEvent = {
|
|||||||
percent?: number;
|
percent?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExtractResult = {
|
||||||
|
buffer: Buffer;
|
||||||
|
format: RawExtractedFormat;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MediaRepository {
|
export class MediaRepository {
|
||||||
constructor(private logger: LoggingRepository) {
|
constructor(private logger: LoggingRepository) {
|
||||||
this.logger.setContext(MediaRepository.name);
|
this.logger.setContext(MediaRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
async extract(input: string, output: string): Promise<boolean> {
|
/**
|
||||||
|
*
|
||||||
|
* @param input file path to the input image
|
||||||
|
* @returns ExtractResult if succeeded, or null if failed
|
||||||
|
*/
|
||||||
|
async extract(input: string): Promise<ExtractResult | null> {
|
||||||
try {
|
try {
|
||||||
// remove existing output file if it exists
|
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
|
||||||
// as exiftool-vendored does not support overwriting via "-w!" flag
|
return { buffer, format: RawExtractedFormat.JPEG };
|
||||||
// and throws "1 files could not be read" error when the output file exists
|
} catch (error: any) {
|
||||||
await fs.unlink(output).catch(() => null);
|
this.logger.debug('Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next', error.message);
|
||||||
await exiftool.extractBinaryTag('JpgFromRaw2', input, output);
|
}
|
||||||
} catch {
|
|
||||||
try {
|
try {
|
||||||
this.logger.debug('Extracting JPEG from RAW image:', input);
|
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
|
||||||
await exiftool.extractJpgFromRaw(input, output);
|
return { buffer, format: RawExtractedFormat.JPEG };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
this.logger.debug('Could not extract JPEG from image, trying preview', error.message);
|
this.logger.debug('Could not extract JPEG buffer from image, trying PreviewJXL next', error.message);
|
||||||
try {
|
}
|
||||||
await exiftool.extractPreview(input, output);
|
|
||||||
} catch (error: any) {
|
try {
|
||||||
this.logger.debug('Could not extract preview from image', error.message);
|
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
|
||||||
return false;
|
return { buffer, format: RawExtractedFormat.JXL };
|
||||||
}
|
} catch (error: any) {
|
||||||
}
|
this.logger.debug('Could not extract PreviewJXL buffer from image, trying PreviewImage next', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input);
|
||||||
|
return { buffer, format: RawExtractedFormat.JPEG };
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.debug('Could not extract preview buffer from image', error.message);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeExif(tags: Partial<Exif>, output: string): Promise<boolean> {
|
async writeExif(tags: Partial<Exif>, output: string): Promise<boolean> {
|
||||||
@ -104,7 +121,7 @@ export class MediaRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decodeImage(input: string, options: DecodeToBufferOptions) {
|
decodeImage(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||||
return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true });
|
return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,7 +252,7 @@ export class MediaRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getImageDimensions(input: string): Promise<ImageDimensions> {
|
async getImageDimensions(input: string | Buffer): Promise<ImageDimensions> {
|
||||||
const { width = 0, height = 0 } = await sharp(input).metadata();
|
const { width = 0, height = 0 } = await sharp(input).metadata();
|
||||||
return { width, height };
|
return { width, height };
|
||||||
}
|
}
|
||||||
|
103
server/src/repositories/notification.repository.ts
Normal file
103
server/src/repositories/notification.repository.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { Insertable, Kysely, Updateable } from 'kysely';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
|
import { columns } from 'src/database';
|
||||||
|
import { DB, Notifications } from 'src/db';
|
||||||
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
|
import { NotificationSearchDto } from 'src/dtos/notification.dto';
|
||||||
|
|
||||||
|
export class NotificationRepository {
|
||||||
|
constructor(@InjectKysely() private db: Kysely<DB>) {}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
cleanup() {
|
||||||
|
return this.db
|
||||||
|
.deleteFrom('notifications')
|
||||||
|
.where((eb) =>
|
||||||
|
eb.or([
|
||||||
|
// remove soft-deleted notifications
|
||||||
|
eb.and([eb('deletedAt', 'is not', null), eb('deletedAt', '<', DateTime.now().minus({ days: 3 }).toJSDate())]),
|
||||||
|
|
||||||
|
// remove old, read notifications
|
||||||
|
eb.and([
|
||||||
|
// keep recently read messages around for a few days
|
||||||
|
eb('readAt', '>', DateTime.now().minus({ days: 2 }).toJSDate()),
|
||||||
|
eb('createdAt', '<', DateTime.now().minus({ days: 15 }).toJSDate()),
|
||||||
|
]),
|
||||||
|
|
||||||
|
eb.and([
|
||||||
|
// remove super old, unread notifications
|
||||||
|
eb('readAt', '=', null),
|
||||||
|
eb('createdAt', '<', DateTime.now().minus({ days: 30 }).toJSDate()),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, {}] }, { name: 'unread', params: [DummyValue.UUID, { unread: true }] })
|
||||||
|
search(userId: string, dto: NotificationSearchDto) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('notifications')
|
||||||
|
.select(columns.notification)
|
||||||
|
.where((qb) =>
|
||||||
|
qb.and({
|
||||||
|
userId,
|
||||||
|
id: dto.id,
|
||||||
|
level: dto.level,
|
||||||
|
type: dto.type,
|
||||||
|
readAt: dto.unread ? null : undefined,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
|
.orderBy('createdAt', 'desc')
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
create(notification: Insertable<Notifications>) {
|
||||||
|
return this.db
|
||||||
|
.insertInto('notifications')
|
||||||
|
.values(notification)
|
||||||
|
.returning(columns.notification)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
get(id: string) {
|
||||||
|
return this.db
|
||||||
|
.selectFrom('notifications')
|
||||||
|
.select(columns.notification)
|
||||||
|
.where('id', '=', id)
|
||||||
|
.where('deletedAt', 'is not', null)
|
||||||
|
.executeTakeFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, notification: Updateable<Notifications>) {
|
||||||
|
return this.db
|
||||||
|
.updateTable('notifications')
|
||||||
|
.set(notification)
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
|
.where('id', '=', id)
|
||||||
|
.returning(columns.notification)
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAll(ids: string[], notification: Updateable<Notifications>) {
|
||||||
|
await this.db.updateTable('notifications').set(notification).where('id', 'in', ids).execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
await this.db
|
||||||
|
.updateTable('notifications')
|
||||||
|
.set({ deletedAt: DateTime.now().toJSDate() })
|
||||||
|
.where('id', '=', id)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAll(ids: string[]) {
|
||||||
|
await this.db
|
||||||
|
.updateTable('notifications')
|
||||||
|
.set({ deletedAt: DateTime.now().toJSDate() })
|
||||||
|
.where('id', 'in', ids)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ import { MemoryTable } from 'src/schema/tables/memory.table';
|
|||||||
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
|
import { MemoryAssetTable } from 'src/schema/tables/memory_asset.table';
|
||||||
import { MoveTable } from 'src/schema/tables/move.table';
|
import { MoveTable } from 'src/schema/tables/move.table';
|
||||||
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
import { NaturalEarthCountriesTable } from 'src/schema/tables/natural-earth-countries.table';
|
||||||
|
import { NotificationTable } from 'src/schema/tables/notification.table';
|
||||||
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
import { PartnerAuditTable } from 'src/schema/tables/partner-audit.table';
|
||||||
import { PartnerTable } from 'src/schema/tables/partner.table';
|
import { PartnerTable } from 'src/schema/tables/partner.table';
|
||||||
import { PersonTable } from 'src/schema/tables/person.table';
|
import { PersonTable } from 'src/schema/tables/person.table';
|
||||||
@ -76,6 +77,7 @@ export class ImmichDatabase {
|
|||||||
MemoryTable,
|
MemoryTable,
|
||||||
MoveTable,
|
MoveTable,
|
||||||
NaturalEarthCountriesTable,
|
NaturalEarthCountriesTable,
|
||||||
|
NotificationTable,
|
||||||
PartnerAuditTable,
|
PartnerAuditTable,
|
||||||
PartnerTable,
|
PartnerTable,
|
||||||
PersonTable,
|
PersonTable,
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`CREATE TABLE "notifications" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7(), "userId" uuid, "level" character varying NOT NULL DEFAULT 'info', "type" character varying NOT NULL DEFAULT 'info', "data" jsonb, "title" character varying NOT NULL, "description" text, "readAt" timestamp with time zone);`.execute(db);
|
||||||
|
await sql`ALTER TABLE "notifications" ADD CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a" PRIMARY KEY ("id");`.execute(db);
|
||||||
|
await sql`ALTER TABLE "notifications" ADD CONSTRAINT "FK_692a909ee0fa9383e7859f9b406" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_notifications_update_id" ON "notifications" ("updateId")`.execute(db);
|
||||||
|
await sql`CREATE INDEX "IDX_692a909ee0fa9383e7859f9b40" ON "notifications" ("userId")`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "notifications_updated_at"
|
||||||
|
BEFORE UPDATE ON "notifications"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION updated_at();`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DROP TRIGGER "notifications_updated_at" ON "notifications";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_notifications_update_id";`.execute(db);
|
||||||
|
await sql`DROP INDEX "IDX_692a909ee0fa9383e7859f9b40";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "notifications" DROP CONSTRAINT "PK_6a72c3c0f683f6462415e653c3a";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "notifications" DROP CONSTRAINT "FK_692a909ee0fa9383e7859f9b406";`.execute(db);
|
||||||
|
await sql`DROP TABLE "notifications";`.execute(db);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "users" ADD "avatarColor" character varying;`.execute(db);
|
||||||
|
await sql`
|
||||||
|
UPDATE "users"
|
||||||
|
SET "avatarColor" = "user_metadata"."value"->'avatar'->>'color'
|
||||||
|
FROM "user_metadata"
|
||||||
|
WHERE "users"."id" = "user_metadata"."userId" AND "user_metadata"."key" = 'preferences';`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "users" DROP COLUMN "avatarColor";`.execute(db);
|
||||||
|
}
|
52
server/src/schema/tables/notification.table.ts
Normal file
52
server/src/schema/tables/notification.table.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
|
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||||
|
import { UserTable } from 'src/schema/tables/user.table';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
|
ForeignKeyColumn,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Table,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table('notifications')
|
||||||
|
@UpdatedAtTrigger('notifications_updated_at')
|
||||||
|
export class NotificationTable {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
deletedAt?: Date;
|
||||||
|
|
||||||
|
@UpdateIdColumn({ indexName: 'IDX_notifications_update_id' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
|
@ForeignKeyColumn(() => UserTable, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: true })
|
||||||
|
userId!: string;
|
||||||
|
|
||||||
|
@Column({ default: NotificationLevel.Info })
|
||||||
|
level!: NotificationLevel;
|
||||||
|
|
||||||
|
@Column({ default: NotificationLevel.Info })
|
||||||
|
type!: NotificationType;
|
||||||
|
|
||||||
|
@Column({ type: 'jsonb', nullable: true })
|
||||||
|
data!: any | null;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
description!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', nullable: true })
|
||||||
|
readAt?: Date | null;
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { ColumnType } from 'kysely';
|
import { ColumnType } from 'kysely';
|
||||||
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
|
||||||
import { UserStatus } from 'src/enum';
|
import { UserAvatarColor, UserStatus } from 'src/enum';
|
||||||
import { users_delete_audit } from 'src/schema/functions';
|
import { users_delete_audit } from 'src/schema/functions';
|
||||||
import {
|
import {
|
||||||
AfterDeleteTrigger,
|
AfterDeleteTrigger,
|
||||||
@ -49,6 +49,9 @@ export class UserTable {
|
|||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
shouldChangePassword!: Generated<boolean>;
|
shouldChangePassword!: Generated<boolean>;
|
||||||
|
|
||||||
|
@Column({ default: null })
|
||||||
|
avatarColor!: UserAvatarColor | null;
|
||||||
|
|
||||||
@DeleteDateColumn()
|
@DeleteDateColumn()
|
||||||
deletedAt!: Timestamp | null;
|
deletedAt!: Timestamp | null;
|
||||||
|
|
||||||
|
@ -565,7 +565,7 @@ describe(AssetService.name, () => {
|
|||||||
it('should remove faces', async () => {
|
it('should remove faces', async () => {
|
||||||
const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
|
const assetWithFace = { ...assetStub.image, faces: [faceStub.face1, faceStub.mergeFace1] };
|
||||||
|
|
||||||
mocks.asset.getById.mockResolvedValue(assetWithFace);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetWithFace);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({ id: assetWithFace.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: assetWithFace.id, deleteOnDisk: true });
|
||||||
|
|
||||||
@ -592,7 +592,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
|
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
|
||||||
mocks.stack.update.mockResolvedValue(factory.stack() as any);
|
mocks.stack.update.mockResolvedValue(factory.stack() as any);
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.primaryImage);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.primaryImage);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
|
||||||
|
|
||||||
@ -604,7 +604,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
|
it('should delete the entire stack if deleted asset was the primary asset and the stack would only contain one asset afterwards', async () => {
|
||||||
mocks.stack.delete.mockResolvedValue();
|
mocks.stack.delete.mockResolvedValue();
|
||||||
mocks.asset.getById.mockResolvedValue({
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue({
|
||||||
...assetStub.primaryImage,
|
...assetStub.primaryImage,
|
||||||
stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) },
|
stack: { ...assetStub.primaryImage.stack, assets: assetStub.primaryImage.stack!.assets.slice(0, 2) },
|
||||||
});
|
});
|
||||||
@ -615,7 +615,7 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete a live photo', async () => {
|
it('should delete a live photo', async () => {
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any);
|
||||||
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
|
mocks.asset.getLivePhotoCount.mockResolvedValue(0);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({
|
await sut.handleAssetDeletion({
|
||||||
@ -653,7 +653,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
it('should not delete a live motion part if it is being used by another asset', async () => {
|
it('should not delete a live motion part if it is being used by another asset', async () => {
|
||||||
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
|
mocks.asset.getLivePhotoCount.mockResolvedValue(2);
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.livePhotoStillAsset as any);
|
||||||
|
|
||||||
await sut.handleAssetDeletion({
|
await sut.handleAssetDeletion({
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
@ -680,12 +680,13 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update usage', async () => {
|
it('should update usage', async () => {
|
||||||
mocks.asset.getById.mockResolvedValue(assetStub.image);
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(assetStub.image);
|
||||||
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
|
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
|
||||||
expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
expect(mocks.user.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if asset could not be found', async () => {
|
it('should fail if asset could not be found', async () => {
|
||||||
|
mocks.assetJob.getForAssetDeletion.mockResolvedValue(void 0);
|
||||||
await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe(
|
await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe(
|
||||||
JobStatus.FAILED,
|
JobStatus.FAILED,
|
||||||
);
|
);
|
||||||
|
@ -189,13 +189,7 @@ export class AssetService extends BaseService {
|
|||||||
async handleAssetDeletion(job: JobOf<JobName.ASSET_DELETION>): Promise<JobStatus> {
|
async handleAssetDeletion(job: JobOf<JobName.ASSET_DELETION>): Promise<JobStatus> {
|
||||||
const { id, deleteOnDisk } = job;
|
const { id, deleteOnDisk } = job;
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(id, {
|
const asset = await this.assetJobRepository.getForAssetDeletion(id);
|
||||||
faces: { person: true },
|
|
||||||
library: true,
|
|
||||||
stack: { assets: true },
|
|
||||||
exifInfo: true,
|
|
||||||
files: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
|
@ -142,52 +142,55 @@ describe(BackupService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.backupEnabled);
|
||||||
mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
|
mocks.storage.createWriteStream.mockReturnValue(new PassThrough());
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should run a database backup successfully', async () => {
|
it('should run a database backup successfully', async () => {
|
||||||
const result = await sut.handleBackupDatabase();
|
const result = await sut.handleBackupDatabase();
|
||||||
expect(result).toBe(JobStatus.SUCCESS);
|
expect(result).toBe(JobStatus.SUCCESS);
|
||||||
expect(mocks.storage.createWriteStream).toHaveBeenCalled();
|
expect(mocks.storage.createWriteStream).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should rename file on success', async () => {
|
it('should rename file on success', async () => {
|
||||||
const result = await sut.handleBackupDatabase();
|
const result = await sut.handleBackupDatabase();
|
||||||
expect(result).toBe(JobStatus.SUCCESS);
|
expect(result).toBe(JobStatus.SUCCESS);
|
||||||
expect(mocks.storage.rename).toHaveBeenCalled();
|
expect(mocks.storage.rename).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if pg_dumpall fails', async () => {
|
it('should fail if pg_dumpall fails', async () => {
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not rename file if pgdump fails and gzip succeeds', async () => {
|
it('should not rename file if pgdump fails and gzip succeeds', async () => {
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
expect(mocks.storage.rename).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if gzip fails', async () => {
|
it('should fail if gzip fails', async () => {
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', ''));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(0, 'data', ''));
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('Gzip failed with code 1');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if write stream fails', async () => {
|
it('should fail if write stream fails', async () => {
|
||||||
mocks.storage.createWriteStream.mockImplementation(() => {
|
mocks.storage.createWriteStream.mockImplementation(() => {
|
||||||
throw new Error('error');
|
throw new Error('error');
|
||||||
});
|
});
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('error');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fail if rename fails', async () => {
|
it('should fail if rename fails', async () => {
|
||||||
mocks.storage.rename.mockRejectedValue(new Error('error'));
|
mocks.storage.rename.mockRejectedValue(new Error('error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('error');
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ignore unlink failing and still return failed job status', async () => {
|
it('should ignore unlink failing and still return failed job status', async () => {
|
||||||
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
mocks.process.spawn.mockReturnValueOnce(mockSpawn(1, '', 'error'));
|
||||||
mocks.storage.unlink.mockRejectedValue(new Error('error'));
|
mocks.storage.unlink.mockRejectedValue(new Error('error'));
|
||||||
const result = await sut.handleBackupDatabase();
|
await expect(sut.handleBackupDatabase()).rejects.toThrow('Backup failed with code 1');
|
||||||
expect(mocks.storage.unlink).toHaveBeenCalled();
|
expect(mocks.storage.unlink).toHaveBeenCalled();
|
||||||
expect(result).toBe(JobStatus.FAILED);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
postgresVersion | expectedVersion
|
postgresVersion | expectedVersion
|
||||||
${'14.10'} | ${14}
|
${'14.10'} | ${14}
|
||||||
|
@ -174,7 +174,7 @@ export class BackupService extends BaseService {
|
|||||||
await this.storageRepository
|
await this.storageRepository
|
||||||
.unlink(backupFilePath)
|
.unlink(backupFilePath)
|
||||||
.catch((error) => this.logger.error('Failed to delete failed backup file', error));
|
.catch((error) => this.logger.error('Failed to delete failed backup file', error));
|
||||||
return JobStatus.FAILED;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`Database Backup Success`);
|
this.logger.log(`Database Backup Success`);
|
||||||
|
@ -29,6 +29,7 @@ import { MediaRepository } from 'src/repositories/media.repository';
|
|||||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||||
import { MoveRepository } from 'src/repositories/move.repository';
|
import { MoveRepository } from 'src/repositories/move.repository';
|
||||||
|
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
@ -80,6 +81,7 @@ export class BaseService {
|
|||||||
protected memoryRepository: MemoryRepository,
|
protected memoryRepository: MemoryRepository,
|
||||||
protected metadataRepository: MetadataRepository,
|
protected metadataRepository: MetadataRepository,
|
||||||
protected moveRepository: MoveRepository,
|
protected moveRepository: MoveRepository,
|
||||||
|
protected notificationRepository: NotificationRepository,
|
||||||
protected oauthRepository: OAuthRepository,
|
protected oauthRepository: OAuthRepository,
|
||||||
protected partnerRepository: PartnerRepository,
|
protected partnerRepository: PartnerRepository,
|
||||||
protected personRepository: PersonRepository,
|
protected personRepository: PersonRepository,
|
||||||
|
@ -33,7 +33,7 @@ export class DownloadService extends BaseService {
|
|||||||
|
|
||||||
const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
|
const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
|
||||||
const metadata = await this.userRepository.getMetadata(auth.user.id);
|
const metadata = await this.userRepository.getMetadata(auth.user.id);
|
||||||
const preferences = getPreferences(auth.user.email, metadata);
|
const preferences = getPreferences(metadata);
|
||||||
const motionIds = new Set<string>();
|
const motionIds = new Set<string>();
|
||||||
const archives: DownloadArchiveInfo[] = [];
|
const archives: DownloadArchiveInfo[] = [];
|
||||||
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };
|
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };
|
||||||
|
@ -17,6 +17,7 @@ import { MapService } from 'src/services/map.service';
|
|||||||
import { MediaService } from 'src/services/media.service';
|
import { MediaService } from 'src/services/media.service';
|
||||||
import { MemoryService } from 'src/services/memory.service';
|
import { MemoryService } from 'src/services/memory.service';
|
||||||
import { MetadataService } from 'src/services/metadata.service';
|
import { MetadataService } from 'src/services/metadata.service';
|
||||||
|
import { NotificationAdminService } from 'src/services/notification-admin.service';
|
||||||
import { NotificationService } from 'src/services/notification.service';
|
import { NotificationService } from 'src/services/notification.service';
|
||||||
import { PartnerService } from 'src/services/partner.service';
|
import { PartnerService } from 'src/services/partner.service';
|
||||||
import { PersonService } from 'src/services/person.service';
|
import { PersonService } from 'src/services/person.service';
|
||||||
@ -60,6 +61,7 @@ export const services = [
|
|||||||
MemoryService,
|
MemoryService,
|
||||||
MetadataService,
|
MetadataService,
|
||||||
NotificationService,
|
NotificationService,
|
||||||
|
NotificationAdminService,
|
||||||
PartnerService,
|
PartnerService,
|
||||||
PersonService,
|
PersonService,
|
||||||
SearchService,
|
SearchService,
|
||||||
|
@ -215,11 +215,7 @@ export class JobService extends BaseService {
|
|||||||
await this.onDone(job);
|
await this.onDone(job);
|
||||||
}
|
}
|
||||||
} catch (error: Error | any) {
|
} catch (error: Error | any) {
|
||||||
this.logger.error(
|
await this.eventRepository.emit('job.failed', { job, error });
|
||||||
`Unable to run job handler (${queueName}/${job.name}): ${error}`,
|
|
||||||
error?.stack,
|
|
||||||
JSON.stringify(job.data),
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
|
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { OutputInfo } from 'sharp';
|
import { OutputInfo } from 'sharp';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { Exif } from 'src/database';
|
import { Exif } from 'src/database';
|
||||||
import { AssetMediaSize } from 'src/dtos/asset-media.dto';
|
|
||||||
import {
|
import {
|
||||||
AssetFileType,
|
AssetFileType,
|
||||||
AssetPathType,
|
AssetPathType,
|
||||||
@ -11,11 +10,11 @@ import {
|
|||||||
ImageFormat,
|
ImageFormat,
|
||||||
JobName,
|
JobName,
|
||||||
JobStatus,
|
JobStatus,
|
||||||
|
RawExtractedFormat,
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { WithoutProperty } from 'src/repositories/asset.repository';
|
|
||||||
import { MediaService } from 'src/services/media.service';
|
import { MediaService } from 'src/services/media.service';
|
||||||
import { JobCounts, RawImageInfo } from 'src/types';
|
import { JobCounts, RawImageInfo } from 'src/types';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
@ -232,17 +231,19 @@ describe(MediaService.name, () => {
|
|||||||
describe('handleGenerateThumbnails', () => {
|
describe('handleGenerateThumbnails', () => {
|
||||||
let rawBuffer: Buffer;
|
let rawBuffer: Buffer;
|
||||||
let fullsizeBuffer: Buffer;
|
let fullsizeBuffer: Buffer;
|
||||||
|
let extractedBuffer: Buffer;
|
||||||
let rawInfo: RawImageInfo;
|
let rawInfo: RawImageInfo;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fullsizeBuffer = Buffer.from('embedded image data');
|
fullsizeBuffer = Buffer.from('embedded image data');
|
||||||
rawBuffer = Buffer.from('image data');
|
rawBuffer = Buffer.from('raw image data');
|
||||||
|
extractedBuffer = Buffer.from('embedded image file');
|
||||||
rawInfo = { width: 100, height: 100, channels: 3 };
|
rawInfo = { width: 100, height: 100, channels: 3 };
|
||||||
mocks.media.decodeImage.mockImplementation((path) =>
|
mocks.media.decodeImage.mockImplementation((input) =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
path.includes(AssetMediaSize.FULLSIZE)
|
typeof input === 'string'
|
||||||
? { data: fullsizeBuffer, info: rawInfo as OutputInfo }
|
? { data: rawBuffer, info: rawInfo as OutputInfo } // string implies original file
|
||||||
: { data: rawBuffer, info: rawInfo as OutputInfo },
|
: { data: fullsizeBuffer, info: rawInfo as OutputInfo }, // buffer implies embedded image extracted
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -585,16 +586,15 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should extract embedded image if enabled and available', async () => {
|
it('should extract embedded image if enabled and available', async () => {
|
||||||
mocks.media.extract.mockResolvedValue(true);
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||||
|
|
||||||
const convertedPath = mocks.media.extract.mock.lastCall?.[1].toString();
|
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(convertedPath, {
|
expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, {
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
size: 1440,
|
size: 1440,
|
||||||
@ -602,16 +602,13 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should resize original image if embedded image is too small', async () => {
|
it('should resize original image if embedded image is too small', async () => {
|
||||||
mocks.media.extract.mockResolvedValue(true);
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 });
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { extractEmbedded: true } });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||||
|
|
||||||
const extractedPath = mocks.media.extract.mock.lastCall?.[1].toString();
|
|
||||||
expect(extractedPath).toMatch(/-fullsize\.jpeg$/);
|
|
||||||
|
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
|
expect(mocks.media.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, {
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
@ -666,38 +663,40 @@ describe(MediaService.name, () => {
|
|||||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
||||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||||
rawBuffer,
|
rawBuffer,
|
||||||
expect.objectContaining({ processInvalidImages: true }),
|
expect.objectContaining({ processInvalidImages: false }),
|
||||||
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
|
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
|
||||||
);
|
);
|
||||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||||
rawBuffer,
|
rawBuffer,
|
||||||
expect.objectContaining({ processInvalidImages: true }),
|
expect.objectContaining({ processInvalidImages: false }),
|
||||||
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
|
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mocks.media.generateThumbhash).toHaveBeenCalledOnce();
|
expect(mocks.media.generateThumbhash).toHaveBeenCalledOnce();
|
||||||
expect(mocks.media.generateThumbhash).toHaveBeenCalledWith(
|
expect(mocks.media.generateThumbhash).toHaveBeenCalledWith(
|
||||||
rawBuffer,
|
rawBuffer,
|
||||||
expect.objectContaining({ processInvalidImages: true }),
|
expect.objectContaining({ processInvalidImages: false }),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mocks.media.getImageDimensions).not.toHaveBeenCalled();
|
expect(mocks.media.getImageDimensions).not.toHaveBeenCalled();
|
||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate full-size preview using embedded JPEG from RAW images when extractEmbedded is true', async () => {
|
it('should extract full-size JPEG preview from RAW', async () => {
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: true } });
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
mocks.media.extract.mockResolvedValue(true);
|
image: { fullsize: { enabled: true, format: ImageFormat.WEBP }, extractEmbedded: true },
|
||||||
|
});
|
||||||
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||||
|
|
||||||
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||||
|
|
||||||
const extractedPath = mocks.media.extract.mock.lastCall?.[1].toString();
|
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||||
expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedPath, {
|
expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, {
|
||||||
colorspace: Colorspace.P3,
|
colorspace: Colorspace.P3,
|
||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
|
size: 1440, // capped to preview size as fullsize conversion is skipped
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(2);
|
||||||
@ -715,9 +714,51 @@ describe(MediaService.name, () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should convert full-size WEBP preview from JXL preview of RAW', async () => {
|
||||||
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
|
image: { fullsize: { enabled: true, format: ImageFormat.WEBP }, extractEmbedded: true },
|
||||||
|
});
|
||||||
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JXL });
|
||||||
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||||
|
|
||||||
|
await sut.handleGenerateThumbnails({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(mocks.media.decodeImage).toHaveBeenCalledOnce();
|
||||||
|
expect(mocks.media.decodeImage).toHaveBeenCalledWith(extractedBuffer, {
|
||||||
|
colorspace: Colorspace.P3,
|
||||||
|
processInvalidImages: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledTimes(3);
|
||||||
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||||
|
fullsizeBuffer,
|
||||||
|
{
|
||||||
|
colorspace: Colorspace.P3,
|
||||||
|
format: ImageFormat.WEBP,
|
||||||
|
quality: 80,
|
||||||
|
processInvalidImages: false,
|
||||||
|
raw: rawInfo,
|
||||||
|
},
|
||||||
|
'upload/thumbs/user-id/as/se/asset-id-fullsize.webp',
|
||||||
|
);
|
||||||
|
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||||
|
fullsizeBuffer,
|
||||||
|
{
|
||||||
|
colorspace: Colorspace.P3,
|
||||||
|
format: ImageFormat.JPEG,
|
||||||
|
size: 1440,
|
||||||
|
quality: 80,
|
||||||
|
processInvalidImages: false,
|
||||||
|
raw: rawInfo,
|
||||||
|
},
|
||||||
|
'upload/thumbs/user-id/as/se/asset-id-preview.jpeg',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should generate full-size preview directly from RAW images when extractEmbedded is false', async () => {
|
it('should generate full-size preview directly from RAW images when extractEmbedded is false', async () => {
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true }, extractEmbedded: false } });
|
||||||
mocks.media.extract.mockResolvedValue(true);
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageDng);
|
||||||
|
|
||||||
@ -757,7 +798,7 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
it('should generate full-size preview from non-web-friendly images', async () => {
|
it('should generate full-size preview from non-web-friendly images', async () => {
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
||||||
mocks.media.extract.mockResolvedValue(true);
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
||||||
@ -786,7 +827,7 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
it('should skip generating full-size preview for web-friendly images', async () => {
|
it('should skip generating full-size preview for web-friendly images', async () => {
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
mocks.systemMetadata.get.mockResolvedValue({ image: { fullsize: { enabled: true } } });
|
||||||
mocks.media.extract.mockResolvedValue(true);
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.image);
|
||||||
|
|
||||||
@ -811,7 +852,7 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
image: { fullsize: { enabled: true, format: ImageFormat.WEBP, quality: 90 } },
|
image: { fullsize: { enabled: true, format: ImageFormat.WEBP, quality: 90 } },
|
||||||
});
|
});
|
||||||
mocks.media.extract.mockResolvedValue(true);
|
mocks.media.extract.mockResolvedValue({ buffer: extractedBuffer, format: RawExtractedFormat.JPEG });
|
||||||
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
mocks.media.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 });
|
||||||
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
// HEIF/HIF image taken by cameras are not web-friendly, only has limited support on Safari.
|
||||||
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
mocks.assetJob.getForGenerateThumbnailJob.mockResolvedValue(assetStub.imageHif);
|
||||||
@ -841,16 +882,12 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
describe('handleQueueVideoConversion', () => {
|
describe('handleQueueVideoConversion', () => {
|
||||||
it('should queue all video assets', async () => {
|
it('should queue all video assets', async () => {
|
||||||
mocks.asset.getAll.mockResolvedValue({
|
mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([assetStub.video]));
|
||||||
items: [assetStub.video],
|
|
||||||
hasNextPage: false,
|
|
||||||
});
|
|
||||||
mocks.person.getAll.mockReturnValue(makeStream());
|
mocks.person.getAll.mockReturnValue(makeStream());
|
||||||
|
|
||||||
await sut.handleQueueVideoConversion({ force: true });
|
await sut.handleQueueVideoConversion({ force: true });
|
||||||
|
|
||||||
expect(mocks.asset.getAll).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { type: AssetType.VIDEO });
|
expect(mocks.assetJob.streamForVideoConversion).toHaveBeenCalledWith(true);
|
||||||
expect(mocks.asset.getWithout).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.VIDEO_CONVERSION,
|
name: JobName.VIDEO_CONVERSION,
|
||||||
@ -860,15 +897,11 @@ describe(MediaService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should queue all video assets without encoded videos', async () => {
|
it('should queue all video assets without encoded videos', async () => {
|
||||||
mocks.asset.getWithout.mockResolvedValue({
|
mocks.assetJob.streamForVideoConversion.mockReturnValue(makeStream([assetStub.video]));
|
||||||
items: [assetStub.video],
|
|
||||||
hasNextPage: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await sut.handleQueueVideoConversion({});
|
await sut.handleQueueVideoConversion({});
|
||||||
|
|
||||||
expect(mocks.asset.getAll).not.toHaveBeenCalled();
|
expect(mocks.assetJob.streamForVideoConversion).toHaveBeenCalledWith(void 0);
|
||||||
expect(mocks.asset.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.ENCODED_VIDEO);
|
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.VIDEO_CONVERSION,
|
name: JobName.VIDEO_CONVERSION,
|
||||||
@ -880,26 +913,18 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
describe('handleVideoConversion', () => {
|
describe('handleVideoConversion', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
mocks.assetJob.getForVideoConversion.mockResolvedValue(assetStub.video);
|
||||||
sut.videoInterfaces = { dri: ['renderD128'], mali: true };
|
sut.videoInterfaces = { dri: ['renderD128'], mali: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip transcoding if asset not found', async () => {
|
it('should skip transcoding if asset not found', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([]);
|
mocks.assetJob.getForVideoConversion.mockResolvedValue(void 0);
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.probe).not.toHaveBeenCalled();
|
expect(mocks.media.probe).not.toHaveBeenCalled();
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip transcoding if non-video asset', async () => {
|
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.image]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.image.id });
|
|
||||||
expect(mocks.media.probe).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should transcode the longest stream', async () => {
|
it('should transcode the longest stream', async () => {
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
mocks.logger.isLevelEnabled.mockReturnValue(false);
|
mocks.logger.isLevelEnabled.mockReturnValue(false);
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams);
|
mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams);
|
||||||
|
|
||||||
@ -921,14 +946,12 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
it('should skip a video without any streams', async () => {
|
it('should skip a video without any streams', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams);
|
mocks.media.probe.mockResolvedValue(probeStub.noVideoStreams);
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip a video without any height', async () => {
|
it('should skip a video without any height', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.noHeight);
|
mocks.media.probe.mockResolvedValue(probeStub.noHeight);
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -936,7 +959,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should throw an error if an unknown transcode policy is configured', async () => {
|
it('should throw an error if an unknown transcode policy is configured', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.noAudioStreams);
|
mocks.media.probe.mockResolvedValue(probeStub.noAudioStreams);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: 'foo' } } as never as SystemConfig);
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: 'foo' } } as never as SystemConfig);
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
@ -947,7 +969,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { transcode: TranscodePolicy.ALL, accel: TranscodeHWAccel.DISABLED },
|
ffmpeg: { transcode: TranscodePolicy.ALL, accel: TranscodeHWAccel.DISABLED },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
mocks.media.transcode.mockRejectedValue(new Error('Error transcoding video'));
|
mocks.media.transcode.mockRejectedValue(new Error('Error transcoding video'));
|
||||||
|
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED);
|
||||||
@ -957,7 +978,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should transcode when set to all', async () => {
|
it('should transcode when set to all', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams);
|
mocks.media.probe.mockResolvedValue(probeStub.multipleVideoStreams);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.ALL } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1035,7 +1055,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should scale horizontally when video is horizontal', async () => {
|
it('should scale horizontally when video is horizontal', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1051,7 +1070,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should scale vertically when video is vertical', async () => {
|
it('should scale vertically when video is vertical', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1069,7 +1087,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' },
|
ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1087,7 +1104,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' },
|
ffmpeg: { transcode: TranscodePolicy.ALL, targetResolution: 'original' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1105,7 +1121,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { targetVideoCodec: VideoCodec.HEVC, acceptedAudioCodecs: [AudioCodec.AAC] },
|
ffmpeg: { targetVideoCodec: VideoCodec.HEVC, acceptedAudioCodecs: [AudioCodec.AAC] },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1127,7 +1142,6 @@ describe(MediaService.name, () => {
|
|||||||
acceptedAudioCodecs: [AudioCodec.AAC],
|
acceptedAudioCodecs: [AudioCodec.AAC],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1149,7 +1163,6 @@ describe(MediaService.name, () => {
|
|||||||
acceptedAudioCodecs: [AudioCodec.AAC],
|
acceptedAudioCodecs: [AudioCodec.AAC],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1165,7 +1178,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should copy audio stream when audio matches target', async () => {
|
it('should copy audio stream when audio matches target', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.audioStreamAac);
|
mocks.media.probe.mockResolvedValue(probeStub.audioStreamAac);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1180,7 +1192,6 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
it('should remux when input is not an accepted container', async () => {
|
it('should remux when input is not an accepted container', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamAvi);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamAvi);
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1204,7 +1215,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should not transcode if transcoding is disabled', async () => {
|
it('should not transcode if transcoding is disabled', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -1212,7 +1222,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should not remux when input is not an accepted container and transcoding is disabled', async () => {
|
it('should not remux when input is not an accepted container and transcoding is disabled', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -1220,7 +1229,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should not transcode if target codec is invalid', async () => {
|
it('should not transcode if target codec is invalid', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: 'invalid' as any } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: 'invalid' as any } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -1229,7 +1237,7 @@ describe(MediaService.name, () => {
|
|||||||
const asset = assetStub.hasEncodedVideo;
|
const asset = assetStub.hasEncodedVideo;
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.DISABLED } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([asset]);
|
mocks.assetJob.getForVideoConversion.mockResolvedValue(asset);
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: asset.id });
|
await sut.handleVideoConversion({ id: asset.id });
|
||||||
|
|
||||||
@ -1243,7 +1251,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set max bitrate if above 0', async () => {
|
it('should set max bitrate if above 0', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500k' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500k' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1259,7 +1266,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should default max bitrate to kbps if no unit is provided', async () => {
|
it('should default max bitrate to kbps if no unit is provided', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { maxBitrate: '4500' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1275,7 +1281,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should transcode in two passes for h264/h265 when enabled and max bitrate is above 0', async () => {
|
it('should transcode in two passes for h264/h265 when enabled and max bitrate is above 0', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '4500k' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '4500k' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1291,7 +1296,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should fallback to one pass for h264/h265 if two-pass is enabled but no max bitrate is set', async () => {
|
it('should fallback to one pass for h264/h265 if two-pass is enabled but no max bitrate is set', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { twoPass: true } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1313,7 +1317,6 @@ describe(MediaService.name, () => {
|
|||||||
targetVideoCodec: VideoCodec.VP9,
|
targetVideoCodec: VideoCodec.VP9,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1335,7 +1338,6 @@ describe(MediaService.name, () => {
|
|||||||
targetVideoCodec: VideoCodec.VP9,
|
targetVideoCodec: VideoCodec.VP9,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1351,7 +1353,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should configure preset for vp9', async () => {
|
it('should configure preset for vp9', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, preset: 'slow' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, preset: 'slow' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1367,7 +1368,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should not configure preset for vp9 if invalid', async () => {
|
it('should not configure preset for vp9 if invalid', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { preset: 'invalid', targetVideoCodec: VideoCodec.VP9 } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { preset: 'invalid', targetVideoCodec: VideoCodec.VP9 } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1383,7 +1383,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should configure threads if above 0', async () => {
|
it('should configure threads if above 0', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, threads: 2 } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.VP9, threads: 2 } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1399,7 +1398,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should disable thread pooling for h264 if thread limit is 1', async () => {
|
it('should disable thread pooling for h264 if thread limit is 1', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1 } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1 } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1415,7 +1413,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should omit thread flags for h264 if thread limit is at or below 0', async () => {
|
it('should omit thread flags for h264 if thread limit is at or below 0', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0 } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0 } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1431,7 +1428,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should disable thread pooling for hevc if thread limit is 1', async () => {
|
it('should disable thread pooling for hevc if thread limit is 1', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1, targetVideoCodec: VideoCodec.HEVC } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 1, targetVideoCodec: VideoCodec.HEVC } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1447,7 +1443,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should omit thread flags for hevc if thread limit is at or below 0', async () => {
|
it('should omit thread flags for hevc if thread limit is at or below 0', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0, targetVideoCodec: VideoCodec.HEVC } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { threads: 0, targetVideoCodec: VideoCodec.HEVC } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1463,7 +1458,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should use av1 if specified', async () => {
|
it('should use av1 if specified', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1 } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1 } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1489,7 +1483,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should map `veryslow` preset to 4 for av1', async () => {
|
it('should map `veryslow` preset to 4 for av1', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, preset: 'veryslow' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, preset: 'veryslow' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1505,7 +1498,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set max bitrate for av1 if specified', async () => {
|
it('should set max bitrate for av1 if specified', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, maxBitrate: '2M' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, maxBitrate: '2M' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1521,7 +1513,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set threads for av1 if specified', async () => {
|
it('should set threads for av1 if specified', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamVp9);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4 } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4 } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1539,7 +1530,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4, maxBitrate: '2M' },
|
ffmpeg: { targetVideoCodec: VideoCodec.AV1, threads: 4, maxBitrate: '2M' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1561,7 +1551,6 @@ describe(MediaService.name, () => {
|
|||||||
targetResolution: '1080p',
|
targetResolution: '1080p',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -1571,7 +1560,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.NVENC, targetVideoCodec: VideoCodec.VP9 },
|
ffmpeg: { accel: TranscodeHWAccel.NVENC, targetVideoCodec: VideoCodec.VP9 },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -1579,7 +1567,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should fail if hwaccel option is invalid', async () => {
|
it('should fail if hwaccel option is invalid', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: 'invalid' as any } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: 'invalid' as any } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -1587,7 +1574,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set options for nvenc', async () => {
|
it('should set options for nvenc', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1625,7 +1611,6 @@ describe(MediaService.name, () => {
|
|||||||
twoPass: true,
|
twoPass: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1641,7 +1626,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set vbr options for nvenc when max bitrate is enabled', async () => {
|
it('should set vbr options for nvenc when max bitrate is enabled', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1657,7 +1641,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set cq options for nvenc when max bitrate is disabled', async () => {
|
it('should set cq options for nvenc when max bitrate is disabled', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, maxBitrate: '10000k' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1673,7 +1656,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should omit preset for nvenc if invalid', async () => {
|
it('should omit preset for nvenc if invalid', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, preset: 'invalid' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, preset: 'invalid' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1689,7 +1671,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should ignore two pass for nvenc if max bitrate is disabled', async () => {
|
it('should ignore two pass for nvenc if max bitrate is disabled', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1707,7 +1688,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
|
ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1730,7 +1710,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
|
ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1752,7 +1731,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
|
ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1768,7 +1746,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set options for qsv', async () => {
|
it('should set options for qsv', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, maxBitrate: '10000k' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, maxBitrate: '10000k' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1809,7 +1786,6 @@ describe(MediaService.name, () => {
|
|||||||
preferredHwDevice: '/dev/dri/renderD128',
|
preferredHwDevice: '/dev/dri/renderD128',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1828,7 +1804,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should omit preset for qsv if invalid', async () => {
|
it('should omit preset for qsv if invalid', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, preset: 'invalid' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, preset: 'invalid' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1849,7 +1824,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.QSV, targetVideoCodec: VideoCodec.VP9 },
|
ffmpeg: { accel: TranscodeHWAccel.QSV, targetVideoCodec: VideoCodec.VP9 },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1869,7 +1843,6 @@ describe(MediaService.name, () => {
|
|||||||
sut.videoInterfaces = { dri: [], mali: false };
|
sut.videoInterfaces = { dri: [], mali: false };
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
||||||
|
|
||||||
@ -1880,7 +1853,6 @@ describe(MediaService.name, () => {
|
|||||||
sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false };
|
sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false };
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -1901,7 +1873,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
|
ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
|
|
||||||
@ -1928,7 +1899,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
|
ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
|
|
||||||
@ -1958,7 +1928,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true, preferredHwDevice: 'renderD129' },
|
ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true, preferredHwDevice: 'renderD129' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
@ -1977,7 +1946,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
|
ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
|
|
||||||
@ -2000,7 +1968,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set options for vaapi', async () => {
|
it('should set options for vaapi', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2031,7 +1998,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set vbr options for vaapi when max bitrate is enabled', async () => {
|
it('should set vbr options for vaapi when max bitrate is enabled', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, maxBitrate: '10000k' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, maxBitrate: '10000k' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2056,7 +2022,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set cq options for vaapi when max bitrate is disabled', async () => {
|
it('should set cq options for vaapi when max bitrate is disabled', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2081,7 +2046,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should omit preset for vaapi if invalid', async () => {
|
it('should omit preset for vaapi if invalid', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, preset: 'invalid' } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, preset: 'invalid' } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2101,7 +2065,6 @@ describe(MediaService.name, () => {
|
|||||||
sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false };
|
sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false };
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2123,7 +2086,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.VAAPI, preferredHwDevice: '/dev/dri/renderD128' },
|
ffmpeg: { accel: TranscodeHWAccel.VAAPI, preferredHwDevice: '/dev/dri/renderD128' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2144,7 +2106,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
|
ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
|
|
||||||
@ -2170,7 +2131,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
|
ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
|
|
||||||
@ -2194,7 +2154,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
|
ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
|
|
||||||
@ -2215,7 +2174,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true, preferredHwDevice: 'renderD129' },
|
ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true, preferredHwDevice: 'renderD129' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
@ -2232,7 +2190,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should fallback to hw encoding and sw decoding if hw transcoding fails and hw decoding is enabled', async () => {
|
it('should fallback to hw encoding and sw decoding if hw transcoding fails and hw decoding is enabled', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
mocks.media.transcode.mockRejectedValueOnce(new Error('error'));
|
mocks.media.transcode.mockRejectedValueOnce(new Error('error'));
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledTimes(2);
|
expect(mocks.media.transcode).toHaveBeenCalledTimes(2);
|
||||||
@ -2253,7 +2210,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should fallback to sw decoding if fallback to sw decoding + hw encoding fails', async () => {
|
it('should fallback to sw decoding if fallback to sw decoding + hw encoding fails', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
mocks.media.transcode.mockRejectedValueOnce(new Error('error'));
|
mocks.media.transcode.mockRejectedValueOnce(new Error('error'));
|
||||||
mocks.media.transcode.mockRejectedValueOnce(new Error('error'));
|
mocks.media.transcode.mockRejectedValueOnce(new Error('error'));
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
@ -2272,7 +2228,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should fallback to sw transcoding if hw transcoding fails and hw decoding is disabled', async () => {
|
it('should fallback to sw transcoding if hw transcoding fails and hw decoding is disabled', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
mocks.media.transcode.mockRejectedValueOnce(new Error('error'));
|
mocks.media.transcode.mockRejectedValueOnce(new Error('error'));
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledTimes(2);
|
expect(mocks.media.transcode).toHaveBeenCalledTimes(2);
|
||||||
@ -2291,7 +2246,6 @@ describe(MediaService.name, () => {
|
|||||||
sut.videoInterfaces = { dri: [], mali: true };
|
sut.videoInterfaces = { dri: [], mali: true };
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError();
|
||||||
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
expect(mocks.media.transcode).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@ -2299,7 +2253,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should set options for rkmpp', async () => {
|
it('should set options for rkmpp', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2340,7 +2293,6 @@ describe(MediaService.name, () => {
|
|||||||
targetVideoCodec: VideoCodec.HEVC,
|
targetVideoCodec: VideoCodec.HEVC,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2358,7 +2310,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2376,7 +2327,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2399,7 +2349,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2419,7 +2368,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: false, crf: 30, maxBitrate: '0' },
|
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: false, crf: 30, maxBitrate: '0' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2442,7 +2390,6 @@ describe(MediaService.name, () => {
|
|||||||
mocks.systemMetadata.get.mockResolvedValue({
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
|
||||||
});
|
});
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2462,7 +2409,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should tonemap when policy is required and video is hdr', async () => {
|
it('should tonemap when policy is required and video is hdr', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2482,7 +2428,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should tonemap when policy is optimal and video is hdr', async () => {
|
it('should tonemap when policy is optimal and video is hdr', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2502,7 +2447,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should transcode when policy is required and video is not yuv420p', async () => {
|
it('should transcode when policy is required and video is not yuv420p', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream10Bit);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStream10Bit);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2518,7 +2462,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should convert to yuv420p when scaling without tone-mapping', async () => {
|
it('should convert to yuv420p when scaling without tone-mapping', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream4K10Bit);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStream4K10Bit);
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
@ -2534,7 +2477,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should count frames for progress when log level is debug', async () => {
|
it('should count frames for progress when log level is debug', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
mocks.logger.isLevelEnabled.mockReturnValue(true);
|
mocks.logger.isLevelEnabled.mockReturnValue(true);
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
|
|
||||||
@ -2557,7 +2499,6 @@ describe(MediaService.name, () => {
|
|||||||
it('should not count frames for progress when log level is not debug', async () => {
|
it('should not count frames for progress when log level is not debug', async () => {
|
||||||
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
mocks.media.probe.mockResolvedValue(probeStub.videoStream2160p);
|
||||||
mocks.logger.isLevelEnabled.mockReturnValue(false);
|
mocks.logger.isLevelEnabled.mockReturnValue(false);
|
||||||
mocks.asset.getByIds.mockResolvedValue([assetStub.video]);
|
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
|
|
||||||
expect(mocks.media.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: false });
|
expect(mocks.media.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: false });
|
||||||
@ -2582,48 +2523,39 @@ describe(MediaService.name, () => {
|
|||||||
|
|
||||||
describe('isSRGB', () => {
|
describe('isSRGB', () => {
|
||||||
it('should return true for srgb colorspace', () => {
|
it('should return true for srgb colorspace', () => {
|
||||||
const asset = { ...assetStub.image, exifInfo: { colorspace: 'sRGB' } as Exif };
|
expect(sut.isSRGB({ colorspace: 'sRGB' } as Exif)).toEqual(true);
|
||||||
expect(sut.isSRGB(asset)).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for srgb profile description', () => {
|
it('should return true for srgb profile description', () => {
|
||||||
const asset = { ...assetStub.image, exifInfo: { profileDescription: 'sRGB v1.31' } as Exif };
|
expect(sut.isSRGB({ profileDescription: 'sRGB v1.31' } as Exif)).toEqual(true);
|
||||||
expect(sut.isSRGB(asset)).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for 8-bit image with no colorspace metadata', () => {
|
it('should return true for 8-bit image with no colorspace metadata', () => {
|
||||||
const asset = { ...assetStub.image, exifInfo: { bitsPerSample: 8 } as Exif };
|
expect(sut.isSRGB({ bitsPerSample: 8 } as Exif)).toEqual(true);
|
||||||
expect(sut.isSRGB(asset)).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for image with no colorspace or bit depth metadata', () => {
|
it('should return true for image with no colorspace or bit depth metadata', () => {
|
||||||
const asset = { ...assetStub.image, exifInfo: {} as Exif };
|
expect(sut.isSRGB({} as Exif)).toEqual(true);
|
||||||
expect(sut.isSRGB(asset)).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for non-srgb colorspace', () => {
|
it('should return false for non-srgb colorspace', () => {
|
||||||
const asset = { ...assetStub.image, exifInfo: { colorspace: 'Adobe RGB' } as Exif };
|
expect(sut.isSRGB({ colorspace: 'Adobe RGB' } as Exif)).toEqual(false);
|
||||||
expect(sut.isSRGB(asset)).toEqual(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for non-srgb profile description', () => {
|
it('should return false for non-srgb profile description', () => {
|
||||||
const asset = { ...assetStub.image, exifInfo: { profileDescription: 'sP3C' } as Exif };
|
expect(sut.isSRGB({ profileDescription: 'sP3C' } as Exif)).toEqual(false);
|
||||||
expect(sut.isSRGB(asset)).toEqual(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for 16-bit image with no colorspace metadata', () => {
|
it('should return false for 16-bit image with no colorspace metadata', () => {
|
||||||
const asset = { ...assetStub.image, exifInfo: { bitsPerSample: 16 } as Exif };
|
expect(sut.isSRGB({ bitsPerSample: 16 } as Exif)).toEqual(false);
|
||||||
expect(sut.isSRGB(asset)).toEqual(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for 16-bit image with sRGB colorspace', () => {
|
it('should return true for 16-bit image with sRGB colorspace', () => {
|
||||||
const asset = { ...assetStub.image, exifInfo: { colorspace: 'sRGB', bitsPerSample: 16 } as Exif };
|
expect(sut.isSRGB({ colorspace: 'sRGB', bitsPerSample: 16 } as Exif)).toEqual(true);
|
||||||
expect(sut.isSRGB(asset)).toEqual(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true for 16-bit image with sRGB profile', () => {
|
it('should return true for 16-bit image with sRGB profile', () => {
|
||||||
const asset = { ...assetStub.image, exifInfo: { profileDescription: 'sRGB', bitsPerSample: 16 } as Exif };
|
expect(sut.isSRGB({ profileDescription: 'sRGB', bitsPerSample: 16 } as Exif)).toEqual(true);
|
||||||
expect(sut.isSRGB(asset)).toEqual(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,11 +10,11 @@ import {
|
|||||||
AssetType,
|
AssetType,
|
||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
ImageFormat,
|
|
||||||
JobName,
|
JobName,
|
||||||
JobStatus,
|
JobStatus,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
QueueName,
|
QueueName,
|
||||||
|
RawExtractedFormat,
|
||||||
StorageFolder,
|
StorageFolder,
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
@ -22,12 +22,11 @@ import {
|
|||||||
VideoCodec,
|
VideoCodec,
|
||||||
VideoContainer,
|
VideoContainer,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { UpsertFileOptions, WithoutProperty } from 'src/repositories/asset.repository';
|
import { UpsertFileOptions } from 'src/repositories/asset.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import {
|
import {
|
||||||
AudioStreamInfo,
|
AudioStreamInfo,
|
||||||
DecodeToBufferOptions,
|
DecodeToBufferOptions,
|
||||||
GenerateThumbnailOptions,
|
|
||||||
JobItem,
|
JobItem,
|
||||||
JobOf,
|
JobOf,
|
||||||
VideoFormat,
|
VideoFormat,
|
||||||
@ -213,6 +212,29 @@ export class MediaService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async extractImage(originalPath: string, minSize: number) {
|
||||||
|
let extracted = await this.mediaRepository.extract(originalPath);
|
||||||
|
if (extracted && !(await this.shouldUseExtractedImage(extracted.buffer, minSize))) {
|
||||||
|
extracted = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return extracted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async decodeImage(thumbSource: string | Buffer, exifInfo: Exif, targetSize?: number) {
|
||||||
|
const { image } = await this.getConfig({ withCache: true });
|
||||||
|
const colorspace = this.isSRGB(exifInfo) ? Colorspace.SRGB : image.colorspace;
|
||||||
|
const decodeOptions: DecodeToBufferOptions = {
|
||||||
|
colorspace,
|
||||||
|
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
|
||||||
|
size: targetSize,
|
||||||
|
orientation: exifInfo.orientation ? Number(exifInfo.orientation) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { info, data } = await this.mediaRepository.decodeImage(thumbSource, decodeOptions);
|
||||||
|
return { info, data, colorspace };
|
||||||
|
}
|
||||||
|
|
||||||
private async generateImageThumbnails(asset: {
|
private async generateImageThumbnails(asset: {
|
||||||
id: string;
|
id: string;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
@ -225,68 +247,48 @@ export class MediaService extends BaseService {
|
|||||||
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
|
const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format);
|
||||||
this.storageCore.ensureFolders(previewPath);
|
this.storageCore.ensureFolders(previewPath);
|
||||||
|
|
||||||
const processInvalidImages = process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true';
|
// Handle embedded preview extraction for RAW files
|
||||||
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
|
const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
|
||||||
|
const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
|
||||||
|
const generateFullsize = image.fullsize.enabled && !mimeTypes.isWebSupportedImage(asset.originalPath);
|
||||||
|
const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
|
||||||
|
|
||||||
// prevents this extra "enabled" from leaking into fullsizeOptions later
|
const { info, data, colorspace } = await this.decodeImage(
|
||||||
const { enabled: imageFullsizeEnabled, ...imageFullsizeConfig } = image.fullsize;
|
extracted ? extracted.buffer : asset.originalPath,
|
||||||
|
asset.exifInfo,
|
||||||
|
convertFullsize ? undefined : image.preview.size,
|
||||||
|
);
|
||||||
|
|
||||||
const shouldConvertFullsize = imageFullsizeEnabled && !mimeTypes.isWebSupportedImage(asset.originalFileName);
|
// generate final images
|
||||||
const shouldExtractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
|
const thumbnailOptions = { colorspace, processInvalidImages: false, raw: info };
|
||||||
const decodeOptions: DecodeToBufferOptions = { colorspace, processInvalidImages, size: image.preview.size };
|
|
||||||
|
|
||||||
let useExtracted = false;
|
|
||||||
let decodeInputPath: string = asset.originalPath;
|
|
||||||
// Converted or extracted image from non-web-supported formats (e.g. RAW)
|
|
||||||
let fullsizePath: string | undefined;
|
|
||||||
|
|
||||||
if (shouldConvertFullsize) {
|
|
||||||
// unset size to decode fullsize image
|
|
||||||
decodeOptions.size = undefined;
|
|
||||||
fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FULLSIZE, image.fullsize.format);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldExtractEmbedded) {
|
|
||||||
// For RAW files, try extracting embedded preview first
|
|
||||||
// Assume extracted image from RAW always in JPEG format, as implied from the `jpgFromRaw` tag name
|
|
||||||
const extractedPath = StorageCore.getImagePath(asset, AssetPathType.FULLSIZE, ImageFormat.JPEG);
|
|
||||||
const didExtract = await this.mediaRepository.extract(asset.originalPath, extractedPath);
|
|
||||||
useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.preview.size));
|
|
||||||
|
|
||||||
if (useExtracted) {
|
|
||||||
if (shouldConvertFullsize) {
|
|
||||||
// skip re-encoding and directly use extracted as fullsize preview
|
|
||||||
// as usually the extracted image is already heavily compressed, no point doing lossy conversion again
|
|
||||||
fullsizePath = extractedPath;
|
|
||||||
}
|
|
||||||
// use this as origin of preview and thumbnail
|
|
||||||
decodeInputPath = extractedPath;
|
|
||||||
if (asset.exifInfo) {
|
|
||||||
// write essential orientation and colorspace EXIF for correct fullsize preview and subsequent processing
|
|
||||||
const exif = { orientation: asset.exifInfo.orientation, colorspace: asset.exifInfo.colorspace };
|
|
||||||
await this.mediaRepository.writeExif(exif, extractedPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { info, data } = await this.mediaRepository.decodeImage(decodeInputPath, decodeOptions);
|
|
||||||
|
|
||||||
const thumbnailOptions = { colorspace, processInvalidImages, raw: info };
|
|
||||||
const promises = [
|
const promises = [
|
||||||
this.mediaRepository.generateThumbhash(data, thumbnailOptions),
|
this.mediaRepository.generateThumbhash(data, thumbnailOptions),
|
||||||
this.mediaRepository.generateThumbnail(data, { ...image.thumbnail, ...thumbnailOptions }, thumbnailPath),
|
this.mediaRepository.generateThumbnail(data, { ...image.thumbnail, ...thumbnailOptions }, thumbnailPath),
|
||||||
this.mediaRepository.generateThumbnail(data, { ...image.preview, ...thumbnailOptions }, previewPath),
|
this.mediaRepository.generateThumbnail(data, { ...image.preview, ...thumbnailOptions }, previewPath),
|
||||||
];
|
];
|
||||||
|
|
||||||
// did not extract a usable image from RAW
|
let fullsizePath: string | undefined;
|
||||||
if (fullsizePath && !useExtracted) {
|
|
||||||
const fullsizeOptions: GenerateThumbnailOptions = {
|
if (convertFullsize) {
|
||||||
...imageFullsizeConfig,
|
// convert a new fullsize image from the same source as the thumbnail
|
||||||
...thumbnailOptions,
|
fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FULLSIZE, image.fullsize.format);
|
||||||
size: undefined,
|
const fullsizeOptions = { format: image.fullsize.format, quality: image.fullsize.quality, ...thumbnailOptions };
|
||||||
};
|
|
||||||
promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath));
|
promises.push(this.mediaRepository.generateThumbnail(data, fullsizeOptions, fullsizePath));
|
||||||
|
} else if (generateFullsize && extracted && extracted.format === RawExtractedFormat.JPEG) {
|
||||||
|
fullsizePath = StorageCore.getImagePath(asset, AssetPathType.FULLSIZE, extracted.format);
|
||||||
|
this.storageCore.ensureFolders(fullsizePath);
|
||||||
|
|
||||||
|
// Write the buffer to disk with essential EXIF data
|
||||||
|
await this.storageRepository.createOrOverwriteFile(fullsizePath, extracted.buffer);
|
||||||
|
await this.mediaRepository.writeExif(
|
||||||
|
{
|
||||||
|
orientation: asset.exifInfo.orientation,
|
||||||
|
colorspace: asset.exifInfo.colorspace,
|
||||||
|
},
|
||||||
|
fullsizePath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const outputs = await Promise.all(promises);
|
const outputs = await Promise.all(promises);
|
||||||
|
|
||||||
return { previewPath, thumbnailPath, fullsizePath, thumbhash: outputs[0] as Buffer };
|
return { previewPath, thumbnailPath, fullsizePath, thumbhash: outputs[0] as Buffer };
|
||||||
@ -330,25 +332,25 @@ export class MediaService extends BaseService {
|
|||||||
async handleQueueVideoConversion(job: JobOf<JobName.QUEUE_VIDEO_CONVERSION>): Promise<JobStatus> {
|
async handleQueueVideoConversion(job: JobOf<JobName.QUEUE_VIDEO_CONVERSION>): Promise<JobStatus> {
|
||||||
const { force } = job;
|
const { force } = job;
|
||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
let queue: { name: JobName.VIDEO_CONVERSION; data: { id: string } }[] = [];
|
||||||
return force
|
for await (const asset of this.assetJobRepository.streamForVideoConversion(force)) {
|
||||||
? this.assetRepository.getAll(pagination, { type: AssetType.VIDEO })
|
queue.push({ name: JobName.VIDEO_CONVERSION, data: { id: asset.id } });
|
||||||
: this.assetRepository.getWithout(pagination, WithoutProperty.ENCODED_VIDEO);
|
|
||||||
});
|
|
||||||
|
|
||||||
for await (const assets of assetPagination) {
|
if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) {
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(queue);
|
||||||
assets.map((asset) => ({ name: JobName.VIDEO_CONVERSION, data: { id: asset.id } })),
|
queue = [];
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.jobRepository.queueAll(queue);
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION })
|
@OnJob({ name: JobName.VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION })
|
||||||
async handleVideoConversion({ id }: JobOf<JobName.VIDEO_CONVERSION>): Promise<JobStatus> {
|
async handleVideoConversion({ id }: JobOf<JobName.VIDEO_CONVERSION>): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const asset = await this.assetJobRepository.getForVideoConversion(id);
|
||||||
if (!asset || asset.type !== AssetType.VIDEO) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -521,8 +523,7 @@ export class MediaService extends BaseService {
|
|||||||
return name !== VideoContainer.MP4 && !ffmpegConfig.acceptedContainers.includes(name);
|
return name !== VideoContainer.MP4 && !ffmpegConfig.acceptedContainers.includes(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSRGB(asset: { exifInfo: Exif }): boolean {
|
isSRGB({ colorspace, profileDescription, bitsPerSample }: Exif): boolean {
|
||||||
const { colorspace, profileDescription, bitsPerSample } = asset.exifInfo;
|
|
||||||
if (colorspace || profileDescription) {
|
if (colorspace || profileDescription) {
|
||||||
return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb'));
|
return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb'));
|
||||||
} else if (bitsPerSample) {
|
} else if (bitsPerSample) {
|
||||||
@ -550,10 +551,9 @@ export class MediaService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async shouldUseExtractedImage(extractedPath: string, targetSize: number) {
|
private async shouldUseExtractedImage(extractedPathOrBuffer: string | Buffer, targetSize: number) {
|
||||||
const { width, height } = await this.mediaRepository.getImageDimensions(extractedPath);
|
const { width, height } = await this.mediaRepository.getImageDimensions(extractedPathOrBuffer);
|
||||||
const extractedSize = Math.min(width, height);
|
const extractedSize = Math.min(width, height);
|
||||||
|
|
||||||
return extractedSize >= targetSize;
|
return extractedSize >= targetSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import { probeStub } from 'test/fixtures/media.stub';
|
|||||||
import { personStub } from 'test/fixtures/person.stub';
|
import { personStub } from 'test/fixtures/person.stub';
|
||||||
import { tagStub } from 'test/fixtures/tag.stub';
|
import { tagStub } from 'test/fixtures/tag.stub';
|
||||||
import { factory } from 'test/small.factory';
|
import { factory } from 'test/small.factory';
|
||||||
import { newTestService, ServiceMocks } from 'test/utils';
|
import { makeStream, newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
const makeFaceTags = (face: Partial<{ Name: string }> = {}) => ({
|
const makeFaceTags = (face: Partial<{ Name: string }> = {}) => ({
|
||||||
RegionInfo: {
|
RegionInfo: {
|
||||||
@ -104,10 +104,10 @@ describe(MetadataService.name, () => {
|
|||||||
|
|
||||||
describe('handleQueueMetadataExtraction', () => {
|
describe('handleQueueMetadataExtraction', () => {
|
||||||
it('should queue metadata extraction for all assets without exif values', async () => {
|
it('should queue metadata extraction for all assets without exif values', async () => {
|
||||||
mocks.asset.getWithout.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image]));
|
||||||
|
|
||||||
await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleQueueMetadataExtraction({ force: false })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(mocks.asset.getWithout).toHaveBeenCalled();
|
expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(false);
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.METADATA_EXTRACTION,
|
name: JobName.METADATA_EXTRACTION,
|
||||||
@ -117,10 +117,10 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should queue metadata extraction for all assets', async () => {
|
it('should queue metadata extraction for all assets', async () => {
|
||||||
mocks.asset.getAll.mockResolvedValue({ items: [assetStub.image], hasNextPage: false });
|
mocks.assetJob.streamForMetadataExtraction.mockReturnValue(makeStream([assetStub.image]));
|
||||||
|
|
||||||
await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleQueueMetadataExtraction({ force: true })).resolves.toBe(JobStatus.SUCCESS);
|
||||||
expect(mocks.asset.getAll).toHaveBeenCalled();
|
expect(mocks.assetJob.streamForMetadataExtraction).toHaveBeenCalledWith(true);
|
||||||
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
expect(mocks.job.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.METADATA_EXTRACTION,
|
name: JobName.METADATA_EXTRACTION,
|
||||||
|
@ -168,18 +168,18 @@ export class MetadataService extends BaseService {
|
|||||||
@OnJob({ name: JobName.QUEUE_METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
|
@OnJob({ name: JobName.QUEUE_METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
|
||||||
async handleQueueMetadataExtraction(job: JobOf<JobName.QUEUE_METADATA_EXTRACTION>): Promise<JobStatus> {
|
async handleQueueMetadataExtraction(job: JobOf<JobName.QUEUE_METADATA_EXTRACTION>): Promise<JobStatus> {
|
||||||
const { force } = job;
|
const { force } = job;
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
|
||||||
return force
|
|
||||||
? this.assetRepository.getAll(pagination)
|
|
||||||
: this.assetRepository.getWithout(pagination, WithoutProperty.EXIF);
|
|
||||||
});
|
|
||||||
|
|
||||||
for await (const assets of assetPagination) {
|
let queue: { name: JobName.METADATA_EXTRACTION; data: { id: string } }[] = [];
|
||||||
await this.jobRepository.queueAll(
|
for await (const asset of this.assetJobRepository.streamForMetadataExtraction(force)) {
|
||||||
assets.map((asset) => ({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id } })),
|
queue.push({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id } });
|
||||||
);
|
|
||||||
|
if (queue.length >= JOBS_ASSET_PAGINATION_SIZE) {
|
||||||
|
await this.jobRepository.queueAll(queue);
|
||||||
|
queue = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.jobRepository.queueAll(queue);
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
111
server/src/services/notification-admin.service.spec.ts
Normal file
111
server/src/services/notification-admin.service.spec.ts
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { defaults, SystemConfig } from 'src/config';
|
||||||
|
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||||
|
import { NotificationService } from 'src/services/notification.service';
|
||||||
|
import { userStub } from 'test/fixtures/user.stub';
|
||||||
|
import { newTestService, ServiceMocks } from 'test/utils';
|
||||||
|
|
||||||
|
const smtpTransport = Object.freeze<SystemConfig>({
|
||||||
|
...defaults,
|
||||||
|
notifications: {
|
||||||
|
smtp: {
|
||||||
|
...defaults.notifications.smtp,
|
||||||
|
enabled: true,
|
||||||
|
transport: {
|
||||||
|
ignoreCert: false,
|
||||||
|
host: 'localhost',
|
||||||
|
port: 587,
|
||||||
|
username: 'test',
|
||||||
|
password: 'test',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(NotificationService.name, () => {
|
||||||
|
let sut: NotificationService;
|
||||||
|
let mocks: ServiceMocks;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
({ sut, mocks } = newTestService(NotificationService));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work', () => {
|
||||||
|
expect(sut).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sendTestEmail', () => {
|
||||||
|
it('should throw error if user could not be found', async () => {
|
||||||
|
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).rejects.toThrow('User not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error if smtp validation fails', async () => {
|
||||||
|
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||||
|
mocks.email.verifySmtp.mockRejectedValue('');
|
||||||
|
|
||||||
|
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).rejects.toThrow(
|
||||||
|
'Failed to verify SMTP configuration',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send email to default domain', async () => {
|
||||||
|
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||||
|
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||||
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
|
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||||
|
|
||||||
|
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||||
|
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||||
|
template: EmailTemplate.TEST_EMAIL,
|
||||||
|
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||||
|
});
|
||||||
|
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
subject: 'Test email from Immich',
|
||||||
|
smtp: smtpTransport.notifications.smtp.transport,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send email to external domain', async () => {
|
||||||
|
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||||
|
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||||
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
|
mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
|
||||||
|
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||||
|
|
||||||
|
await expect(sut.sendTestEmail('', smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
||||||
|
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||||
|
template: EmailTemplate.TEST_EMAIL,
|
||||||
|
data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name },
|
||||||
|
});
|
||||||
|
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
subject: 'Test email from Immich',
|
||||||
|
smtp: smtpTransport.notifications.smtp.transport,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send email with replyTo', async () => {
|
||||||
|
mocks.user.get.mockResolvedValue(userStub.admin);
|
||||||
|
mocks.email.verifySmtp.mockResolvedValue(true);
|
||||||
|
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
||||||
|
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.sendTestEmail('', { ...smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }),
|
||||||
|
).resolves.not.toThrow();
|
||||||
|
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
||||||
|
template: EmailTemplate.TEST_EMAIL,
|
||||||
|
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
||||||
|
});
|
||||||
|
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
subject: 'Test email from Immich',
|
||||||
|
smtp: smtpTransport.notifications.smtp.transport,
|
||||||
|
replyTo: 'demo@immich.app',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
120
server/src/services/notification-admin.service.ts
Normal file
120
server/src/services/notification-admin.service.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.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';
|
||||||
|
import { getExternalDomain } from 'src/utils/misc';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NotificationAdminService extends BaseService {
|
||||||
|
async create(auth: AuthDto, dto: NotificationCreateDto) {
|
||||||
|
const item = await this.notificationRepository.create({
|
||||||
|
userId: dto.userId,
|
||||||
|
type: dto.type ?? NotificationType.Custom,
|
||||||
|
level: dto.level ?? NotificationLevel.Info,
|
||||||
|
title: dto.title,
|
||||||
|
description: dto.description,
|
||||||
|
data: dto.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapNotification(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTestEmail(id: string, dto: SystemConfigSmtpDto, tempTemplate?: string) {
|
||||||
|
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||||
|
if (!user) {
|
||||||
|
throw new Error('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.emailRepository.verifySmtp(dto.transport);
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestException('Failed to verify SMTP configuration', { cause: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { server } = await this.getConfig({ withCache: false });
|
||||||
|
const { html, text } = await this.emailRepository.renderEmail({
|
||||||
|
template: EmailTemplate.TEST_EMAIL,
|
||||||
|
data: {
|
||||||
|
baseUrl: getExternalDomain(server),
|
||||||
|
displayName: user.name,
|
||||||
|
},
|
||||||
|
customTemplate: tempTemplate!,
|
||||||
|
});
|
||||||
|
const { messageId } = await this.emailRepository.sendEmail({
|
||||||
|
to: user.email,
|
||||||
|
subject: 'Test email from Immich',
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
from: dto.from,
|
||||||
|
replyTo: dto.replyTo || dto.from,
|
||||||
|
smtp: dto.transport,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { messageId };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTemplate(name: EmailTemplate, customTemplate: string) {
|
||||||
|
const { server, templates } = await this.getConfig({ withCache: false });
|
||||||
|
|
||||||
|
let templateResponse = '';
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case EmailTemplate.WELCOME: {
|
||||||
|
const { html: _welcomeHtml } = await this.emailRepository.renderEmail({
|
||||||
|
template: EmailTemplate.WELCOME,
|
||||||
|
data: {
|
||||||
|
baseUrl: getExternalDomain(server),
|
||||||
|
displayName: 'John Doe',
|
||||||
|
username: 'john@doe.com',
|
||||||
|
password: 'thisIsAPassword123',
|
||||||
|
},
|
||||||
|
customTemplate: customTemplate || templates.email.welcomeTemplate,
|
||||||
|
});
|
||||||
|
|
||||||
|
templateResponse = _welcomeHtml;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case EmailTemplate.ALBUM_UPDATE: {
|
||||||
|
const { html: _updateAlbumHtml } = await this.emailRepository.renderEmail({
|
||||||
|
template: EmailTemplate.ALBUM_UPDATE,
|
||||||
|
data: {
|
||||||
|
baseUrl: getExternalDomain(server),
|
||||||
|
albumId: '1',
|
||||||
|
albumName: 'Favorite Photos',
|
||||||
|
recipientName: 'Jane Doe',
|
||||||
|
cid: undefined,
|
||||||
|
},
|
||||||
|
customTemplate: customTemplate || templates.email.albumInviteTemplate,
|
||||||
|
});
|
||||||
|
templateResponse = _updateAlbumHtml;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case EmailTemplate.ALBUM_INVITE: {
|
||||||
|
const { html } = await this.emailRepository.renderEmail({
|
||||||
|
template: EmailTemplate.ALBUM_INVITE,
|
||||||
|
data: {
|
||||||
|
baseUrl: getExternalDomain(server),
|
||||||
|
albumId: '1',
|
||||||
|
albumName: "John Doe's Favorites",
|
||||||
|
senderName: 'John Doe',
|
||||||
|
recipientName: 'Jane Doe',
|
||||||
|
cid: undefined,
|
||||||
|
},
|
||||||
|
customTemplate: customTemplate || templates.email.albumInviteTemplate,
|
||||||
|
});
|
||||||
|
templateResponse = html;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
templateResponse = '';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { name, html: templateResponse };
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,6 @@ import { defaults, SystemConfig } from 'src/config';
|
|||||||
import { AlbumUser } from 'src/database';
|
import { AlbumUser } from 'src/database';
|
||||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||||
import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum';
|
import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum';
|
||||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
|
||||||
import { NotificationService } from 'src/services/notification.service';
|
import { NotificationService } from 'src/services/notification.service';
|
||||||
import { INotifyAlbumUpdateJob } from 'src/types';
|
import { INotifyAlbumUpdateJob } from 'src/types';
|
||||||
import { albumStub } from 'test/fixtures/album.stub';
|
import { albumStub } from 'test/fixtures/album.stub';
|
||||||
@ -241,82 +240,6 @@ describe(NotificationService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sendTestEmail', () => {
|
|
||||||
it('should throw error if user could not be found', async () => {
|
|
||||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow('User not found');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw error if smtp validation fails', async () => {
|
|
||||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
|
||||||
mocks.email.verifySmtp.mockRejectedValue('');
|
|
||||||
|
|
||||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow(
|
|
||||||
'Failed to verify SMTP configuration',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send email to default domain', async () => {
|
|
||||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
|
||||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
|
||||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
|
||||||
|
|
||||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
|
||||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
|
||||||
template: EmailTemplate.TEST_EMAIL,
|
|
||||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
|
||||||
});
|
|
||||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
subject: 'Test email from Immich',
|
|
||||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send email to external domain', async () => {
|
|
||||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
|
||||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
|
||||||
mocks.systemMetadata.get.mockResolvedValue({ server: { externalDomain: 'https://demo.immich.app' } });
|
|
||||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
|
||||||
|
|
||||||
await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).resolves.not.toThrow();
|
|
||||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
|
||||||
template: EmailTemplate.TEST_EMAIL,
|
|
||||||
data: { baseUrl: 'https://demo.immich.app', displayName: userStub.admin.name },
|
|
||||||
});
|
|
||||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
subject: 'Test email from Immich',
|
|
||||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should send email with replyTo', async () => {
|
|
||||||
mocks.user.get.mockResolvedValue(userStub.admin);
|
|
||||||
mocks.email.verifySmtp.mockResolvedValue(true);
|
|
||||||
mocks.email.renderEmail.mockResolvedValue({ html: '', text: '' });
|
|
||||||
mocks.email.sendEmail.mockResolvedValue({ messageId: 'message-1', response: '' });
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.sendTestEmail('', { ...configs.smtpTransport.notifications.smtp, replyTo: 'demo@immich.app' }),
|
|
||||||
).resolves.not.toThrow();
|
|
||||||
expect(mocks.email.renderEmail).toHaveBeenCalledWith({
|
|
||||||
template: EmailTemplate.TEST_EMAIL,
|
|
||||||
data: { baseUrl: 'https://my.immich.app', displayName: userStub.admin.name },
|
|
||||||
});
|
|
||||||
expect(mocks.email.sendEmail).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
subject: 'Test email from Immich',
|
|
||||||
smtp: configs.smtpTransport.notifications.smtp.transport,
|
|
||||||
replyTo: 'demo@immich.app',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handleUserSignup', () => {
|
describe('handleUserSignup', () => {
|
||||||
it('should skip if user could not be found', async () => {
|
it('should skip if user could not be found', async () => {
|
||||||
await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SKIPPED);
|
await expect(sut.handleUserSignup({ id: '' })).resolves.toBe(JobStatus.SKIPPED);
|
||||||
|
@ -1,7 +1,24 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnEvent, OnJob } from 'src/decorators';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import {
|
||||||
|
mapNotification,
|
||||||
|
NotificationDeleteAllDto,
|
||||||
|
NotificationDto,
|
||||||
|
NotificationSearchDto,
|
||||||
|
NotificationUpdateAllDto,
|
||||||
|
NotificationUpdateDto,
|
||||||
|
} from 'src/dtos/notification.dto';
|
||||||
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||||
import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum';
|
import {
|
||||||
|
AssetFileType,
|
||||||
|
JobName,
|
||||||
|
JobStatus,
|
||||||
|
NotificationLevel,
|
||||||
|
NotificationType,
|
||||||
|
Permission,
|
||||||
|
QueueName,
|
||||||
|
} from 'src/enum';
|
||||||
import { EmailTemplate } from 'src/repositories/email.repository';
|
import { EmailTemplate } from 'src/repositories/email.repository';
|
||||||
import { ArgOf } from 'src/repositories/event.repository';
|
import { ArgOf } from 'src/repositories/event.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
@ -15,6 +32,80 @@ import { getPreferences } from 'src/utils/preferences';
|
|||||||
export class NotificationService extends BaseService {
|
export class NotificationService extends BaseService {
|
||||||
private static albumUpdateEmailDelayMs = 300_000;
|
private static albumUpdateEmailDelayMs = 300_000;
|
||||||
|
|
||||||
|
async search(auth: AuthDto, dto: NotificationSearchDto): Promise<NotificationDto[]> {
|
||||||
|
const items = await this.notificationRepository.search(auth.user.id, dto);
|
||||||
|
return items.map((item) => mapNotification(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAll(auth: AuthDto, dto: NotificationUpdateAllDto) {
|
||||||
|
await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_UPDATE });
|
||||||
|
await this.notificationRepository.updateAll(dto.ids, {
|
||||||
|
readAt: dto.readAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAll(auth: AuthDto, dto: NotificationDeleteAllDto) {
|
||||||
|
await this.requireAccess({ auth, ids: dto.ids, permission: Permission.NOTIFICATION_DELETE });
|
||||||
|
await this.notificationRepository.deleteAll(dto.ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(auth: AuthDto, id: string) {
|
||||||
|
await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_READ });
|
||||||
|
const item = await this.notificationRepository.get(id);
|
||||||
|
if (!item) {
|
||||||
|
throw new BadRequestException('Notification not found');
|
||||||
|
}
|
||||||
|
return mapNotification(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(auth: AuthDto, id: string, dto: NotificationUpdateDto) {
|
||||||
|
await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_UPDATE });
|
||||||
|
const item = await this.notificationRepository.update(id, {
|
||||||
|
readAt: dto.readAt,
|
||||||
|
});
|
||||||
|
return mapNotification(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(auth: AuthDto, id: string) {
|
||||||
|
await this.requireAccess({ auth, ids: [id], permission: Permission.NOTIFICATION_DELETE });
|
||||||
|
await this.notificationRepository.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnJob({ name: JobName.NOTIFICATIONS_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
||||||
|
async onNotificationsCleanup() {
|
||||||
|
await this.notificationRepository.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnEvent({ name: 'job.failed' })
|
||||||
|
async onJobFailed({ job, error }: ArgOf<'job.failed'>) {
|
||||||
|
const admin = await this.userRepository.getAdmin();
|
||||||
|
if (!admin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`Unable to run job handler (${job.name}): ${error}`, error?.stack, JSON.stringify(job.data));
|
||||||
|
|
||||||
|
switch (job.name) {
|
||||||
|
case JobName.BACKUP_DATABASE: {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : error;
|
||||||
|
const item = await this.notificationRepository.create({
|
||||||
|
userId: admin.id,
|
||||||
|
type: NotificationType.JobFailed,
|
||||||
|
level: NotificationLevel.Error,
|
||||||
|
title: 'Job Failed',
|
||||||
|
description: `Job ${[job.name]} failed with error: ${errorMessage}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.eventRepository.clientSend('on_notification', admin.id, mapNotification(item));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'config.update' })
|
@OnEvent({ name: 'config.update' })
|
||||||
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
|
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
|
||||||
this.eventRepository.clientBroadcast('on_config_update');
|
this.eventRepository.clientBroadcast('on_config_update');
|
||||||
@ -271,7 +362,7 @@ export class NotificationService extends BaseService {
|
|||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { emailNotifications } = getPreferences(recipient.email, recipient.metadata);
|
const { emailNotifications } = getPreferences(recipient.metadata);
|
||||||
|
|
||||||
if (!emailNotifications.enabled || !emailNotifications.albumInvite) {
|
if (!emailNotifications.enabled || !emailNotifications.albumInvite) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@ -333,7 +424,7 @@ export class NotificationService extends BaseService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { emailNotifications } = getPreferences(user.email, user.metadata);
|
const { emailNotifications } = getPreferences(user.metadata);
|
||||||
|
|
||||||
if (!emailNotifications.enabled || !emailNotifications.albumUpdate) {
|
if (!emailNotifications.enabled || !emailNotifications.albumUpdate) {
|
||||||
continue;
|
continue;
|
||||||
|
@ -106,21 +106,19 @@ export class UserAdminService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPreferences(auth: AuthDto, id: string): Promise<UserPreferencesResponseDto> {
|
async getPreferences(auth: AuthDto, id: string): Promise<UserPreferencesResponseDto> {
|
||||||
const { email } = await this.findOrFail(id, { withDeleted: true });
|
await this.findOrFail(id, { withDeleted: true });
|
||||||
const metadata = await this.userRepository.getMetadata(id);
|
const metadata = await this.userRepository.getMetadata(id);
|
||||||
const preferences = getPreferences(email, metadata);
|
return mapPreferences(getPreferences(metadata));
|
||||||
return mapPreferences(preferences);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updatePreferences(auth: AuthDto, id: string, dto: UserPreferencesUpdateDto) {
|
async updatePreferences(auth: AuthDto, id: string, dto: UserPreferencesUpdateDto) {
|
||||||
const { email } = await this.findOrFail(id, { withDeleted: false });
|
await this.findOrFail(id, { withDeleted: false });
|
||||||
const metadata = await this.userRepository.getMetadata(id);
|
const metadata = await this.userRepository.getMetadata(id);
|
||||||
const preferences = getPreferences(email, metadata);
|
const newPreferences = mergePreferences(getPreferences(metadata), dto);
|
||||||
const newPreferences = mergePreferences(preferences, dto);
|
|
||||||
|
|
||||||
await this.userRepository.upsertMetadata(id, {
|
await this.userRepository.upsertMetadata(id, {
|
||||||
key: UserMetadataKey.PREFERENCES,
|
key: UserMetadataKey.PREFERENCES,
|
||||||
value: getPreferencesPartial({ email }, newPreferences),
|
value: getPreferencesPartial(newPreferences),
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapPreferences(newPreferences);
|
return mapPreferences(newPreferences);
|
||||||
|
@ -53,6 +53,7 @@ export class UserService extends BaseService {
|
|||||||
const update: Updateable<UserTable> = {
|
const update: Updateable<UserTable> = {
|
||||||
email: dto.email,
|
email: dto.email,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
|
avatarColor: dto.avatarColor,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (dto.password) {
|
if (dto.password) {
|
||||||
@ -68,18 +69,16 @@ export class UserService extends BaseService {
|
|||||||
|
|
||||||
async getMyPreferences(auth: AuthDto): Promise<UserPreferencesResponseDto> {
|
async getMyPreferences(auth: AuthDto): Promise<UserPreferencesResponseDto> {
|
||||||
const metadata = await this.userRepository.getMetadata(auth.user.id);
|
const metadata = await this.userRepository.getMetadata(auth.user.id);
|
||||||
const preferences = getPreferences(auth.user.email, metadata);
|
return mapPreferences(getPreferences(metadata));
|
||||||
return mapPreferences(preferences);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateMyPreferences(auth: AuthDto, dto: UserPreferencesUpdateDto) {
|
async updateMyPreferences(auth: AuthDto, dto: UserPreferencesUpdateDto) {
|
||||||
const metadata = await this.userRepository.getMetadata(auth.user.id);
|
const metadata = await this.userRepository.getMetadata(auth.user.id);
|
||||||
const current = getPreferences(auth.user.email, metadata);
|
const updated = mergePreferences(getPreferences(metadata), dto);
|
||||||
const updated = mergePreferences(current, dto);
|
|
||||||
|
|
||||||
await this.userRepository.upsertMetadata(auth.user.id, {
|
await this.userRepository.upsertMetadata(auth.user.id, {
|
||||||
key: UserMetadataKey.PREFERENCES,
|
key: UserMetadataKey.PREFERENCES,
|
||||||
value: getPreferencesPartial(auth.user, updated),
|
value: getPreferencesPartial(updated),
|
||||||
});
|
});
|
||||||
|
|
||||||
return mapPreferences(updated);
|
return mapPreferences(updated);
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
SyncEntityType,
|
SyncEntityType,
|
||||||
SystemMetadataKey,
|
SystemMetadataKey,
|
||||||
TranscodeTarget,
|
TranscodeTarget,
|
||||||
UserAvatarColor,
|
|
||||||
UserMetadataKey,
|
UserMetadataKey,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
@ -298,6 +297,10 @@ export type JobItem =
|
|||||||
// Metadata Extraction
|
// Metadata Extraction
|
||||||
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
||||||
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
|
| { name: JobName.METADATA_EXTRACTION; data: IEntityJob }
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
| { name: JobName.NOTIFICATIONS_CLEANUP; data?: IBaseJob }
|
||||||
|
|
||||||
// Sidecar Scanning
|
// Sidecar Scanning
|
||||||
| { name: JobName.QUEUE_SIDECAR; data: IBaseJob }
|
| { name: JobName.QUEUE_SIDECAR; data: IBaseJob }
|
||||||
| { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob }
|
| { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob }
|
||||||
@ -486,9 +489,6 @@ export interface UserPreferences {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sidebarWeb: boolean;
|
sidebarWeb: boolean;
|
||||||
};
|
};
|
||||||
avatar: {
|
|
||||||
color: UserAvatarColor;
|
|
||||||
};
|
|
||||||
emailNotifications: {
|
emailNotifications: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
albumInvite: boolean;
|
albumInvite: boolean;
|
||||||
|
@ -221,6 +221,12 @@ const checkOtherAccess = async (access: AccessRepository, request: OtherAccessRe
|
|||||||
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
return access.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case Permission.NOTIFICATION_READ:
|
||||||
|
case Permission.NOTIFICATION_UPDATE:
|
||||||
|
case Permission.NOTIFICATION_DELETE: {
|
||||||
|
return access.notification.checkOwnerAccess(auth.user.id, ids);
|
||||||
|
}
|
||||||
|
|
||||||
case Permission.TAG_ASSET:
|
case Permission.TAG_ASSET:
|
||||||
case Permission.TAG_READ:
|
case Permission.TAG_READ:
|
||||||
case Permission.TAG_UPDATE:
|
case Permission.TAG_UPDATE:
|
||||||
|
@ -34,45 +34,40 @@ const raw: Record<string, string[]> = {
|
|||||||
'.x3f': ['image/x3f', 'image/x-sigma-x3f'],
|
'.x3f': ['image/x3f', 'image/x-sigma-x3f'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* list of supported image extensions from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types excluding svg
|
||||||
|
* @TODO share with the client
|
||||||
|
* @see {@link web/src/lib/utils/asset-utils.ts#L329}
|
||||||
|
**/
|
||||||
|
const webSupportedImage = {
|
||||||
|
'.avif': ['image/avif'],
|
||||||
|
'.gif': ['image/gif'],
|
||||||
|
'.jpeg': ['image/jpeg'],
|
||||||
|
'.jpg': ['image/jpeg'],
|
||||||
|
'.png': ['image/png', 'image/apng'],
|
||||||
|
'.webp': ['image/webp'],
|
||||||
|
};
|
||||||
|
|
||||||
const image: Record<string, string[]> = {
|
const image: Record<string, string[]> = {
|
||||||
...raw,
|
...raw,
|
||||||
'.avif': ['image/avif'],
|
...webSupportedImage,
|
||||||
'.bmp': ['image/bmp'],
|
'.bmp': ['image/bmp'],
|
||||||
'.gif': ['image/gif'],
|
|
||||||
'.heic': ['image/heic'],
|
'.heic': ['image/heic'],
|
||||||
'.heif': ['image/heif'],
|
'.heif': ['image/heif'],
|
||||||
'.hif': ['image/hif'],
|
'.hif': ['image/hif'],
|
||||||
'.insp': ['image/jpeg'],
|
'.insp': ['image/jpeg'],
|
||||||
'.jp2': ['image/jp2'],
|
'.jp2': ['image/jp2'],
|
||||||
'.jpe': ['image/jpeg'],
|
'.jpe': ['image/jpeg'],
|
||||||
'.jpeg': ['image/jpeg'],
|
|
||||||
'.jpg': ['image/jpeg'],
|
|
||||||
'.jxl': ['image/jxl'],
|
'.jxl': ['image/jxl'],
|
||||||
'.png': ['image/png'],
|
|
||||||
'.svg': ['image/svg'],
|
'.svg': ['image/svg'],
|
||||||
'.tif': ['image/tiff'],
|
'.tif': ['image/tiff'],
|
||||||
'.tiff': ['image/tiff'],
|
'.tiff': ['image/tiff'],
|
||||||
'.webp': ['image/webp'],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const extensionOverrides: Record<string, string> = {
|
const extensionOverrides: Record<string, string> = {
|
||||||
'image/jpeg': '.jpg',
|
'image/jpeg': '.jpg',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* list of supported image extensions from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types excluding svg
|
|
||||||
* @TODO share with the client
|
|
||||||
* @see {@link web/src/lib/utils/asset-utils.ts#L329}
|
|
||||||
**/
|
|
||||||
const webSupportedImageMimeTypes = new Set([
|
|
||||||
'image/apng',
|
|
||||||
'image/avif',
|
|
||||||
'image/gif',
|
|
||||||
'image/jpeg',
|
|
||||||
'image/png',
|
|
||||||
'image/webp',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']);
|
const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp', '.svg']);
|
||||||
const profile: Record<string, string[]> = Object.fromEntries(
|
const profile: Record<string, string[]> = Object.fromEntries(
|
||||||
Object.entries(image).filter(([key]) => profileExtensions.has(key)),
|
Object.entries(image).filter(([key]) => profileExtensions.has(key)),
|
||||||
@ -123,7 +118,7 @@ export const mimeTypes = {
|
|||||||
|
|
||||||
isAsset: (filename: string) => isType(filename, image) || isType(filename, video),
|
isAsset: (filename: string) => isType(filename, image) || isType(filename, video),
|
||||||
isImage: (filename: string) => isType(filename, image),
|
isImage: (filename: string) => isType(filename, image),
|
||||||
isWebSupportedImage: (filename: string) => webSupportedImageMimeTypes.has(lookup(filename)),
|
isWebSupportedImage: (filename: string) => isType(filename, webSupportedImage),
|
||||||
isProfile: (filename: string) => isType(filename, profile),
|
isProfile: (filename: string) => isType(filename, profile),
|
||||||
isSidecar: (filename: string) => isType(filename, sidecar),
|
isSidecar: (filename: string) => isType(filename, sidecar),
|
||||||
isVideo: (filename: string) => isType(filename, video),
|
isVideo: (filename: string) => isType(filename, video),
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto';
|
||||||
import { UserAvatarColor, UserMetadataKey } from 'src/enum';
|
import { UserMetadataKey } from 'src/enum';
|
||||||
import { DeepPartial, UserMetadataItem, UserPreferences } from 'src/types';
|
import { DeepPartial, UserMetadataItem, UserPreferences } from 'src/types';
|
||||||
import { HumanReadableSize } from 'src/utils/bytes';
|
import { HumanReadableSize } from 'src/utils/bytes';
|
||||||
import { getKeysDeep } from 'src/utils/misc';
|
import { getKeysDeep } from 'src/utils/misc';
|
||||||
|
|
||||||
const getDefaultPreferences = (user: { email: string }): UserPreferences => {
|
const getDefaultPreferences = (): UserPreferences => {
|
||||||
const values = Object.values(UserAvatarColor);
|
|
||||||
const randomIndex = Math.floor(
|
|
||||||
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
folders: {
|
folders: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@ -34,9 +29,6 @@ const getDefaultPreferences = (user: { email: string }): UserPreferences => {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
sidebarWeb: false,
|
sidebarWeb: false,
|
||||||
},
|
},
|
||||||
avatar: {
|
|
||||||
color: values[randomIndex],
|
|
||||||
},
|
|
||||||
emailNotifications: {
|
emailNotifications: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
albumInvite: true,
|
albumInvite: true,
|
||||||
@ -53,8 +45,8 @@ const getDefaultPreferences = (user: { email: string }): UserPreferences => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPreferences = (email: string, metadata: UserMetadataItem[]): UserPreferences => {
|
export const getPreferences = (metadata: UserMetadataItem[]): UserPreferences => {
|
||||||
const preferences = getDefaultPreferences({ email });
|
const preferences = getDefaultPreferences();
|
||||||
const item = metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES);
|
const item = metadata.find(({ key }) => key === UserMetadataKey.PREFERENCES);
|
||||||
const partial = item?.value || {};
|
const partial = item?.value || {};
|
||||||
for (const property of getKeysDeep(partial)) {
|
for (const property of getKeysDeep(partial)) {
|
||||||
@ -64,8 +56,8 @@ export const getPreferences = (email: string, metadata: UserMetadataItem[]): Use
|
|||||||
return preferences;
|
return preferences;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPreferencesPartial = (user: { email: string }, newPreferences: UserPreferences) => {
|
export const getPreferencesPartial = (newPreferences: UserPreferences) => {
|
||||||
const defaultPreferences = getDefaultPreferences(user);
|
const defaultPreferences = getDefaultPreferences();
|
||||||
const partial: DeepPartial<UserPreferences> = {};
|
const partial: DeepPartial<UserPreferences> = {};
|
||||||
for (const property of getKeysDeep(defaultPreferences)) {
|
for (const property of getKeysDeep(defaultPreferences)) {
|
||||||
const newValue = _.get(newPreferences, property);
|
const newValue = _.get(newPreferences, property);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||||
import { json } from 'body-parser';
|
import { json } from 'body-parser';
|
||||||
|
import compression from 'compression';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import sirv from 'sirv';
|
import sirv from 'sirv';
|
||||||
@ -60,6 +61,7 @@ async function bootstrap() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
app.use(app.get(ApiService).ssr(excludePaths));
|
app.use(app.get(ApiService).ssr(excludePaths));
|
||||||
|
app.use(compression());
|
||||||
|
|
||||||
const server = await (host ? app.listen(port, host) : app.listen(port));
|
const server = await (host ? app.listen(port, host) : app.listen(port));
|
||||||
server.requestTimeout = 24 * 60 * 60 * 1000;
|
server.requestTimeout = 24 * 60 * 60 * 1000;
|
||||||
|
12
server/test/fixtures/user.stub.ts
vendored
12
server/test/fixtures/user.stub.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
import { UserAdmin } from 'src/database';
|
import { UserAdmin } from 'src/database';
|
||||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
import { UserStatus } from 'src/enum';
|
||||||
import { authStub } from 'test/fixtures/auth.stub';
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
|
||||||
export const userStub = {
|
export const userStub = {
|
||||||
@ -12,6 +12,7 @@ export const userStub = {
|
|||||||
storageLabel: 'admin',
|
storageLabel: 'admin',
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
|
avatarColor: null,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
@ -28,16 +29,12 @@ export const userStub = {
|
|||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
|
avatarColor: null,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
updatedAt: new Date('2021-01-01'),
|
updatedAt: new Date('2021-01-01'),
|
||||||
metadata: [
|
metadata: [],
|
||||||
{
|
|
||||||
key: UserMetadataKey.PREFERENCES,
|
|
||||||
value: { avatar: { color: UserAvatarColor.PRIMARY } },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
quotaSizeInBytes: null,
|
quotaSizeInBytes: null,
|
||||||
quotaUsageInBytes: 0,
|
quotaUsageInBytes: 0,
|
||||||
},
|
},
|
||||||
@ -50,6 +47,7 @@ export const userStub = {
|
|||||||
storageLabel: null,
|
storageLabel: null,
|
||||||
oauthId: '',
|
oauthId: '',
|
||||||
shouldChangePassword: false,
|
shouldChangePassword: false,
|
||||||
|
avatarColor: null,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
createdAt: new Date('2021-01-01'),
|
createdAt: new Date('2021-01-01'),
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
|
@ -13,9 +13,11 @@ import { AssetRepository } from 'src/repositories/asset.repository';
|
|||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||||
|
import { EmailRepository } from 'src/repositories/email.repository';
|
||||||
import { JobRepository } from 'src/repositories/job.repository';
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||||
|
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
import { SearchRepository } from 'src/repositories/search.repository';
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
@ -42,10 +44,12 @@ type RepositoriesTypes = {
|
|||||||
config: ConfigRepository;
|
config: ConfigRepository;
|
||||||
crypto: CryptoRepository;
|
crypto: CryptoRepository;
|
||||||
database: DatabaseRepository;
|
database: DatabaseRepository;
|
||||||
|
email: EmailRepository;
|
||||||
job: JobRepository;
|
job: JobRepository;
|
||||||
user: UserRepository;
|
user: UserRepository;
|
||||||
logger: LoggingRepository;
|
logger: LoggingRepository;
|
||||||
memory: MemoryRepository;
|
memory: MemoryRepository;
|
||||||
|
notification: NotificationRepository;
|
||||||
partner: PartnerRepository;
|
partner: PartnerRepository;
|
||||||
person: PersonRepository;
|
person: PersonRepository;
|
||||||
search: SearchRepository;
|
search: SearchRepository;
|
||||||
@ -142,6 +146,11 @@ export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kys
|
|||||||
return new DatabaseRepository(db, new LoggingRepository(undefined, configRepo), configRepo);
|
return new DatabaseRepository(db, new LoggingRepository(undefined, configRepo), configRepo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'email': {
|
||||||
|
const logger = new LoggingRepository(undefined, new ConfigRepository());
|
||||||
|
return new EmailRepository(logger);
|
||||||
|
}
|
||||||
|
|
||||||
case 'logger': {
|
case 'logger': {
|
||||||
const configMock = { getEnv: () => ({ noColor: false }) };
|
const configMock = { getEnv: () => ({ noColor: false }) };
|
||||||
return new LoggingRepository(undefined, configMock as ConfigRepository);
|
return new LoggingRepository(undefined, configMock as ConfigRepository);
|
||||||
@ -151,6 +160,10 @@ export const getRepository = <K extends keyof RepositoriesTypes>(key: K, db: Kys
|
|||||||
return new MemoryRepository(db);
|
return new MemoryRepository(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'notification': {
|
||||||
|
return new NotificationRepository(db);
|
||||||
|
}
|
||||||
|
|
||||||
case 'partner': {
|
case 'partner': {
|
||||||
return new PartnerRepository(db);
|
return new PartnerRepository(db);
|
||||||
}
|
}
|
||||||
@ -221,6 +234,10 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'email': {
|
||||||
|
return automock(EmailRepository, { args: [{ setContext: () => {} }] });
|
||||||
|
}
|
||||||
|
|
||||||
case 'job': {
|
case 'job': {
|
||||||
return automock(JobRepository, { args: [undefined, undefined, undefined, { setContext: () => {} }] });
|
return automock(JobRepository, { args: [undefined, undefined, undefined, { setContext: () => {} }] });
|
||||||
}
|
}
|
||||||
@ -234,6 +251,10 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
|
|||||||
return automock(MemoryRepository);
|
return automock(MemoryRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'notification': {
|
||||||
|
return automock(NotificationRepository);
|
||||||
|
}
|
||||||
|
|
||||||
case 'partner': {
|
case 'partner': {
|
||||||
return automock(PartnerRepository);
|
return automock(PartnerRepository);
|
||||||
}
|
}
|
||||||
@ -284,7 +305,7 @@ export const asDeps = (repositories: ServiceOverrides) => {
|
|||||||
repositories.crypto || getRepositoryMock('crypto'),
|
repositories.crypto || getRepositoryMock('crypto'),
|
||||||
repositories.database || getRepositoryMock('database'),
|
repositories.database || getRepositoryMock('database'),
|
||||||
repositories.downloadRepository,
|
repositories.downloadRepository,
|
||||||
repositories.email,
|
repositories.email || getRepositoryMock('email'),
|
||||||
repositories.event,
|
repositories.event,
|
||||||
repositories.job || getRepositoryMock('job'),
|
repositories.job || getRepositoryMock('job'),
|
||||||
repositories.library,
|
repositories.library,
|
||||||
@ -294,6 +315,7 @@ export const asDeps = (repositories: ServiceOverrides) => {
|
|||||||
repositories.memory || getRepositoryMock('memory'),
|
repositories.memory || getRepositoryMock('memory'),
|
||||||
repositories.metadata,
|
repositories.metadata,
|
||||||
repositories.move,
|
repositories.move,
|
||||||
|
repositories.notification || getRepositoryMock('notification'),
|
||||||
repositories.oauth,
|
repositories.oauth,
|
||||||
repositories.partner || getRepositoryMock('partner'),
|
repositories.partner || getRepositoryMock('partner'),
|
||||||
repositories.person || getRepositoryMock('person'),
|
repositories.person || getRepositoryMock('person'),
|
||||||
|
@ -0,0 +1,86 @@
|
|||||||
|
import { NotificationController } from 'src/controllers/notification.controller';
|
||||||
|
import { AuthService } from 'src/services/auth.service';
|
||||||
|
import { NotificationService } from 'src/services/notification.service';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { errorDto } from 'test/medium/responses';
|
||||||
|
import { createControllerTestApp, TestControllerApp } from 'test/medium/utils';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
|
||||||
|
describe(NotificationController.name, () => {
|
||||||
|
let realApp: TestControllerApp;
|
||||||
|
let mockApp: TestControllerApp;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
realApp = await createControllerTestApp({ authType: 'real' });
|
||||||
|
mockApp = await createControllerTestApp({ authType: 'mock' });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /notifications', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(realApp.getHttpServer()).get('/notifications');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the service with an auth dto', async () => {
|
||||||
|
const auth = factory.auth({ user: factory.user() });
|
||||||
|
mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth);
|
||||||
|
const service = mockApp.getMockedService(NotificationService);
|
||||||
|
|
||||||
|
const { status } = await request(mockApp.getHttpServer())
|
||||||
|
.get('/notifications')
|
||||||
|
.set('Authorization', `Bearer token`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(service.search).toHaveBeenCalledWith(auth, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should reject an invalid notification level`, async () => {
|
||||||
|
const auth = factory.auth({ user: factory.user() });
|
||||||
|
mockApp.getMockedService(AuthService).authenticate.mockResolvedValue(auth);
|
||||||
|
const service = mockApp.getMockedService(NotificationService);
|
||||||
|
|
||||||
|
const { status, body } = await request(mockApp.getHttpServer())
|
||||||
|
.get(`/notifications`)
|
||||||
|
.query({ level: 'invalid' })
|
||||||
|
.set('Authorization', `Bearer token`);
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')]));
|
||||||
|
expect(service.search).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /notifications', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(realApp.getHttpServer())
|
||||||
|
.put(`/notifications`)
|
||||||
|
.send({ ids: [], readAt: new Date().toISOString() });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /notifications/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(realApp.getHttpServer()).get(`/notifications/${factory.uuid()}`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /notifications/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(realApp.getHttpServer())
|
||||||
|
.put(`/notifications/${factory.uuid()}`)
|
||||||
|
.send({ readAt: factory.date() });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await realApp.close();
|
||||||
|
await mockApp.close();
|
||||||
|
});
|
||||||
|
});
|
@ -37,6 +37,10 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => {
|
|||||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
notification: {
|
||||||
|
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
|
},
|
||||||
|
|
||||||
person: {
|
person: {
|
||||||
checkFaceOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkFaceOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()),
|
||||||
|
@ -8,7 +8,7 @@ export const newMediaRepositoryMock = (): Mocked<RepositoryInterface<MediaReposi
|
|||||||
writeExif: vitest.fn().mockImplementation(() => Promise.resolve()),
|
writeExif: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')),
|
generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')),
|
||||||
decodeImage: vitest.fn().mockResolvedValue({ data: Buffer.from(''), info: {} }),
|
decodeImage: vitest.fn().mockResolvedValue({ data: Buffer.from(''), info: {} }),
|
||||||
extract: vitest.fn().mockResolvedValue(false),
|
extract: vitest.fn().mockResolvedValue(null),
|
||||||
probe: vitest.fn(),
|
probe: vitest.fn(),
|
||||||
transcode: vitest.fn(),
|
transcode: vitest.fn(),
|
||||||
getImageDimensions: vitest.fn(),
|
getImageDimensions: vitest.fn(),
|
||||||
|
@ -140,6 +140,7 @@ const userFactory = (user: Partial<User> = {}) => ({
|
|||||||
id: newUuid(),
|
id: newUuid(),
|
||||||
name: 'Test User',
|
name: 'Test User',
|
||||||
email: 'test@immich.cloud',
|
email: 'test@immich.cloud',
|
||||||
|
avatarColor: null,
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
profileChangedAt: newDate(),
|
profileChangedAt: newDate(),
|
||||||
...user,
|
...user,
|
||||||
@ -155,6 +156,7 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
|
|||||||
storageLabel = null,
|
storageLabel = null,
|
||||||
shouldChangePassword = false,
|
shouldChangePassword = false,
|
||||||
isAdmin = false,
|
isAdmin = false,
|
||||||
|
avatarColor = null,
|
||||||
createdAt = newDate(),
|
createdAt = newDate(),
|
||||||
updatedAt = newDate(),
|
updatedAt = newDate(),
|
||||||
deletedAt = null,
|
deletedAt = null,
|
||||||
@ -173,6 +175,7 @@ const userAdminFactory = (user: Partial<UserAdmin> = {}) => {
|
|||||||
storageLabel,
|
storageLabel,
|
||||||
shouldChangePassword,
|
shouldChangePassword,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
avatarColor,
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
deletedAt,
|
deletedAt,
|
||||||
@ -311,4 +314,5 @@ export const factory = {
|
|||||||
sidecarWrite: assetSidecarWriteFactory,
|
sidecarWrite: assetSidecarWriteFactory,
|
||||||
},
|
},
|
||||||
uuid: newUuid,
|
uuid: newUuid,
|
||||||
|
date: newDate,
|
||||||
};
|
};
|
||||||
|
@ -29,6 +29,7 @@ import { MediaRepository } from 'src/repositories/media.repository';
|
|||||||
import { MemoryRepository } from 'src/repositories/memory.repository';
|
import { MemoryRepository } from 'src/repositories/memory.repository';
|
||||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||||
import { MoveRepository } from 'src/repositories/move.repository';
|
import { MoveRepository } from 'src/repositories/move.repository';
|
||||||
|
import { NotificationRepository } from 'src/repositories/notification.repository';
|
||||||
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
import { OAuthRepository } from 'src/repositories/oauth.repository';
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
@ -135,6 +136,7 @@ export type ServiceOverrides = {
|
|||||||
memory: MemoryRepository;
|
memory: MemoryRepository;
|
||||||
metadata: MetadataRepository;
|
metadata: MetadataRepository;
|
||||||
move: MoveRepository;
|
move: MoveRepository;
|
||||||
|
notification: NotificationRepository;
|
||||||
oauth: OAuthRepository;
|
oauth: OAuthRepository;
|
||||||
partner: PartnerRepository;
|
partner: PartnerRepository;
|
||||||
person: PersonRepository;
|
person: PersonRepository;
|
||||||
@ -202,6 +204,7 @@ export const newTestService = <T extends BaseService>(
|
|||||||
memory: automock(MemoryRepository),
|
memory: automock(MemoryRepository),
|
||||||
metadata: newMetadataRepositoryMock(),
|
metadata: newMetadataRepositoryMock(),
|
||||||
move: automock(MoveRepository, { strict: false }),
|
move: automock(MoveRepository, { strict: false }),
|
||||||
|
notification: automock(NotificationRepository),
|
||||||
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
oauth: automock(OAuthRepository, { args: [loggerMock] }),
|
||||||
partner: automock(PartnerRepository, { strict: false }),
|
partner: automock(PartnerRepository, { strict: false }),
|
||||||
person: newPersonRepositoryMock(),
|
person: newPersonRepositoryMock(),
|
||||||
@ -250,6 +253,7 @@ export const newTestService = <T extends BaseService>(
|
|||||||
overrides.memory || (mocks.memory as As<MemoryRepository>),
|
overrides.memory || (mocks.memory as As<MemoryRepository>),
|
||||||
overrides.metadata || (mocks.metadata as As<MetadataRepository>),
|
overrides.metadata || (mocks.metadata as As<MetadataRepository>),
|
||||||
overrides.move || (mocks.move as As<MoveRepository>),
|
overrides.move || (mocks.move as As<MoveRepository>),
|
||||||
|
overrides.notification || (mocks.notification as As<NotificationRepository>),
|
||||||
overrides.oauth || (mocks.oauth as As<OAuthRepository>),
|
overrides.oauth || (mocks.oauth as As<OAuthRepository>),
|
||||||
overrides.partner || (mocks.partner as As<PartnerRepository>),
|
overrides.partner || (mocks.partner as As<PartnerRepository>),
|
||||||
overrides.person || (mocks.person as As<PersonRepository>),
|
overrides.person || (mocks.person as As<PersonRepository>),
|
||||||
|
@ -58,6 +58,8 @@ export default typescriptEslint.config(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ignores: ['**/service-worker/**'],
|
||||||
|
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/no-unused-vars': [
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'warn',
|
'warn',
|
||||||
|
8
web/package-lock.json
generated
8
web/package-lock.json
generated
@ -11,7 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@immich/ui": "^0.17.3",
|
"@immich/ui": "^0.18.1",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.11.5",
|
"@photo-sphere-viewer/core": "^5.11.5",
|
||||||
@ -1320,9 +1320,9 @@
|
|||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@immich/ui": {
|
"node_modules/@immich/ui": {
|
||||||
"version": "0.17.4",
|
"version": "0.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.17.4.tgz",
|
"resolved": "https://registry.npmjs.org/@immich/ui/-/ui-0.18.1.tgz",
|
||||||
"integrity": "sha512-a6M7Fxno5fwY5A0kxdluS8r+A4L6xZhSTKMW8c8hoFhQHvbBTHAsGFKQF3GOEQLOlUuvsS2Lt7dMevBlAPgo/A==",
|
"integrity": "sha512-XWWO6OTfH3MektyxCn0hWefZyOGyWwwx/2zHinuShpxTHSyfveJ4mOkFP8DkyMz0dnvJ1EfdkPBMkld3y5R/Hw==",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@immich/ui": "^0.17.3",
|
"@immich/ui": "^0.18.1",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.11.5",
|
"@photo-sphere-viewer/core": "^5.11.5",
|
||||||
|
@ -51,7 +51,7 @@
|
|||||||
let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused);
|
let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused);
|
||||||
let multipleButtons = $derived(allText || refreshText);
|
let multipleButtons = $derived(allText || refreshText);
|
||||||
|
|
||||||
const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pr-4 pl-6';
|
const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pe-4 ps-6';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -110,7 +110,7 @@
|
|||||||
|
|
||||||
<div class="mt-2 flex w-full max-w-md flex-col sm:flex-row">
|
<div class="mt-2 flex w-full max-w-md flex-col sm:flex-row">
|
||||||
<div
|
<div
|
||||||
class="{commonClasses} rounded-t-lg bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray sm:rounded-l-lg sm:rounded-r-none"
|
class="{commonClasses} rounded-t-lg bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray sm:rounded-s-lg sm:rounded-e-none"
|
||||||
>
|
>
|
||||||
<p>{$t('active')}</p>
|
<p>{$t('active')}</p>
|
||||||
<p class="text-2xl">
|
<p class="text-2xl">
|
||||||
@ -119,7 +119,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="{commonClasses} flex-row-reverse rounded-b-lg bg-gray-200 text-immich-dark-bg dark:bg-gray-700 dark:text-immich-gray sm:rounded-l-none sm:rounded-r-lg"
|
class="{commonClasses} flex-row-reverse rounded-b-lg bg-gray-200 text-immich-dark-bg dark:bg-gray-700 dark:text-immich-gray sm:rounded-s-none sm:rounded-e-lg"
|
||||||
>
|
>
|
||||||
<p class="text-2xl">
|
<p class="text-2xl">
|
||||||
{waitingCount.toLocaleString($locale)}
|
{waitingCount.toLocaleString($locale)}
|
||||||
|
@ -79,7 +79,7 @@
|
|||||||
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros(statsUsage)}</span><span
|
<span class="text-[#DCDADA] dark:text-[#525252]">{zeros(statsUsage)}</span><span
|
||||||
class="text-immich-primary dark:text-immich-dark-primary">{statsUsage}</span
|
class="text-immich-primary dark:text-immich-dark-primary">{statsUsage}</span
|
||||||
>
|
>
|
||||||
<span class="my-auto ml-2 text-center text-base font-light text-gray-400">{statsUsageUnit}</span>
|
<span class="my-auto ms-2 text-center text-base font-light text-gray-400">{statsUsageUnit}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -88,7 +88,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm dark:text-immich-dark-fg">{$t('user_usage_detail').toUpperCase()}</p>
|
<p class="text-sm dark:text-immich-dark-fg">{$t('user_usage_detail').toUpperCase()}</p>
|
||||||
<table class="mt-5 w-full text-left">
|
<table class="mt-5 w-full text-start">
|
||||||
<thead
|
<thead
|
||||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
||||||
>
|
>
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
class="text-immich-primary dark:text-immich-dark-primary">{value}</span
|
class="text-immich-primary dark:text-immich-dark-primary">{value}</span
|
||||||
>
|
>
|
||||||
{#if unit}
|
{#if unit}
|
||||||
<span class="absolute -top-5 right-2 text-base font-light text-gray-400">{unit}</span>
|
<span class="absolute -top-5 end-2 text-base font-light text-gray-400">{unit}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,13 +76,13 @@
|
|||||||
<div>
|
<div>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
|
<form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
|
||||||
<div class="ml-4 mt-4 flex flex-col">
|
<div class="ms-4 mt-4 flex flex-col">
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="oauth"
|
key="oauth"
|
||||||
title={$t('admin.oauth_settings')}
|
title={$t('admin.oauth_settings')}
|
||||||
subtitle={$t('admin.oauth_settings_description')}
|
subtitle={$t('admin.oauth_settings_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<p class="text-sm dark:text-immich-dark-fg">
|
<p class="text-sm dark:text-immich-dark-fg">
|
||||||
<FormatMessage key="admin.oauth_settings_more_details">
|
<FormatMessage key="admin.oauth_settings_more_details">
|
||||||
{#snippet children({ message })}
|
{#snippet children({ message })}
|
||||||
@ -243,8 +243,8 @@
|
|||||||
title={$t('admin.password_settings')}
|
title={$t('admin.password_settings')}
|
||||||
subtitle={$t('admin.password_settings_description')}
|
subtitle={$t('admin.password_settings_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<div class="ml-4 mt-4 flex flex-col">
|
<div class="ms-4 mt-4 flex flex-col">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title={$t('admin.password_enable_description')}
|
title={$t('admin.password_enable_description')}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" {onsubmit}>
|
<form autocomplete="off" {onsubmit}>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title={$t('admin.backup_database_enable_description')}
|
title={$t('admin.backup_database_enable_description')}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -43,7 +43,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" {onsubmit}>
|
<form autocomplete="off" {onsubmit}>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<p class="text-sm dark:text-immich-dark-fg">
|
<p class="text-sm dark:text-immich-dark-fg">
|
||||||
<Icon path={mdiHelpCircleOutline} class="inline" size="15" />
|
<Icon path={mdiHelpCircleOutline} class="inline" size="15" />
|
||||||
<FormatMessage key="admin.transcoding_codecs_learn_more">
|
<FormatMessage key="admin.transcoding_codecs_learn_more">
|
||||||
@ -70,7 +70,7 @@
|
|||||||
title={$t('admin.transcoding_policy')}
|
title={$t('admin.transcoding_policy')}
|
||||||
subtitle={$t('admin.transcoding_policy_description')}
|
subtitle={$t('admin.transcoding_policy_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label={$t('admin.transcoding_transcode_policy')}
|
label={$t('admin.transcoding_transcode_policy')}
|
||||||
{disabled}
|
{disabled}
|
||||||
@ -159,7 +159,7 @@
|
|||||||
title={$t('admin.transcoding_encoding_options')}
|
title={$t('admin.transcoding_encoding_options')}
|
||||||
subtitle={$t('admin.transcoding_encoding_options_description')}
|
subtitle={$t('admin.transcoding_encoding_options_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label={$t('admin.transcoding_video_codec')}
|
label={$t('admin.transcoding_video_codec')}
|
||||||
{disabled}
|
{disabled}
|
||||||
@ -302,7 +302,7 @@
|
|||||||
title={$t('admin.transcoding_hardware_acceleration')}
|
title={$t('admin.transcoding_hardware_acceleration')}
|
||||||
subtitle={$t('admin.transcoding_hardware_acceleration_description')}
|
subtitle={$t('admin.transcoding_hardware_acceleration_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label={$t('admin.transcoding_acceleration_api')}
|
label={$t('admin.transcoding_acceleration_api')}
|
||||||
{disabled}
|
{disabled}
|
||||||
@ -376,7 +376,7 @@
|
|||||||
title={$t('advanced')}
|
title={$t('advanced')}
|
||||||
subtitle={$t('admin.transcoding_advanced_options_description')}
|
subtitle={$t('admin.transcoding_advanced_options_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label={$t('admin.transcoding_max_b_frames')}
|
label={$t('admin.transcoding_max_b_frames')}
|
||||||
@ -407,7 +407,7 @@
|
|||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-4">
|
<div class="ms-4">
|
||||||
<SettingButtonsRow
|
<SettingButtonsRow
|
||||||
onReset={(options) => onReset({ ...options, configKeys: ['ffmpeg'] })}
|
onReset={(options) => onReset({ ...options, configKeys: ['ffmpeg'] })}
|
||||||
onSave={() => onSave({ ffmpeg: config.ffmpeg })}
|
onSave={() => onSave({ ffmpeg: config.ffmpeg })}
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" {onsubmit}>
|
<form autocomplete="off" {onsubmit}>
|
||||||
<div class="ml-4 mt-4">
|
<div class="ms-4 mt-4">
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="thumbnail-settings"
|
key="thumbnail-settings"
|
||||||
title={$t('admin.image_thumbnail_title')}
|
title={$t('admin.image_thumbnail_title')}
|
||||||
@ -195,7 +195,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-4 mt-4">
|
<div class="ms-4 mt-4">
|
||||||
<SettingButtonsRow
|
<SettingButtonsRow
|
||||||
onReset={(options) => onReset({ ...options, configKeys: ['image'] })}
|
onReset={(options) => onReset({ ...options, configKeys: ['image'] })}
|
||||||
onSave={() => onSave({ image: config.image })}
|
onSave={() => onSave({ image: config.image })}
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" {onsubmit}>
|
<form autocomplete="off" {onsubmit}>
|
||||||
{#each jobNames as jobName (jobName)}
|
{#each jobNames as jobName (jobName)}
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
{#if isSystemConfigJobDto(jobName)}
|
{#if isSystemConfigJobDto(jobName)}
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<div class="ml-4">
|
<div class="ms-4">
|
||||||
<SettingButtonsRow
|
<SettingButtonsRow
|
||||||
onReset={(options) => onReset({ ...options, configKeys: ['job'] })}
|
onReset={(options) => onReset({ ...options, configKeys: ['job'] })}
|
||||||
onSave={() => onSave({ job: config.job })}
|
onSave={() => onSave({ job: config.job })}
|
||||||
|
@ -47,14 +47,14 @@
|
|||||||
<div>
|
<div>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<div in:fade={{ duration: 500 }}>
|
||||||
<form autocomplete="off" {onsubmit}>
|
<form autocomplete="off" {onsubmit}>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
key="library-watching"
|
key="library-watching"
|
||||||
title={$t('admin.library_watching_settings')}
|
title={$t('admin.library_watching_settings')}
|
||||||
subtitle={$t('admin.library_watching_settings_description')}
|
subtitle={$t('admin.library_watching_settings_description')}
|
||||||
isOpen={openByDefault}
|
isOpen={openByDefault}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title={$t('admin.library_watching_enable_description')}
|
title={$t('admin.library_watching_enable_description')}
|
||||||
{disabled}
|
{disabled}
|
||||||
@ -69,7 +69,7 @@
|
|||||||
subtitle={$t('admin.library_scanning_description')}
|
subtitle={$t('admin.library_scanning_description')}
|
||||||
isOpen={openByDefault}
|
isOpen={openByDefault}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title={$t('admin.library_scanning_enable_description')}
|
title={$t('admin.library_scanning_enable_description')}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user