From df970da59e7d249bb4843b7f6998e6ea54ac3c37 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 23:47:14 -0400 Subject: [PATCH] chore(deps): update dependency eslint-plugin-unicorn to v70 - abandoned (#29684) * chore(deps): update dependency eslint-plugin-unicorn to v70 * fix: linting --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler --- .vscode/settings.json | 38 +++--- e2e/eslint.config.mjs | 10 +- e2e/package.json | 2 +- .../server/database-backups.e2e-spec.ts | 6 +- .../server/maintenance.e2e-spec.ts | 4 +- .../maintenance/web/maintenance.e2e-spec.ts | 2 +- e2e/src/specs/server/api/album.e2e-spec.ts | 1 + e2e/src/specs/server/api/asset.e2e-spec.ts | 6 +- e2e/src/specs/server/api/map.e2e-spec.ts | 20 +-- e2e/src/specs/server/api/oauth.e2e-spec.ts | 2 +- e2e/src/specs/server/api/search.e2e-spec.ts | 34 ++--- e2e/src/specs/server/cli/login.e2e-spec.ts | 4 +- e2e/src/specs/server/cli/upload.e2e-spec.ts | 8 +- e2e/src/ui/generators/timeline/images.ts | 2 +- .../ui/generators/timeline/model-objects.ts | 4 +- .../ui/generators/timeline/rest-response.ts | 10 +- .../ui/generators/timeline/timeline-config.ts | 2 +- e2e/src/ui/generators/timeline/utils.ts | 5 +- e2e/src/ui/mock-network/timeline-network.ts | 3 +- e2e/src/ui/specs/memory/utils.ts | 1 + e2e/src/ui/specs/timeline/utils.ts | 3 + e2e/src/utils.ts | 9 +- e2e/tsconfig.json | 1 + packages/cli/eslint.config.mjs | 8 +- packages/cli/package.json | 2 +- packages/cli/src/commands/asset.spec.ts | 2 +- packages/cli/src/commands/asset.ts | 5 +- packages/cli/src/queue.ts | 5 +- packages/cli/src/utils.ts | 16 ++- packages/cli/tsconfig.json | 1 + pnpm-lock.yaml | 50 ++++---- server/eslint.config.mjs | 20 ++- server/package.json | 2 +- server/src/app.module.ts | 1 + server/src/bin/sync-sql.ts | 7 +- server/src/commands/media-location.command.ts | 4 +- .../src/controllers/asset-media.controller.ts | 2 +- .../controllers/download.controller.spec.ts | 2 + .../src/controllers/maintenance.controller.ts | 14 ++- .../system-config.controller.spec.ts | 8 +- .../controllers/timeline.controller.spec.ts | 2 +- server/src/cores/storage.core.ts | 16 +-- server/src/decorators.ts | 5 +- server/src/dtos/asset-response.dto.ts | 2 +- server/src/dtos/library.dto.ts | 5 +- server/src/main.ts | 11 +- .../maintenance-health.repository.ts | 8 +- .../maintenance/maintenance-worker.service.ts | 8 +- server/src/repositories/access.repository.ts | 5 +- server/src/repositories/album.repository.ts | 2 +- .../src/repositories/database.repository.ts | 19 +-- server/src/repositories/event.repository.ts | 4 +- server/src/repositories/job.repository.ts | 24 ++-- .../repositories/logging.repository.spec.ts | 2 +- server/src/repositories/logging.repository.ts | 8 +- .../machine-learning.repository.ts | 6 +- server/src/repositories/map.repository.ts | 5 +- server/src/repositories/media.repository.ts | 6 +- .../src/repositories/metadata.repository.ts | 1 + server/src/repositories/oauth.repository.ts | 3 +- server/src/repositories/process.repository.ts | 14 +-- .../repositories/server-info.repository.ts | 3 +- .../src/repositories/telemetry.repository.ts | 11 +- server/src/repositories/user.repository.ts | 2 +- server/src/services/activity.service.ts | 6 +- server/src/services/album.service.ts | 6 +- server/src/services/api.service.ts | 2 +- .../src/services/asset-media.service.spec.ts | 4 +- server/src/services/asset.service.ts | 18 +-- server/src/services/auth.service.spec.ts | 6 +- server/src/services/auth.service.ts | 12 +- server/src/services/base.service.ts | 2 +- server/src/services/cli.service.ts | 19 ++- .../src/services/database-backup.service.ts | 24 ++-- server/src/services/duplicate.service.ts | 6 +- server/src/services/hls.service.spec.ts | 8 +- server/src/services/integrity.service.ts | 42 ++++--- server/src/services/library.service.ts | 94 +++++++------- server/src/services/maintenance.service.ts | 1 + server/src/services/media.service.ts | 48 +++---- server/src/services/memory.service.ts | 4 +- server/src/services/metadata.service.spec.ts | 2 +- server/src/services/metadata.service.ts | 23 ++-- server/src/services/person.service.ts | 12 +- server/src/services/queue.service.ts | 9 +- server/src/services/server.service.ts | 10 +- server/src/services/shared-link.service.ts | 3 +- server/src/services/smart-info.service.ts | 6 +- server/src/services/stack.service.ts | 2 +- .../src/services/storage-template.service.ts | 8 +- server/src/services/storage.service.ts | 14 +-- server/src/services/sync.service.ts | 18 +-- server/src/services/tag.service.ts | 16 ++- server/src/services/telemetry.service.ts | 8 +- server/src/services/timeline.service.ts | 14 +-- .../src/services/transcoding.service.spec.ts | 13 +- server/src/services/transcoding.service.ts | 8 +- server/src/services/user.service.ts | 10 +- server/src/services/version.service.ts | 8 +- .../services/workflow-execution.service.ts | 13 +- server/src/utils/access.ts | 6 +- server/src/utils/config.ts | 23 ++-- server/src/utils/database.ts | 26 ++-- server/src/utils/fetch.spec.ts | 7 +- server/src/utils/fetch.ts | 3 +- server/src/utils/file.ts | 2 +- server/src/utils/logger.ts | 2 +- server/src/utils/maintenance.ts | 14 +-- server/src/utils/media.ts | 72 +++++------ server/src/utils/mime-types.ts | 3 +- server/src/utils/misc.ts | 34 +++-- server/src/utils/preferences.ts | 2 +- server/src/utils/set.ts | 6 +- server/src/utils/transform.ts | 12 +- server/src/validation.ts | 7 +- server/test/factories/asset-exif.factory.ts | 4 +- server/test/fixtures/media.stub.ts | 6 +- server/test/fixtures/shared-link.stub.ts | 7 +- server/test/mappers.ts | 1 - server/test/medium.factory.ts | 8 +- .../repositories/asset-job.repository.spec.ts | 6 +- .../specs/services/timeline.service.spec.ts | 1 + .../specs/services/user.service.spec.ts | 4 +- .../test/medium/specs/sync/sync-types.spec.ts | 2 +- .../workflow/workflow-core-plugin.spec.ts | 14 +-- .../repositories/storage.repository.mock.ts | 2 +- server/test/small.factory.ts | 9 +- server/test/utils.ts | 8 +- server/tsconfig.json | 2 +- web/eslint.config.js | 31 ++++- web/package.json | 2 +- web/src/hooks.server.ts | 2 +- web/src/lib/__mocks__/jsdom-url.mock.ts | 1 + web/src/lib/__mocks__/sdk.mock.ts | 1 + web/src/lib/actions/click-outside.ts | 4 +- web/src/lib/actions/drag-and-drop.ts | 118 ------------------ web/src/lib/actions/focus-trap.ts | 52 ++++---- web/src/lib/actions/scroll-memory.ts | 10 +- web/src/lib/actions/use-actions.ts | 20 +-- .../lib/attachments/drag-and-drop.svelte.ts | 2 +- web/src/lib/commands.ts | 2 +- .../components/ApiKeyPermissionsPicker.svelte | 2 +- .../lib/components/AssetViewerEvents.svelte | 8 +- web/src/lib/components/Image.svelte | 12 +- web/src/lib/components/OnEvents.svelte | 8 +- .../lib/components/SchemaConfiguration.svelte | 4 +- .../StorageTemplateSettings.svelte | 9 +- .../lib/components/album-page/AlbumMap.svelte | 8 +- .../components/album-page/AlbumViewer.svelte | 8 +- .../components/album-page/AlbumsList.svelte | 18 ++- .../album-page/__tests__/AlbumCard.spec.ts | 2 +- .../asset-viewer/AlbumListItem.svelte | 2 +- .../asset-viewer/AssetViewer.svelte | 12 +- .../asset-viewer/DetailPanel.svelte | 4 +- .../asset-viewer/DetailPanelPeople.svelte | 3 +- .../asset-viewer/OcrBoundingBox.svelte | 1 + .../asset-viewer/PhotoViewer.svelte | 3 +- .../asset-viewer/SlideshowBar.svelte | 16 ++- .../SlideshowMetadataOverlay.svelte | 2 +- .../asset-viewer/VideoNativeViewer.svelte | 23 ++-- .../asset-viewer/VideoRemoteViewer.svelte | 4 +- .../transform-tool/TransformTool.svelte | 3 +- .../asset-viewer/immich-time-range.ts | 4 +- .../assets/thumbnail/Thumbnail.svelte | 5 +- .../assets/thumbnail/VideoThumbnail.svelte | 5 +- .../faces-page/PersonSidePanel.svelte | 10 +- .../maintenance/MaintenanceBackupsList.svelte | 4 +- .../ServerStatisticsCard.svelte | 2 +- .../share-page/IndividualSharedViewer.svelte | 8 +- .../shared-components/Combobox.svelte | 10 +- .../shared-components/SingleGridRow.svelte | 2 +- .../context-menu/MenuOption.svelte | 1 + .../context-menu/RightClickContextMenu.svelte | 10 +- .../gallery-viewer/GalleryViewer.svelte | 14 ++- .../shared-components/map/Map.svelte | 51 ++++---- .../search-bar/SearchBar.svelte | 8 +- .../search-bar/SearchHistoryBox.svelte | 3 +- .../settings/SettingsLanguageSelector.svelte | 10 +- .../side-bar/RecentAlbums.svelte | 2 +- .../lib/components/timeline/Scrubber.svelte | 27 ++-- .../lib/components/timeline/Timeline.svelte | 20 ++- web/src/lib/elements/FormatMessage.svelte | 8 +- .../managers/asset-viewer-manager.svelte.ts | 8 +- web/src/lib/managers/auth-manager.svelte.ts | 2 +- .../lib/managers/download-manager.svelte.ts | 2 +- .../managers/edit/transform-manager.svelte.ts | 4 + .../lib/managers/language-manager.svelte.ts | 7 +- .../media-capabilities-manager.svelte.ts | 1 + .../group-insertion-cache.svelte.ts | 4 +- .../internal/layout-support.svelte.ts | 4 +- .../timeline-manager/internal/utils.svelte.ts | 2 +- .../timeline-manager.svelte.ts | 10 +- .../timeline-manager/timeline-month.svelte.ts | 36 +++--- web/src/lib/modals/AlbumPickerModal.svelte | 1 + .../modals/GeolocationPointPickerModal.svelte | 22 ++-- .../modals/PersonMergeSuggestionModal.svelte | 1 + web/src/lib/modals/SearchFilterModal.svelte | 6 +- web/src/lib/modals/ServerAboutModal.svelte | 2 +- web/src/lib/modals/timezone-utils.ts | 2 +- web/src/lib/route.spec.ts | 1 + web/src/lib/route.ts | 6 +- .../lib/services/database-backups.service.ts | 2 +- web/src/lib/services/shared-link.service.ts | 2 +- web/src/lib/services/system-config.service.ts | 4 +- web/src/lib/services/user-admin.service.ts | 4 +- web/src/lib/services/workflow.service.ts | 5 - web/src/lib/stores/keyboard-manager.svelte.ts | 3 + web/src/lib/stores/slideshow.store.ts | 16 ++- web/src/lib/stores/upload.ts | 2 +- web/src/lib/stores/user.svelte.ts | 1 + web/src/lib/stores/websocket.ts | 13 +- web/src/lib/utils.ts | 10 +- web/src/lib/utils/actions.ts | 26 ++-- .../lib/utils/adaptive-image-loader.svelte.ts | 2 + web/src/lib/utils/asset-utils.ts | 17 +-- web/src/lib/utils/auth.ts | 8 +- web/src/lib/utils/byte-units.ts | 2 +- .../utils/cast/gcast-destination.svelte.ts | 3 +- web/src/lib/utils/date-time.ts | 26 ++-- web/src/lib/utils/duplicate-utils.ts | 6 +- web/src/lib/utils/file-uploader.ts | 2 +- web/src/lib/utils/handle-error.ts | 2 +- web/src/lib/utils/invocationTracker.ts | 5 +- web/src/lib/utils/navigation.ts | 24 ++-- web/src/lib/utils/ocr-utils.ts | 2 +- web/src/lib/utils/string-utils.ts | 2 +- web/src/lib/utils/timeline-util.ts | 4 +- web/src/lib/utils/wakelock.svelte.ts | 16 +-- .../[[assetId=id]]/+page.svelte | 18 +-- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 6 +- web/src/routes/(user)/people/+page.svelte | 2 +- .../(user)/people/PeopleInfiniteScroll.svelte | 8 +- .../[[assetId=id]]/MergeFaceSelector.svelte | 1 + .../[[assetId=id]]/PeopleList.svelte | 4 +- .../[[assetId=id]]/UnmergeFaceSelector.svelte | 9 +- .../routes/(user)/people/manage/+page.svelte | 5 +- .../routes/(user)/places/PlacesList.svelte | 5 +- .../[[assetId=id]]/+page.svelte | 6 +- .../user-settings/DownloadSettings.svelte | 2 +- .../(user)/user-settings/OauthSettings.svelte | 8 +- .../user-settings/UserSettingsList.svelte | 3 +- .../[[assetId=id]]/+page.svelte | 2 +- .../DuplicatesCompareControl.svelte | 2 +- .../(user)/utilities/geolocation/+page.svelte | 6 +- .../[[assetId=id]]/+page.svelte | 2 +- .../workflows/[workflowId]/+page.svelte | 18 +-- .../[workflowId]/WorkflowSummary.svelte | 2 +- web/src/routes/+layout.svelte | 5 +- web/src/routes/admin/maintenance/+page.svelte | 15 +-- .../admin/system-settings/AuthSettings.svelte | 36 +++--- .../system-settings/FFmpegSettings.svelte | 2 +- .../IntegrityChecksSettings.svelte | 18 +-- .../admin/system-settings/JobSettings.svelte | 4 +- .../MachineLearningSettings.svelte | 2 +- .../NightlyTasksSettings.svelte | 2 +- .../routes/admin/users/[id]/+layout.svelte | 8 +- web/src/routes/auth/login/+page.svelte | 12 +- web/src/routes/maintenance/+page.svelte | 1 + .../RestoreFlowDetectInstall.svelte | 7 +- web/src/service-worker/index.ts | 2 +- web/src/service-worker/messaging.ts | 2 +- web/src/service-worker/request.ts | 6 +- web/src/test-data/factories/asset-factory.ts | 2 +- 266 files changed, 1260 insertions(+), 1212 deletions(-) delete mode 100644 web/src/lib/actions/drag-and-drop.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 4653a1123b..30dac3216e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,9 +29,6 @@ "editor.formatOnSave": true, "tailwindCSS.lint.suggestCanonicalClasses": "ignore" }, - "svelte.plugin.svelte.compilerWarnings": { - "state_referenced_locally": "ignore" - }, "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true @@ -43,37 +40,40 @@ "eslint.useFlatConfig": true, "eslint.validate": ["javascript", "typescript", "svelte"], "eslint.workingDirectories": [ - { "directory": "cli", "changeProcessCWD": true }, - { "directory": "e2e", "changeProcessCWD": true }, - { "directory": "server", "changeProcessCWD": true }, - { "directory": "web", "changeProcessCWD": true } + { "changeProcessCWD": true, "directory": "cli" }, + { "changeProcessCWD": true, "directory": "e2e" }, + { "changeProcessCWD": true, "directory": "server" }, + { "changeProcessCWD": true, "directory": "web" } ], - "files.watcherExclude": { - "**/.jj/**": true, - "**/.git/**": true, - "**/node_modules/**": true, - "**/build/**": true, - "**/dist/**": true, - "**/.svelte-kit/**": true - }, "explorer.fileNesting.enabled": true, "explorer.fileNesting.patterns": { "*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart", - "*.ts": "${capture}.spec.ts,${capture}.mock.ts", "*.js": "${capture}.spec.js,${capture}.mock.js", + "*.ts": "${capture}.spec.ts,${capture}.mock.ts", "package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs" }, + "files.watcherExclude": { + "**/.git/**": true, + "**/.jj/**": true, + "**/.svelte-kit/**": true, + "**/build/**": true, + "**/dist/**": true, + "**/node_modules/**": true + }, + "js/ts.preferences.importModuleSpecifier": "non-relative", "search.exclude": { - "**/node_modules": true, + "**/.svelte-kit": true, "**/build": true, "**/dist": true, - "**/.svelte-kit": true, + "**/node_modules": true, "**/open-api/typescript-sdk/src": true }, "svelte.enable-ts-plugin": true, + "svelte.plugin.svelte.compilerWarnings": { + "state_referenced_locally": "ignore" + }, "tailwindCSS.experimental.configFile": { "web/src/app.css": "web/src/**" }, - "js/ts.preferences.importModuleSpecifier": "non-relative", "vitest.maximumConfigs": 10 } diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs index f73e31e48e..744af3627a 100644 --- a/e2e/eslint.config.mjs +++ b/e2e/eslint.config.mjs @@ -41,9 +41,17 @@ export default typescriptEslint.config([ '@typescript-eslint/no-floating-promises': 'error', 'unicorn/prefer-module': 'off', 'unicorn/import-style': 'off', + 'unicorn/consistent-boolean-name': 'off', + 'unicorn/no-non-function-verb-prefix': 'off', + 'unicorn/no-unreadable-for-of-expression': 'off', + 'unicorn/max-nested-calls': 'off', + 'unicorn/prefer-uint8array-base64': 'off', + 'unicorn/isolated-functions': 'off', + 'unicorn/prefer-promise-with-resolvers': 'off', + 'unicorn/no-declarations-before-early-exit': 'off', curly: 2, 'prettier/prettier': 0, - 'unicorn/prevent-abbreviations': 'off', + 'unicorn/name-replacements': 'off', 'unicorn/filename-case': 'off', 'unicorn/no-null': 'off', 'unicorn/prefer-top-level-await': 'off', diff --git a/e2e/package.json b/e2e/package.json index 3c065d194c..6d518e5a43 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -40,7 +40,7 @@ "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^64.0.0", + "eslint-plugin-unicorn": "^70.0.0", "exiftool-vendored": "^35.0.0", "globals": "^17.0.0", "luxon": "^3.4.4", diff --git a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts index e3bd98db28..cf6d752561 100644 --- a/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts +++ b/e2e/src/specs/maintenance/server/database-backups.e2e-spec.ts @@ -118,7 +118,7 @@ describe('/admin/database-backups', () => { expect(status).toBe(201); - cookie = headers['set-cookie'][0].split(';')[0]; + cookie = headers['set-cookie'][0].split(';', 1)[0]; await expect .poll( @@ -224,7 +224,7 @@ describe('/admin/database-backups', () => { }); expect(status).toBe(201); - cookie = headers['set-cookie'][0].split(';')[0]; + cookie = headers['set-cookie'][0].split(';', 1)[0]; await expect .poll( @@ -295,7 +295,7 @@ describe('/admin/database-backups', () => { }); expect(status).toBe(201); - cookie = headers['set-cookie'][0].split(';')[0]; + cookie = headers['set-cookie'][0].split(';', 1)[0]; await expect .poll( diff --git a/e2e/src/specs/maintenance/server/maintenance.e2e-spec.ts b/e2e/src/specs/maintenance/server/maintenance.e2e-spec.ts index 819a4e31f7..f3833ffc00 100644 --- a/e2e/src/specs/maintenance/server/maintenance.e2e-spec.ts +++ b/e2e/src/specs/maintenance/server/maintenance.e2e-spec.ts @@ -85,7 +85,7 @@ describe('/admin/maintenance', () => { expect(status).toBe(201); - cookie = headers['set-cookie'][0].split(';')[0]; + cookie = headers['set-cookie'][0].split(';', 1)[0]; expect(cookie).toEqual( expect.stringMatching(/^immich_maintenance_token=[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$/), ); @@ -149,7 +149,7 @@ describe('/admin/maintenance', () => { const { status, body } = await request(app) .post('/admin/maintenance/login') .send({ - token: cookie!.split('=')[1].trim(), + token: cookie!.split('=', 2)[1].trim(), }); expect(status).toBe(201); expect(body).toEqual( diff --git a/e2e/src/specs/maintenance/web/maintenance.e2e-spec.ts b/e2e/src/specs/maintenance/web/maintenance.e2e-spec.ts index 8b1631f0bf..6b58f5a425 100644 --- a/e2e/src/specs/maintenance/web/maintenance.e2e-spec.ts +++ b/e2e/src/specs/maintenance/web/maintenance.e2e-spec.ts @@ -27,7 +27,7 @@ test.describe('Maintenance', () => { test('maintenance shows no options to users until they authenticate', async ({ page }) => { const setCookie = await utils.enterMaintenance(admin.accessToken); const cookie = setCookie - ?.map((cookie) => cookie.split(';')[0].split('=')) + ?.map((cookie) => cookie.split(';', 1)[0].split('=')) ?.find(([name]) => name === 'immich_maintenance_token'); expect(cookie).toBeTruthy(); diff --git a/e2e/src/specs/server/api/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts index 432362019c..f252e5d321 100644 --- a/e2e/src/specs/server/api/album.e2e-spec.ts +++ b/e2e/src/specs/server/api/album.e2e-spec.ts @@ -120,6 +120,7 @@ describe('/albums', () => { }), ]); + // eslint-disable-next-line unicorn/no-unreadable-array-destructuring [user2Albums[0]] = await Promise.all([ getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }), deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }), diff --git a/e2e/src/specs/server/api/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts index 010b096c4d..ccebd1ca77 100644 --- a/e2e/src/specs/server/api/asset.e2e-spec.ts +++ b/e2e/src/specs/server/api/asset.e2e-spec.ts @@ -781,7 +781,7 @@ describe('/asset', () => { exifImageWidth: 4032, exifImageHeight: 3024, latitude: 41.2203, - longitude: -96.071_625, + longitude: -96.071625, make: 'Apple', model: 'iPhone 7', lensModel: 'iPhone 7 back camera 3.99mm f/1.8', @@ -973,9 +973,9 @@ describe('/asset', () => { fileSizeInByte: 31_175_472, focalLength: 18.3, iso: 100, - latitude: 36.613_24, + latitude: 36.61324, lensModel: '18.3mm F2.8', - longitude: -121.897_85, + longitude: -121.89785, make: 'RICOH IMAGING COMPANY, LTD.', model: 'RICOH GR III', orientation: '1', diff --git a/e2e/src/specs/server/api/map.e2e-spec.ts b/e2e/src/specs/server/api/map.e2e-spec.ts index 2eb363eabd..c177fa740b 100644 --- a/e2e/src/specs/server/api/map.e2e-spec.ts +++ b/e2e/src/specs/server/api/map.e2e-spec.ts @@ -75,7 +75,7 @@ describe('/map', () => { country: 'United States of America', id: expect.any(String), lat: expect.closeTo(39.115), - lon: expect.closeTo(-108.400_968), + lon: expect.closeTo(-108.400968), state: 'Colorado', }, { @@ -83,7 +83,7 @@ describe('/map', () => { country: 'United States of America', id: expect.any(String), lat: expect.closeTo(41.2203), - lon: expect.closeTo(-96.071_625), + lon: expect.closeTo(-96.071625), state: 'Nebraska', }, ]); @@ -123,7 +123,7 @@ describe('/map', () => { country: 'United States of America', id: expect.any(String), lat: expect.closeTo(39.115), - lon: expect.closeTo(-108.400_968), + lon: expect.closeTo(-108.400968), state: 'Colorado', }, { @@ -131,7 +131,7 @@ describe('/map', () => { country: 'United States of America', id: expect.any(String), lat: expect.closeTo(41.2203), - lon: expect.closeTo(-96.071_625), + lon: expect.closeTo(-96.071625), state: 'Nebraska', }, ]); @@ -188,20 +188,20 @@ describe('/map', () => { const reverseGeocodeTestCases = [ { name: 'Vaucluse', - lat: -33.858_977_058_663_13, - lon: 151.278_490_730_270_48, + lat: -33.85897705866313, + lon: 151.27849073027048, results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }], }, { name: 'Ravenhall', - lat: -37.765_732_399_174_75, - lon: 144.752_453_164_883_3, + lat: -37.76573239917475, + lon: 144.7524531648833, results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }], }, { name: 'Scarborough', - lat: -31.894_346_156_789_997, - lon: 115.757_617_103_904_64, + lat: -31.894346156789997, + lon: 115.75761710390464, results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }], }, ]; diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts index 4bf4f197b1..ade6475519 100644 --- a/e2e/src/specs/server/api/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -44,7 +44,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) => }); // login - const response1 = await redirect(url.replace(authServer.internal, authServer.external)); + const response1 = await redirect(url.replace(authServer.internal, () => authServer.external)); const response2 = await request(authServer.external + response1.location) .post('') .set('Cookie', response1.cookies) diff --git a/e2e/src/specs/server/api/search.e2e-spec.ts b/e2e/src/specs/server/api/search.e2e-spec.ts index 753a23d442..47c88c238c 100644 --- a/e2e/src/specs/server/api/search.e2e-spec.ts +++ b/e2e/src/specs/server/api/search.e2e-spec.ts @@ -87,23 +87,23 @@ describe('/search', () => { // note: the coordinates here are not the actual coordinates of the images and are random for most of them const coordinates = [ - { latitude: 48.853_41, longitude: 2.3488 }, // paris - { latitude: 35.6895, longitude: 139.691_71 }, // tokyo - { latitude: 52.524_37, longitude: 13.410_53 }, // berlin - { latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore - { latitude: 41.013_84, longitude: 28.949_66 }, // istanbul - { latitude: 5.556_02, longitude: -0.1969 }, // accra - { latitude: 37.544_270_6, longitude: -4.727_752_8 }, // andalusia - { latitude: 23.133_02, longitude: -82.383_04 }, // havana - { latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi - { latitude: 31.222_22, longitude: 121.458_06 }, // shanghai + { latitude: 48.85341, longitude: 2.3488 }, // paris + { latitude: 35.6895, longitude: 139.69171 }, // tokyo + { latitude: 52.52437, longitude: 13.41053 }, // berlin + { latitude: 1.3146631, longitude: 103.8454093 }, // singapore + { latitude: 41.01384, longitude: 28.94966 }, // istanbul + { latitude: 5.55602, longitude: -0.1969 }, // accra + { latitude: 37.5442706, longitude: -4.7277528 }, // andalusia + { latitude: 23.13302, longitude: -82.38304 }, // havana + { latitude: 41.69411, longitude: 44.83368 }, // tbilisi + { latitude: 31.22222, longitude: 121.45806 }, // shanghai { latitude: 38.9711, longitude: -109.7137 }, // thompson springs - { latitude: 40.714_27, longitude: -74.005_97 }, // new york - { latitude: 47.040_57, longitude: 9.068_04 }, // glarus - { latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia - { latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh - { latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge - { latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg + { latitude: 40.71427, longitude: -74.00597 }, // new york + { latitude: 47.04057, longitude: 9.06804 }, // glarus + { latitude: 32.77152, longitude: -89.11673 }, // philadelphia + { latitude: 31.63416, longitude: -7.99994 }, // marrakesh + { latitude: 38.5237354, longitude: -78.4886194 }, // tanners ridge + { latitude: 59.93863, longitude: 30.31413 }, // st. petersburg { latitude: 0, longitude: 0 }, // null island ]; @@ -112,7 +112,7 @@ describe('/search', () => { ); await Promise.all(updates); - for (const [i] of coordinates.entries()) { + for (const i of coordinates.keys()) { await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: assets[i].id }); } diff --git a/e2e/src/specs/server/cli/login.e2e-spec.ts b/e2e/src/specs/server/cli/login.e2e-spec.ts index 3bc3ebc9c2..caf5550b6e 100644 --- a/e2e/src/specs/server/cli/login.e2e-spec.ts +++ b/e2e/src/specs/server/cli/login.e2e-spec.ts @@ -31,7 +31,7 @@ describe(`immich login`, () => { it('should login and save auth.yml with 600', async () => { const admin = await utils.adminSetup(); const key = await utils.createApiKey(admin.accessToken, [Permission.All]); - const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]); + const { stdout, stderr, exitCode } = await immichCli(['login', app, key.secret]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2285/api', 'Logged in as admin@immich.cloud', @@ -48,7 +48,7 @@ describe(`immich login`, () => { it('should login without /api in the url', async () => { const admin = await utils.adminSetup(); const key = await utils.createApiKey(admin.accessToken, [Permission.All]); - const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]); + const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), key.secret]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2285', 'Discovered API at http://127.0.0.1:2285/api', diff --git a/e2e/src/specs/server/cli/upload.e2e-spec.ts b/e2e/src/specs/server/cli/upload.e2e-spec.ts index b53b4403f8..5df36746d7 100644 --- a/e2e/src/specs/server/cli/upload.e2e-spec.ts +++ b/e2e/src/specs/server/cli/upload.e2e-spec.ts @@ -119,7 +119,9 @@ describe(`immich upload`, () => { const baseDir = `/tmp/upload/`; const testPaths = Object.keys(files).map((filePath) => `${baseDir}/${filePath}`); - testPaths.map((filePath) => utils.createImageFile(filePath)); + for (const filePath of testPaths) { + utils.createImageFile(filePath); + } const commandLine = paths.map((argument) => `${baseDir}/${argument}`); @@ -135,7 +137,9 @@ describe(`immich upload`, () => { const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) }); expect(assets.total).toBe(expectedCount); - testPaths.map((filePath) => utils.removeImageFile(filePath)); + for (const filePath of testPaths) { + utils.removeImageFile(filePath); + } }); } }); diff --git a/e2e/src/ui/generators/timeline/images.ts b/e2e/src/ui/generators/timeline/images.ts index 9330cf137d..2eb141c557 100644 --- a/e2e/src/ui/generators/timeline/images.ts +++ b/e2e/src/ui/generators/timeline/images.ts @@ -21,7 +21,7 @@ export const randomImageFromString = async ( let seedNumber = 0; for (let i = 0; i < seed.length; i++) { seedNumber = (seedNumber << 5) - seedNumber + (seed.codePointAt(i) ?? 0); - seedNumber = seedNumber & seedNumber; // Convert to 32bit integer + seedNumber &= seedNumber; // Convert to 32bit integer } return randomImage(new SeededRandom(Math.abs(seedNumber)), { width, height }); }; diff --git a/e2e/src/ui/generators/timeline/model-objects.ts b/e2e/src/ui/generators/timeline/model-objects.ts index f5654afd5e..094e5133a3 100644 --- a/e2e/src/ui/generators/timeline/model-objects.ts +++ b/e2e/src/ui/generators/timeline/model-objects.ts @@ -64,7 +64,7 @@ export function generateAsset( const asset: MockTimelineAsset = { id: assetId, ownerId, - ratio: Number.parseFloat(ratio.split(':')[0]) / Number.parseFloat(ratio.split(':')[1]), + ratio: Number(ratio.split(':', 1)[0]) / Number(ratio.split(':', 2)[1]), thumbhash: generateThumbhash(rng), localDateTime: date.toISOString(), fileCreatedAt: date.toISOString(), @@ -214,7 +214,7 @@ export function generateTimelineData(config: TimelineConfig): MockTimelineData { } // Create a mock album from random assets - const allAssets = [...buckets.values()].flat(); + const allAssets = buckets.values().toArray().flat(); // Select 10-30 random assets for the album (or all assets if less than 10) const albumSize = Math.min(allAssets.length, globalRng.nextInt(10, 31)); diff --git a/e2e/src/ui/generators/timeline/rest-response.ts b/e2e/src/ui/generators/timeline/rest-response.ts index 553ce18005..d7cf7adf0a 100644 --- a/e2e/src/ui/generators/timeline/rest-response.ts +++ b/e2e/src/ui/generators/timeline/rest-response.ts @@ -172,11 +172,7 @@ function shouldIncludeAsset( if (isArchived !== undefined && actuallyArchived !== isArchived) { return false; } - if (isFavorite !== undefined && actuallyFavorited !== isFavorite) { - return false; - } - - return true; + return isFavorite === undefined || actuallyFavorited === isFavorite; } /** * Get summary for all buckets (mimics getTimeBuckets API) @@ -361,7 +357,7 @@ export function getAsset( owner?: UserResponseDto, ): AssetResponseDto | undefined { // Search through all buckets for the asset - const buckets = [...timelineData.buckets.values()]; + const buckets = timelineData.buckets.values().toArray(); for (const assets of buckets) { const asset = assets.find((a) => a.id === assetId); if (asset) { @@ -395,7 +391,7 @@ export function getAlbum( // Get the actual asset objects from the timeline data const albumAssets: AssetResponseDto[] = []; - const allAssets = [...timelineData.buckets.values()].flat(); + const allAssets = timelineData.buckets.values().toArray().flat(); for (const assetId of album.assetIds) { const assetConfig = allAssets.find((a) => a.id === assetId); diff --git a/e2e/src/ui/generators/timeline/timeline-config.ts b/e2e/src/ui/generators/timeline/timeline-config.ts index 4dea2f4f78..a3b59bb8c8 100644 --- a/e2e/src/ui/generators/timeline/timeline-config.ts +++ b/e2e/src/ui/generators/timeline/timeline-config.ts @@ -143,7 +143,7 @@ export function validateTimelineConfig(config: TimelineConfig): void { } // Validate seed if provided - if (config.seed !== undefined && (config.seed < 0 || !Number.isInteger(config.seed))) { + if (config.seed !== undefined && (config.seed < 0 || !Number.isSafeInteger(config.seed))) { throw new Error('Seed must be a non-negative integer'); } diff --git a/e2e/src/ui/generators/timeline/utils.ts b/e2e/src/ui/generators/timeline/utils.ts index 283f56c6f0..fbb9630a3c 100644 --- a/e2e/src/ui/generators/timeline/utils.ts +++ b/e2e/src/ui/generators/timeline/utils.ts @@ -153,11 +153,8 @@ export function getMockAsset( const isInDifferentPeriod = (date1: DateTime, date2: DateTime): boolean => { if (unit === 'day') { return !date1.startOf('day').equals(date2.startOf('day')); - } else if (unit === 'month') { - return date1.year !== date2.year || date1.month !== date2.month; - } else { - return date1.year !== date2.year; } + return unit === 'month' ? date1.year !== date2.year || date1.month !== date2.month : date1.year !== date2.year; }; if (direction === 'next') { diff --git a/e2e/src/ui/mock-network/timeline-network.ts b/e2e/src/ui/mock-network/timeline-network.ts index 6af2ebb7c1..1a745b294a 100644 --- a/e2e/src/ui/mock-network/timeline-network.ts +++ b/e2e/src/ui/mock-network/timeline-network.ts @@ -40,7 +40,8 @@ export const setupTimelineMockApiRoutes = async ( contentType: 'application/json', json: getTimeBuckets(timelineRestData, isTrashed, isArchived, isFavorite, albumId, changes), }); - } else if (pathname === '/api/timeline/bucket') { + } + if (pathname === '/api/timeline/bucket') { const timeBucket = url.searchParams.get('timeBucket'); if (!timeBucket) { return route.continue(); diff --git a/e2e/src/ui/specs/memory/utils.ts b/e2e/src/ui/specs/memory/utils.ts index cf99033e7e..378f649cff 100644 --- a/e2e/src/ui/specs/memory/utils.ts +++ b/e2e/src/ui/specs/memory/utils.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-this-outside-of-class */ import type { AssetResponseDto } from '@immich/sdk'; import { expect, Page } from '@playwright/test'; diff --git a/e2e/src/ui/specs/timeline/utils.ts b/e2e/src/ui/specs/timeline/utils.ts index e67229d3c9..e6af7fe3b5 100644 --- a/e2e/src/ui/specs/timeline/utils.ts +++ b/e2e/src/ui/specs/timeline/utils.ts @@ -72,6 +72,7 @@ export const thumbnailUtils = { }, async queryThumbnailInViewport(page: Page, collector: (assetId: string) => boolean) { const assetIds: string[] = []; + // eslint-disable-next-line unicorn/no-this-outside-of-class for (const thumb of await this.locator(page).all()) { const box = await thumb.boundingBox(); if (box) { @@ -151,6 +152,7 @@ export const timelineUtils = { page.evaluate(() => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore + return document.querySelector('#asset-grid').scrollTop; }); await expect.poll(queryTop).toBeGreaterThan(0); @@ -177,6 +179,7 @@ export const assetViewerUtils = { page.evaluate(() => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore + // eslint-disable-next-line unicorn/no-optional-chaining-on-undeclared-variable return document.activeElement?.dataset?.asset; }); await expect(poll(page, activeElement, (result) => result === assetId)).resolves.toBe(assetId); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 23b3b14306..3124dd0609 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-top-level-assignment-in-function */ import { AssetMediaCreateDto, AssetMediaResponseDto, @@ -177,7 +178,7 @@ export const utils = { resetDatabase: async (tables?: string[]) => { client = await utils.connectDatabase(); - tables = tables || [ + tables ||= [ // TODO e2e test for deleting a stack, since it is quite complex 'stack', 'library', @@ -304,7 +305,7 @@ export const utils = { }, adminSetup: async (options?: AdminSetupOptions) => { - options = options || { onboarding: true }; + options ||= { onboarding: true }; await signUpAdmin({ signUpDto: signupDto.admin }); const response = await login({ loginCredentialDto: loginDto.admin }); @@ -545,6 +546,7 @@ export const utils = { { headers: asBearerAuth(accessToken), fetch: (...args: Parameters) => + // eslint-disable-next-line unicorn/no-invalid-argument-count, unicorn/prefer-await fetch(...args).then((response) => { setCookie = response.headers.getSetCookie(); return response; @@ -674,7 +676,7 @@ export const utils = { cliLogin: async (accessToken: string) => { const key = await utils.createApiKey(accessToken, [Permission.All]); - await immichCli(['login', app, `${key.secret}`]); + await immichCli(['login', app, key.secret]); return key.secret; }, @@ -706,6 +708,7 @@ export const utils = { }, }; +// eslint-disable-next-line unicorn/no-top-level-side-effects utils.initSdk(); if (!existsSync(`${testAssetDir}/albums`)) { diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json index 61eefdac07..eebe91e90c 100644 --- a/e2e/tsconfig.json +++ b/e2e/tsconfig.json @@ -10,6 +10,7 @@ "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "target": "es2023", + "lib": ["dom", "ESNext"], "sourceMap": true, "outDir": "./dist", "incremental": true, diff --git a/packages/cli/eslint.config.mjs b/packages/cli/eslint.config.mjs index 101d91bea4..83e356320f 100644 --- a/packages/cli/eslint.config.mjs +++ b/packages/cli/eslint.config.mjs @@ -40,10 +40,16 @@ export default typescriptEslint.config([ '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'error', 'unicorn/prefer-module': 'off', - 'unicorn/prevent-abbreviations': 'off', + 'unicorn/name-replacements': 'off', + 'unicorn/no-unreadable-for-of-expression': 'off', + 'unicorn/no-declarations-before-early-exit': 'off', 'unicorn/no-process-exit': 'off', 'unicorn/import-style': 'off', + 'unicorn/consistent-class-member-order': 'off', curly: 2, + // prefer the typescript-eslint type-aware version + 'unicorn/require-array-sort-compare': 'off', + '@typescript-eslint/require-array-sort-compare': 'error', 'prettier/prettier': 0, 'object-shorthand': ['error', 'always'], }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 61dc7d4b59..3d8bdbb0fb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -33,7 +33,7 @@ "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^64.0.0", + "eslint-plugin-unicorn": "^70.0.0", "globals": "^17.0.0", "mock-fs": "^5.2.0", "prettier": "^3.7.4", diff --git a/packages/cli/src/commands/asset.spec.ts b/packages/cli/src/commands/asset.spec.ts index 4959e43642..7e582039ba 100644 --- a/packages/cli/src/commands/asset.spec.ts +++ b/packages/cli/src/commands/asset.spec.ts @@ -46,7 +46,7 @@ describe('uploadFiles', () => { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-')); const testFilePath = path.join(testDir, 'test.png'); const testFileData = 'test'; - const baseUrl = 'http://example.com'; + const baseUrl = 'https://example.com'; const apiKey = 'key'; const retry = 3; diff --git a/packages/cli/src/commands/asset.ts b/packages/cli/src/commands/asset.ts index 1c3d0163a8..f1dfdaec8e 100644 --- a/packages/cli/src/commands/asset.ts +++ b/packages/cli/src/commands/asset.ts @@ -56,6 +56,7 @@ class UploadFile extends File { super([], basename(filepath)); } + // @ts-expect-error size is already a property on the new File interface get size() { return this._size; } @@ -440,7 +441,7 @@ const uploadFile = async ( throw new Error(await response.text()); } - return response.json(); + return response.json() as Promise; }; export const findSidecar = (filepath: string): string | undefined => { @@ -577,7 +578,7 @@ const updateAlbums = async (assets: Asset[], options: UploadOptionsDto) => { albumUpdateProgress.start(assets.length, 0); try { - for (const [albumId, assets] of albumToAssets.entries()) { + for (const [albumId, assets] of albumToAssets) { for (const assetBatch of chunk(assets, Math.min(1000 * concurrency, 65_000))) { await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } }); albumUpdateProgress.increment(assetBatch.length); diff --git a/packages/cli/src/queue.ts b/packages/cli/src/queue.ts index 0b6d628146..45e1c454f7 100644 --- a/packages/cli/src/queue.ts +++ b/packages/cli/src/queue.ts @@ -52,10 +52,7 @@ export class Queue { } get tasks(): Task[] { - const tasks: Task[] = []; - for (const task of this.store.values()) { - tasks.push(task); - } + const tasks: Task[] = this.store.values().toArray(); return tasks; } diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts index b60f5e3715..d8a567a230 100644 --- a/packages/cli/src/utils.ts +++ b/packages/cli/src/utils.ts @@ -39,6 +39,7 @@ export const s = (count: number) => (count === 1 ? '' : 's'); let _apiKey: ApiKeyResponseDto; export const requirePermissions = async (permissions: Permission[]) => { if (!_apiKey) { + // eslint-disable-next-line unicorn/no-top-level-assignment-in-function _apiKey = await getMyApiKey(); } @@ -67,8 +68,9 @@ Please make sure your API key has the correct permissions.`, export const connect = async (url: string, key: string) => { const wellKnownUrl = new URL('.well-known/immich', url); try { - const wellKnown = await fetch(wellKnownUrl).then((response) => response.json()); - const endpoint = new URL(wellKnown.api.endpoint, url).toString(); + // eslint-disable-next-line unicorn/prefer-await + const wellKnown = (await fetch(wellKnownUrl).then((response) => response.json())) as { api: { endpoint: string } }; + const endpoint = new URL(wellKnown.api.endpoint, url).href; if (endpoint !== url) { console.debug(`Discovered API at ${endpoint}`); } @@ -178,7 +180,7 @@ export const crawl = async (options: CrawlOptions): Promise => { const searchPatterns = patterns.map((pattern) => { let escapedPattern = pattern.replaceAll("'", "[']").replaceAll('"', '["]').replaceAll('`', '[`]'); if (recursive) { - escapedPattern = escapedPattern + '/**'; + escapedPattern += '/**'; } return `${escapedPattern}/*.{${extensions.join(',')}}`; }); @@ -238,10 +240,12 @@ export class Batcher { } private clearDebounceTimer() { - if (this.debounceTimer) { - clearTimeout(this.debounceTimer); - this.debounceTimer = undefined; + if (!this.debounceTimer) { + return; } + + clearTimeout(this.debounceTimer); + this.debounceTimer = undefined; } add(item: T) { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index f2d8d61a5f..26331e32f5 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -10,6 +10,7 @@ "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "target": "es2023", + "lib": ["ESNext"], "sourceMap": true, "outDir": "./dist", "incremental": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e49f7cc8c5..0c2df5de2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -151,8 +151,8 @@ importers: specifier: ^5.1.3 version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) eslint-plugin-unicorn: - specifier: ^64.0.0 - version: 64.0.0(eslint@10.7.0(jiti@2.7.0)) + specifier: ^70.0.0 + version: 70.0.0(eslint@10.7.0(jiti@2.7.0)) exiftool-vendored: specifier: ^35.0.0 version: 35.21.0 @@ -260,8 +260,8 @@ importers: specifier: ^5.1.3 version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) eslint-plugin-unicorn: - specifier: ^64.0.0 - version: 64.0.0(eslint@10.7.0(jiti@2.7.0)) + specifier: ^70.0.0 + version: 70.0.0(eslint@10.7.0(jiti@2.7.0)) globals: specifier: ^17.0.0 version: 17.7.0 @@ -729,8 +729,8 @@ importers: specifier: ^5.1.3 version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.7.0(jiti@2.7.0)))(eslint@10.7.0(jiti@2.7.0))(prettier@3.9.5) eslint-plugin-unicorn: - specifier: ^64.0.0 - version: 64.0.0(eslint@10.7.0(jiti@2.7.0)) + specifier: ^70.0.0 + version: 70.0.0(eslint@10.7.0(jiti@2.7.0)) globals: specifier: ^17.0.0 version: 17.7.0 @@ -991,8 +991,8 @@ importers: specifier: ^3.12.4 version: 3.20.0(eslint@10.7.0(jiti@2.7.0))(svelte@5.56.4(@typescript-eslint/types@8.63.0)) eslint-plugin-unicorn: - specifier: ^64.0.0 - version: 64.0.0(eslint@10.7.0(jiti@2.7.0)) + specifier: ^70.0.0 + version: 70.0.0(eslint@10.7.0(jiti@2.7.0)) factory.ts: specifier: ^1.4.1 version: 1.4.2 @@ -6346,10 +6346,6 @@ packages: resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} engines: {node: '>= 10.0'} - clean-regexp@1.0.0: - resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} - engines: {node: '>=4'} - clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -7117,6 +7113,10 @@ packages: detect-europe-js@0.1.2: resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==} + detect-indent@7.0.2: + resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==} + engines: {node: '>=12.20'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -7467,11 +7467,11 @@ packages: svelte: optional: true - eslint-plugin-unicorn@64.0.0: - resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==} - engines: {node: ^20.10.0 || >=21.0.0} + eslint-plugin-unicorn@70.0.0: + resolution: {integrity: sha512-uAF9xMcVvvhTfvusCgogJ1wh4To3q2KhVMw3i1Apf/ILTbxsCjscvraAZACsEurb7no2fdXblD3whcbVnjw5zg==} + engines: {node: '>=22'} peerDependencies: - eslint: '>=9.38.0' + eslint: '>=10.4' eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} @@ -10897,10 +10897,6 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - regexp-tree@0.1.27: - resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true - regexpu-core@6.4.0: resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} @@ -19247,10 +19243,6 @@ snapshots: dependencies: source-map: 0.6.1 - clean-regexp@1.0.0: - dependencies: - escape-string-regexp: 1.0.5 - clean-stack@2.2.0: {} cli-boxes@3.0.0: {} @@ -20007,6 +19999,8 @@ snapshots: detect-europe-js@0.1.2: {} + detect-indent@7.0.2: {} + detect-libc@2.1.2: {} detect-node@2.1.0: {} @@ -20466,14 +20460,15 @@ snapshots: transitivePeerDependencies: - ts-node - eslint-plugin-unicorn@64.0.0(eslint@10.7.0(jiti@2.7.0)): + eslint-plugin-unicorn@70.0.0(eslint@10.7.0(jiti@2.7.0)): dependencies: '@babel/helper-validator-identifier': 7.29.7 '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(jiti@2.7.0)) + browserslist: 4.28.6 change-case: 5.4.4 ci-info: 4.4.0 - clean-regexp: 1.0.0 core-js-compat: 3.49.0 + detect-indent: 7.0.2 eslint: 10.7.0(jiti@2.7.0) find-up-simple: 1.0.1 globals: 17.7.0 @@ -20481,7 +20476,6 @@ snapshots: is-builtin-module: 5.0.0 jsesc: 3.1.0 pluralize: 8.0.0 - regexp-tree: 0.1.27 regjsparser: 0.13.2 semver: 7.8.5 strip-indent: 4.1.1 @@ -24468,8 +24462,6 @@ snapshots: regenerate@1.4.2: {} - regexp-tree@0.1.27: {} - regexpu-core@6.4.0: dependencies: regenerate: 1.4.2 diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs index c86bc3d72f..579993aec5 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -39,7 +39,7 @@ export default typescriptEslint.config([ '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'error', - 'unicorn/prevent-abbreviations': 'off', + 'unicorn/name-replacements': 'off', 'unicorn/filename-case': 'off', 'unicorn/no-null': 'off', 'unicorn/prefer-top-level-await': 'off', @@ -49,6 +49,24 @@ export default typescriptEslint.config([ 'unicorn/prefer-structured-clone': 'off', 'unicorn/no-for-loop': 'off', 'unicorn/no-array-sort': 'off', + 'unicorn/no-unreadable-for-of-expression': 'off', + 'unicorn/no-break-in-nested-loop': 'off', + 'unicorn/no-top-level-assignment-in-function': 'off', + 'unicorn/prefer-uint8array-base64': 'off', + 'unicorn/max-nested-calls': 'off', + 'unicorn/no-declarations-before-early-exit': 'off', + 'unicorn/no-unreadable-object-destructuring': 'off', + // maybe we do want to enable this later. TBD + 'unicorn/prefer-await': 'off', + 'unicorn/consistent-class-member-order': 'off', + 'unicorn/class-reference-in-static-methods': ['error', { preferThis: false, preferSuper: false }], + 'unicorn/no-unsafe-property-key': 'off', + 'unicorn/consistent-boolean-name': 'off', + 'unicorn/no-computed-property-existence-check': 'off', + 'unicorn/no-non-function-verb-prefix': 'off', + // prefer the typescript-eslint type-aware version + 'unicorn/require-array-sort-compare': 'off', + '@typescript-eslint/require-array-sort-compare': 'error', '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }], diff --git a/server/package.json b/server/package.json index 7bc41e21f7..ccd9dc7eae 100644 --- a/server/package.json +++ b/server/package.json @@ -151,7 +151,7 @@ "eslint": "^10.0.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^64.0.0", + "eslint-plugin-unicorn": "^70.0.0", "globals": "^17.0.0", "mock-fs": "^5.2.0", "pngjs": "^7.0.0", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index ae930762d0..a4e8ac27c6 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -63,6 +63,7 @@ const commonImports = [ const bullImports = [BullModule.forRoot(bull.config), BullModule.registerQueue(...bull.queues)]; +// eslint-disable-next-line unicorn/no-top-level-side-effects configureUserAgent(); export class BaseModule implements OnModuleInit, OnModuleDestroy { diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index 5be9ae29b9..528941e0b2 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -23,6 +23,7 @@ const handleError = (label: string, error: Error | any) => { console.error(`${label} error: ${error}`); }; +// eslint-disable-next-line unicorn/no-exports-in-scripts export class SqlLogger { queries: string[] = []; errors: Array<{ error: string | Error; query: string }> = []; @@ -109,10 +110,12 @@ class SqlGenerator { const instance = this.app.get(Repository); // normal repositories - data.push(...(await this.runTargets(instance, `${Repository.name}`))); + data.push(...(await this.runTargets(instance, Repository.name))); // nested repositories if (Repository.name === AccessRepository.name || Repository.name === SyncRepository.name) { + // probably a bug that this fails linting? + // eslint-disable-next-line unicorn/prefer-object-iterable-methods for (const key of Object.keys(instance)) { const subInstance = (instance as any)[key]; data.push(...(await this.runTargets(subInstance, `${Repository.name}.${key}`))); @@ -127,7 +130,7 @@ class SqlGenerator { for (const key of this.getPropertyNames(instance)) { const target = instance[key]; - if (!(typeof target === 'function')) { + if (typeof target !== 'function') { continue; } diff --git a/server/src/commands/media-location.command.ts b/server/src/commands/media-location.command.ts index 0d32749c02..5a4e9bbe33 100644 --- a/server/src/commands/media-location.command.ts +++ b/server/src/commands/media-location.command.ts @@ -36,7 +36,7 @@ export class ChangeMediaLocationCommand extends CommandRunner { {}, ); - const success = await this.service.migrateFilePaths({ + const isSuccess = await this.service.migrateFilePaths({ oldValue, newValue, confirm: async ({ sourceFolder, targetFolder }) => { @@ -65,7 +65,7 @@ export class ChangeMediaLocationCommand extends CommandRunner { ... )`; - console.log(`\n ${success ? successMessage : 'No rows were updated'}\n`); + console.log(`\n ${isSuccess ? successMessage : 'No rows were updated'}\n`); await this.showSamplePaths('after'); } catch (error) { diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index 240e5b8b3c..e551fc840f 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -131,7 +131,7 @@ export class AssetMediaController { const [_, reqSearch] = req.url.split('?'); const redirSearchParams = new URLSearchParams(reqSearch); redirSearchParams.delete('size'); - return res.redirect('original' + '?' + redirSearchParams.toString()); + return res.redirect('original?' + redirSearchParams.toString()); } const viewThumbnailRes = await this.service.viewThumbnail(auth, id, dto); diff --git a/server/src/controllers/download.controller.spec.ts b/server/src/controllers/download.controller.spec.ts index 00d03fc46f..f42afe8c50 100644 --- a/server/src/controllers/download.controller.spec.ts +++ b/server/src/controllers/download.controller.spec.ts @@ -32,7 +32,9 @@ describe(DownloadController.name, () => { it('should be an authenticated route', async () => { const stream = new Readable({ read() { + // eslint-disable-next-line unicorn/no-this-outside-of-class this.push('test'); + // eslint-disable-next-line unicorn/no-this-outside-of-class this.push(null); }, }); diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index 169fec7890..d5f13e341c 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -65,12 +65,14 @@ export class MaintenanceController { @GetLoginDetails() loginDetails: LoginDetails, @Res({ passthrough: true }) res: Response, ): Promise { - if (dto.action !== MaintenanceAction.End) { - const { jwt } = await this.service.startMaintenance(dto, auth.user.name); - return respondWithCookie(res, undefined, { - isSecure: loginDetails.isSecure, - values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }], - }); + if (dto.action === MaintenanceAction.End) { + return; } + + const { jwt } = await this.service.startMaintenance(dto, auth.user.name); + return respondWithCookie(res, undefined, { + isSecure: loginDetails.isSecure, + values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }], + }); } } diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts index 1770bdd89e..439b5ddec1 100644 --- a/server/src/controllers/system-config.controller.spec.ts +++ b/server/src/controllers/system-config.controller.spec.ts @@ -14,10 +14,10 @@ function validConfig() { notifications: { smtp: { from: string; transport: { host: string } } }; server: { externalDomain: string }; }; - config.oauth.mobileRedirectUri = config.oauth.mobileRedirectUri || 'https://example.com'; - config.server.externalDomain = config.server.externalDomain || 'https://example.com'; - config.notifications.smtp.from = config.notifications.smtp.from || 'noreply@example.com'; - config.notifications.smtp.transport.host = config.notifications.smtp.transport.host || 'localhost'; + config.oauth.mobileRedirectUri ||= 'https://example.com'; + config.server.externalDomain ||= 'https://example.com'; + config.notifications.smtp.from ||= 'noreply@example.com'; + config.notifications.smtp.transport.host ||= 'localhost'; return config; } diff --git a/server/src/controllers/timeline.controller.spec.ts b/server/src/controllers/timeline.controller.spec.ts index b07eb5a78c..ff18c020c1 100644 --- a/server/src/controllers/timeline.controller.spec.ts +++ b/server/src/controllers/timeline.controller.spec.ts @@ -33,7 +33,7 @@ describe(TimelineController.name, () => { expect(service.getTimeBuckets).toHaveBeenCalledWith( undefined, expect.objectContaining({ - bbox: { west: 11.075_683, south: 49.416_711, east: 11.117_589, north: 49.454_875 }, + bbox: { west: 11.075683, south: 49.416711, east: 11.117589, north: 49.454875 }, }), ); }); diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 6c7d6f0e3e..185ab95e62 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -202,20 +202,20 @@ export class StorageCore { let move = await this.moveRepository.getByEntity(entityId, pathType); if (move) { this.logger.log(`Attempting to finish incomplete move: ${move.oldPath} => ${move.newPath}`); - const oldPathExists = await this.storageRepository.checkFileExists(move.oldPath); - const newPathExists = await this.storageRepository.checkFileExists(move.newPath); - const newPathCheck = newPathExists ? move.newPath : null; - const actualPath = oldPathExists ? move.oldPath : newPathCheck; + const isOldPathExists = await this.storageRepository.checkFileExists(move.oldPath); + const isNewPathExists = await this.storageRepository.checkFileExists(move.newPath); + const newPathCheck = isNewPathExists ? move.newPath : null; + const actualPath = isOldPathExists ? move.oldPath : newPathCheck; if (!actualPath) { this.logger.warn('Unable to complete move. File does not exist at either location.'); return; } - const fileAtNewLocation = actualPath === move.newPath; - this.logger.log(`Found file at ${fileAtNewLocation ? 'new' : 'old'} location`); + const isFileAtNewLocation = actualPath === move.newPath; + this.logger.log(`Found file at ${isFileAtNewLocation ? 'new' : 'old'} location`); if ( - fileAtNewLocation && + isFileAtNewLocation && !(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo)) ) { this.logger.fatal( @@ -349,7 +349,7 @@ export class StorageCore { } static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string { - return join(this.getNestedFolder(folder, ownerId, filename), filename); + return join(StorageCore.getNestedFolder(folder, ownerId, filename), filename); } static getTempPathInDir(dir: string): string { diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 67e453bbbd..f89de14610 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -54,9 +54,8 @@ function chunks(collection: Array | Set, size: number): Array> result.push(chunk); } return result; - } else { - return _.chunk(collection, size); } + return _.chunk(collection, size); } /** @@ -82,11 +81,13 @@ export function Chunked( (Array.isArray(argument) && argument.length <= chunkSize) || (argument instanceof Set && argument.size <= chunkSize) ) { + // eslint-disable-next-line unicorn/no-this-outside-of-class return originalMethod.apply(this, arguments_); } return Promise.all( chunks(argument, chunkSize).map((chunk) => { + // eslint-disable-next-line unicorn/no-this-outside-of-class return Reflect.apply(originalMethod, this, [ ...arguments_.slice(0, parameterIndex), chunk, diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index ba64286647..c54ce1cbab 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -175,7 +175,7 @@ const peopleFromFaces = (faces?: MaybeDehydrated[]): PersonResponseDt } } - return [...peopleMap.values()]; + return peopleMap.values().toArray(); }; const mapStack = (entity: { stack?: Stack | null }) => { diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index 0819be278e..906a6e6635 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -92,10 +92,7 @@ export class LibraryResponseDto extends createZodDto(LibraryResponseSchema) {} export class LibraryStatsResponseDto extends createZodDto(LibraryStatsResponseSchema) {} export function mapLibrary(entity: Library): LibraryResponseDto { - let assetCount = 0; - if (entity.assets) { - assetCount = entity.assets.length; - } + const assetCount = entity.assets ? entity.assets.length : 0; return { id: entity.id, ownerId: entity.ownerId, diff --git a/server/src/main.ts b/server/src/main.ts index f2491f07bc..5876d2fe58 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -68,9 +68,9 @@ class Workers { const { database } = new ConfigRepository().getEnv(); const kysely = new Kysely(getKyselyConfig(database.config)); - let locked = false; - while (!locked) { - locked = await kysely.connection().execute(async (conn) => { + let isLocked = false; + while (!isLocked) { + isLocked = await kysely.connection().execute(async (conn) => { const { rows } = await sql<{ pg_try_advisory_lock: boolean; }>`SELECT pg_try_advisory_lock(${DatabaseLock.MaintenanceOperation})`.execute(conn); @@ -110,6 +110,7 @@ class Workers { }); kill = (signal) => void worker.kill(signal); + // eslint-disable-next-line unicorn/prefer-hoisting-branch-code anyWorker = worker; } else { const worker = new Worker(workerFile); @@ -151,9 +152,9 @@ class Workers { if (exitCode !== 0) { console.error(`${name} worker exited with code ${exitCode}`); - if (this.workers[ImmichWorker.Api] && name !== ImmichWorker.Api) { + if (Object.hasOwn(this.workers, ImmichWorker.Api) && name !== ImmichWorker.Api) { console.error('Killing api process'); - void this.workers[ImmichWorker.Api].kill('SIGTERM'); + void this.workers[ImmichWorker.Api]!.kill('SIGTERM'); } } diff --git a/server/src/maintenance/maintenance-health.repository.ts b/server/src/maintenance/maintenance-health.repository.ts index c76cc23b1c..9253892041 100644 --- a/server/src/maintenance/maintenance-health.repository.ts +++ b/server/src/maintenance/maintenance-health.repository.ts @@ -42,10 +42,12 @@ export class MaintenanceHealthRepository { worker.on('error', (error) => reject(new Error(`Server health check failed, process threw: ${error}`))); setTimeout(() => { - if (worker.exitCode === null) { - reject(new Error('Server health check failed, took too long to start.')); - worker.kill('SIGTERM'); + if (worker.exitCode !== null) { + return; } + + reject(new Error('Server health check failed, took too long to start.')); + worker.kill('SIGTERM'); }, 180_000); }); } diff --git a/server/src/maintenance/maintenance-worker.service.ts b/server/src/maintenance/maintenance-worker.service.ts index f2c9086e87..f088637a57 100644 --- a/server/src/maintenance/maintenance-worker.service.ts +++ b/server/src/maintenance/maintenance-worker.service.ts @@ -171,8 +171,8 @@ export class MaintenanceWorkerService { const candidates = ['/data', '/usr/src/app/upload']; for (const candidate of candidates) { - const exists = this.storageRepository.existsSync(candidate); - if (exists) { + const isExists = this.storageRepository.existsSync(candidate); + if (isExists) { targets.push(candidate); } } @@ -295,8 +295,8 @@ export class MaintenanceWorkerService { } async runRestoreDatabase(action: SetMaintenanceModeDto) { - const lock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation); - if (!lock) { + const isLock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation); + if (!isLock) { return; } diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 5752b70863..b315538bf9 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -130,7 +130,10 @@ class AlbumAccess { .where('shared_link.albumId', 'in', [...albumIds]) .execute() .then( - (sharedLinks) => new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))), + (sharedLinks) => + new Set( + sharedLinks.filter((sharedLink) => sharedLink.albumId).map((sharedLink) => sharedLink.albumId), + ) as Set, ); } } diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index 724788fa74..ced4357ffe 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -314,7 +314,7 @@ export class AlbumRepository { albumUsers: AlbumUserCreateDto[], authUserId: string, ) { - if (!albumUsers.some((u) => u.role === AlbumUserRole.Owner)) { + if (albumUsers.every((u) => u.role !== AlbumUserRole.Owner)) { throw new Error('Album must have an owner'); } diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index a4e58c52ec..3fc9fea2e3 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -192,9 +192,8 @@ export class DatabaseRepository { ) { probes[indexName] = this.targetProbeCount(targetLists); return this.reindexVectors(indexName, { lists: targetLists }); - } else { - probes[indexName] = this.targetProbeCount(lists); } + probes[indexName] = this.targetProbeCount(lists); }), ); break; @@ -228,7 +227,7 @@ export class DatabaseRepository { if (table === 'smart_search') { await sql`ALTER TABLE ${sql.raw(table)} DROP CONSTRAINT IF EXISTS dim_size_constraint`.execute(tx); } - if (!rows.some((row) => row.columnName === 'embedding')) { + if (rows.every((row) => row.columnName !== 'embedding')) { this.logger.warn(`Column 'embedding' does not exist in table '${table}', truncating and adding column.`); await sql`TRUNCATE TABLE ${sql.raw(table)}`.execute(tx); await sql`ALTER TABLE ${sql.raw(table)} ADD COLUMN embedding real[] NOT NULL`.execute(tx); @@ -349,11 +348,9 @@ export class DatabaseRepository { private targetListCount(count: number) { if (count < 128_000) { return 1; - } else if (count < 2_048_000) { - return 1 << (32 - Math.clz32(count / 1000)); - } else { - return 1 << (33 - Math.clz32(Math.sqrt(count))); } + // eslint-disable-next-line unicorn/prefer-minimal-ternary + return count < 2_048_000 ? 1 << (32 - Math.clz32(count / 1000)) : 1 << (33 - Math.clz32(Math.sqrt(count))); } private targetProbeCount(lists: number) { @@ -378,9 +375,7 @@ export class DatabaseRepository { for (const result of results ?? []) { if (result.status === 'Success') { this.logger.log(`Migration "${result.migrationName}" succeeded`); - } - - if (result.status === 'Error') { + } else if (result.status === 'Error') { this.logger.warn(`Migration "${result.migrationName}" failed`); } } @@ -485,9 +480,7 @@ export class DatabaseRepository { for (const result of results ?? []) { if (result.status === 'Success') { this.logger.log(`Reverted migration "${result.migrationName}"`); - } - - if (result.status === 'Error') { + } else if (result.status === 'Error') { this.logger.warn(`Failed to revert migration "${result.migrationName}"`); } } diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 09a89fd9bb..82e67ce97c 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -213,11 +213,11 @@ export class EventRepository { private addHandler(item: Item): void { const event = item.event; - if (!this.emitHandlers[event]) { + if (!Object.hasOwn(this.emitHandlers, event)) { this.emitHandlers[event] = []; } - this.emitHandlers[event].push(item); + this.emitHandlers[event]!.push(item); } emit(event: T, ...args: ArgsOf): Promise { diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index 14a242e63b..e925bd5030 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -54,11 +54,11 @@ export class JobRepository { const label = `${Service.name}.${handler.name}`; // one handler per job - if (this.handlers[jobName]) { + if (Object.hasOwn(this.handlers, jobName)) { const jobKey = getKeyByValue(JobName, jobName); const errorMessage = `Failed to add job handler for ${label}`; this.logger.error( - `${errorMessage}. JobName.${jobKey} is already handled by ${this.handlers[jobName].label}.`, + `${errorMessage}. JobName.${jobKey} is already handled by ${this.handlers[jobName]!.label}.`, ); throw new ImmichStartupError(errorMessage); } @@ -104,24 +104,26 @@ export class JobRepository { } teardown() { - if (this.workerWatcher) { - clearInterval(this.workerWatcher); - this.workerWatcher = undefined; + if (!this.workerWatcher) { + return; } + + clearInterval(this.workerWatcher); + this.workerWatcher = undefined; } private async checkWorkers() { - let present: boolean; + let isPresent: boolean; try { const suffix = `:w:${ImmichWorker.Microservices}`; const workers = await this.getQueue(QueueName.BackgroundTask).getWorkers(); - present = workers.some((worker) => worker.rawname?.endsWith(suffix)); + isPresent = workers.some((worker) => worker.rawname?.endsWith(suffix)); } catch { return; } - if (this.microservicesPresent !== present) { - if (present) { + if (this.microservicesPresent !== isPresent) { + if (isPresent) { this.logger.log('Microservices worker connected.'); } else { this.logger.warn( @@ -129,7 +131,7 @@ export class JobRepository { ); } } - this.microservicesPresent = present; + this.microservicesPresent = isPresent; } async run({ name, data }: JobItem) { @@ -212,7 +214,7 @@ export class JobRepository { // need to use add() instead of addBulk() for jobId/deduplication to take effect promises.push(this.getQueue(queueName).add(item.name, item.data, job.options)); } else { - itemsByQueue[queueName] = itemsByQueue[queueName] || []; + itemsByQueue[queueName] ||= []; itemsByQueue[queueName].push(job); } } diff --git a/server/src/repositories/logging.repository.spec.ts b/server/src/repositories/logging.repository.spec.ts index 99bb1dbf18..9e7317cbbd 100644 --- a/server/src/repositories/logging.repository.spec.ts +++ b/server/src/repositories/logging.repository.spec.ts @@ -26,7 +26,7 @@ describe(LoggingRepository.name, () => { const logger = new MyConsoleLogger(clsMock, { color: true }); - expect(logger.formatContext('context')).toBe('\u001B[33m[Api:context]\u001B[39m '); + expect(logger.formatContext('context')).toBe('\u{1B}[33m[Api:context]\u{1B}[39m '); }); it('should not use colors when color is false', () => { diff --git a/server/src/repositories/logging.repository.ts b/server/src/repositories/logging.repository.ts index c1df648f09..f3da39c97c 100644 --- a/server/src/repositories/logging.repository.ts +++ b/server/src/repositories/logging.repository.ts @@ -67,7 +67,7 @@ export class MyConsoleLogger extends ConsoleLogger { }; private withColor(text: string, color: LogColor) { - return this.isColorEnabled ? `\u001B[${color}m${text}\u001B[39m` : text; + return this.isColorEnabled ? `\u{1B}[${color}m${text}\u{1B}[39m` : text; } } @@ -80,17 +80,17 @@ export class LoggingRepository { @Inject(ClsService) cls: ClsService | undefined, @Inject(ConfigRepository) configRepository: ConfigRepository | undefined, ) { - let noColor = false; + let isNoColor = false; let logFormat = LogFormat.Console; if (configRepository) { const env = configRepository.getEnv(); - noColor = env.noColor; + isNoColor = env.noColor; logFormat = env.logFormat ?? logFormat; } this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, json: logFormat === LogFormat.Json, - color: !noColor, + color: !isNoColor, }); } diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts index 6792e8ecf4..c9825f31c1 100644 --- a/server/src/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -130,19 +130,19 @@ export class MachineLearningRepository { } private async check(url: string) { - let healthy = false; + let isHealthy = false; try { const response = await fetch(new URL('ping', url), { signal: AbortSignal.timeout(this.config.availabilityChecks.timeout), }); if (response.ok) { - healthy = true; + isHealthy = true; } } catch { // nothing to do here } - this.setHealthy(url, healthy); + this.setHealthy(url, isHealthy); } private setHealthy(url: string, healthy: boolean) { diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 87cf30b618..3d01474434 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -291,8 +291,8 @@ export class MapRepository { id: Number.parseInt(lineSplit[0]), name: lineSplit[1], alternateNames: lineSplit[3], - latitude: Number.parseFloat(lineSplit[4]), - longitude: Number.parseFloat(lineSplit[5]), + latitude: Number(lineSplit[4]), + longitude: Number(lineSplit[5]), countryCode: lineSplit[8], admin1Code: lineSplit[10], admin2Code: lineSplit[11], @@ -308,6 +308,7 @@ export class MapRepository { .insertInto('geodata_places') .values(bufferGeodata) .execute() + .then(() => { count += curLength; if (count % 10_000 === 0) { diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 092ca227e4..181b1eada9 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -43,9 +43,6 @@ const probe = (input: string, options: string[]): Promise => ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))), ); -sharp.concurrency(0); -sharp.cache({ files: 0 }); - const pascalCase = (str: string) => _.upperFirst(_.camelCase(str.toLowerCase())); type ProgressEvent = { @@ -66,6 +63,8 @@ export type ExtractResult = { export class MediaRepository { constructor(private logger: LoggingRepository) { this.logger.setContext(MediaRepository.name); + sharp.concurrency(0); + sharp.cache({ files: 0 }); } /** @@ -451,6 +450,7 @@ export class MediaRepository { } private parseFloat(value: string | number | undefined): number { + // eslint-disable-next-line unicorn/prefer-number-coercion return Number.parseFloat(value as string) || 0; } diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 1d1e0d7d29..94047bf326 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -108,6 +108,7 @@ export class MetadataRepository { readTags(path: string): Promise { const options: ReadTaskOptions | undefined = mimeTypes.isVideo(path) ? { readArgs: ['-ee'] } : undefined; + return this.exiftool.read(path, options).catch((error) => { this.logger.warn(`Error reading exif data (${path}): ${error}\n${error?.stack}`); return {}; diff --git a/server/src/repositories/oauth.repository.ts b/server/src/repositories/oauth.repository.ts index a22c9d56e2..c3f199d246 100644 --- a/server/src/repositories/oauth.repository.ts +++ b/server/src/repositories/oauth.repository.ts @@ -68,7 +68,7 @@ export class OAuthRepository { params.code_challenge_method = 'S256'; } - const url = buildAuthorizationUrl(client, params).toString(); + const url = buildAuthorizationUrl(client, params).href; return { url, state, codeVerifier }; } @@ -173,6 +173,7 @@ export class OAuthRepository { // Validate specific Logout Token claims (RFC 8963): // "events" claim must exist and contain the backchannel-logout event const events = payload.events as Record | undefined; + // eslint-disable-next-line unicorn/prefer-https if (!events || !events['http://schemas.openid.net/event/backchannel-logout']) { throw new Error('Missing backchannel-logout event claim'); } diff --git a/server/src/repositories/process.repository.ts b/server/src/repositories/process.repository.ts index 928531408f..f5e761a965 100644 --- a/server/src/repositories/process.repository.ts +++ b/server/src/repositories/process.repository.ts @@ -7,7 +7,7 @@ export class ProcessRepository { spawn = spawn; spawnDuplexStream(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): Duplex { - let stdinClosed = false; + let isStdinClosed = false; let drainCallback: undefined | (() => void); const process = this.spawn(command, args, options); @@ -15,7 +15,7 @@ export class ProcessRepository { // duplex -> stdin write(chunk, encoding, callback) { // drain the input if process dies - if (stdinClosed) { + if (isStdinClosed) { return callback(); } @@ -36,7 +36,7 @@ export class ProcessRepository { }, final(callback) { - if (stdinClosed) { + if (isStdinClosed) { callback(); } else { process.stdin.end(callback); @@ -55,19 +55,19 @@ export class ProcessRepository { duplex.on('resume', () => process.stdout.resume()); // end handling - let stdoutClosed = false; + let isStdoutClosed = false; function close(error?: Error) { - stdinClosed = true; + isStdinClosed = true; if (error) { duplex.destroy(error); - } else if (stdoutClosed && typeof process.exitCode === 'number') { + } else if (isStdoutClosed && typeof process.exitCode === 'number') { duplex.push(null); } } process.stdout.on('close', () => { - stdoutClosed = true; + isStdoutClosed = true; close(); }); diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index 0570c9d2cc..285ab4ee80 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -35,7 +35,7 @@ const exec = promisify(execCallback); const maybeFirstLine = async (command: string): Promise => { try { const { stdout } = await exec(command); - return stdout.trim().split('\n')[0] || ''; + return stdout.trim().split('\n', 1)[0] || ''; } catch { return ''; } @@ -111,6 +111,7 @@ export class ServerInfoRepository { const lockfile: BuildLockfile | undefined = await readFile(resourcePaths.lockFile) .then((buffer) => JSON.parse(buffer.toString())) + .catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`)); const [nodejsVersion, ffmpegVersion, magickVersion, exiftoolVersion] = await Promise.all([ diff --git a/server/src/repositories/telemetry.repository.ts b/server/src/repositories/telemetry.repository.ts index 036a1f9fab..9a486005b5 100644 --- a/server/src/repositories/telemetry.repository.ts +++ b/server/src/repositories/telemetry.repository.ts @@ -95,10 +95,12 @@ export const bootstrapTelemetry = (port: number) => { }; export const teardownTelemetry = async () => { - if (instance) { - await instance.shutdown(); - instance = undefined; + if (!instance) { + return; } + + await instance.shutdown(); + instance = undefined; }; @Injectable() @@ -153,7 +155,7 @@ export class TelemetryRepository { } const method = descriptor.value; - const propertyName = snakeCase(String(propName)); + const propertyName = snakeCase(propName); const metricName = `${snakeCase(className).replaceAll(/_(?=(repository)|(controller)|(provider)|(service)|(module))/g, '.')}.${propertyName}.duration`; const histogram = this.metricService.getHistogram(metricName, { @@ -165,6 +167,7 @@ export class TelemetryRepository { descriptor.value = function (...args: any[]) { const start = performance.now(); + // eslint-disable-next-line unicorn/no-this-outside-of-class const result = method.apply(this, args); void Promise.resolve(result) diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts index 20b41c80f8..11a6e532aa 100644 --- a/server/src/repositories/user.repository.ts +++ b/server/src/repositories/user.repository.ts @@ -46,7 +46,7 @@ export class UserRepository { @GenerateSql({ params: [DummyValue.UUID, DummyValue.BOOLEAN] }) get(userId: string, options: UserFindOptions) { - options = options || {}; + options ||= {}; return this.db .selectFrom('user') diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index b1c25f8286..6e179f10f2 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -44,7 +44,7 @@ export class ActivityService extends BaseService { }; let activity: Activity | undefined; - let duplicate = false; + let isDuplicate = false; if (dto.type === ReactionType.LIKE) { delete dto.comment; @@ -54,7 +54,7 @@ export class ActivityService extends BaseService { assetId: dto.assetId ?? null, isLiked: true, }); - duplicate = !!activity; + isDuplicate = !!activity; } if (!activity) { @@ -65,7 +65,7 @@ export class ActivityService extends BaseService { }); } - return { duplicate, value: mapActivity(activity) }; + return { duplicate: isDuplicate, value: mapActivity(activity) }; } async delete(auth: AuthDto, id: string): Promise { diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 6f8f4e1dca..ab86bcf2d4 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -226,7 +226,7 @@ export class AlbumService extends BaseService { const events: { id: string; recipients: string[] }[] = []; for (const albumId of allowedAlbumIds) { const existingAssetIds = await this.albumRepository.getAssetIds(albumId, [...allowedAssetIds]); - const notPresentAssetIds = [...allowedAssetIds].filter((id) => !existingAssetIds.has(id)); + const notPresentAssetIds = [...allowedAssetIds.difference(existingAssetIds)]; if (notPresentAssetIds.length === 0) { continue; } @@ -288,7 +288,7 @@ export class AlbumService extends BaseService { throw new BadRequestException('Cannot add another owner'); } - const exists = album.albumUsers.find(({ user: { id } }) => id === userId); + const exists = album.albumUsers.some(({ user: { id } }) => id === userId); if (exists) { continue; } @@ -303,7 +303,7 @@ export class AlbumService extends BaseService { await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name }); } - return this.findOrFail(id, auth.user.id, { withAssets: true }).then(mapAlbum); + return mapAlbum(await this.findOrFail(id, auth.user.id, { withAssets: true })); } async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise { diff --git a/server/src/services/api.service.ts b/server/src/services/api.service.ts index d5cec13f77..29e84eda7b 100644 --- a/server/src/services/api.service.ts +++ b/server/src/services/api.service.ts @@ -22,7 +22,7 @@ export const render = (index: string, meta: OpenGraphTags) => { ${imageUrl ? `` : ''}`; - return index.replace('', tags); + return index.replace('', () => tags); }; @Injectable() diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 9ef72a0897..2e6a5b36ba 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -163,8 +163,8 @@ const assetEntity = Object.freeze({ duration: null, files: [] as AssetFile[], exifInfo: { - latitude: 49.533_547, - longitude: 10.703_075, + latitude: 49.533547, + longitude: 10.703075, }, livePhotoVideoId: null, } as MapAsset); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index e2d2d95f81..c27bbd4b18 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -280,15 +280,17 @@ export class AssetService extends BaseService { let chunk: Array<{ id: string; isOffline: boolean }> = []; const queueChunk = async () => { - if (chunk.length > 0) { - await this.jobRepository.queueAll( - chunk.map(({ id, isOffline }) => ({ - name: JobName.AssetDelete, - data: { id, deleteOnDisk: !isOffline }, - })), - ); - chunk = []; + if (chunk.length === 0) { + return; } + + await this.jobRepository.queueAll( + chunk.map(({ id, isOffline }) => ({ + name: JobName.AssetDelete, + data: { id, deleteOnDisk: !isOffline }, + })), + ); + chunk = []; }; const assets = this.assetJobRepository.streamForDeletedJob(trashedBefore); diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index a824b68814..ed5b553a5b 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -653,13 +653,13 @@ describe(AuthService.name, () => { describe('getMobileRedirect', () => { it('should pass along the query params', () => { - expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual( + expect(sut.getMobileRedirect('https://immich.app?code=123&state=456')).toEqual( 'app.immich:///oauth-callback?code=123&state=456', ); }); it('should work if called without query params', () => { - expect(sut.getMobileRedirect('http://immich.app')).toEqual('app.immich:///oauth-callback?'); + expect(sut.getMobileRedirect('https://immich.app')).toEqual('app.immich:///oauth-callback?'); }); }); @@ -979,7 +979,7 @@ describe(AuthService.name, () => { }); expect(mocks.oauth.getProfilePicture).toHaveBeenCalledWith(profile.picture); expect(mocks.media.generateThumbnail).toHaveBeenCalledWith( - Buffer.from(pictureBytes.buffer), + Buffer.from(pictureBytes.buffer, pictureBytes.byteOffset, pictureBytes.byteLength), expect.objectContaining({ format: 'webp', processInvalidImages: false }), expect.stringContaining(`/data/profile/${user.id}/${fileId}.webp`), ); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 6cfc60d583..288ef092c7 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -65,9 +65,9 @@ export class AuthService extends BaseService { const user = await this.userRepository.getByEmail(dto.email, { withPassword: true }); // Always run bcrypt so response time is constant regardless of whether the email // is registered, preventing timing-based user enumeration. - const authenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH); + const isAuthenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH); - if (!user || !user.password || !authenticated) { + if (!user || !user.password || !isAuthenticated) { this.logger.warn(`Failed login attempt for user ${dto.email} from ip address ${details.clientIp}`); throw new UnauthorizedException('Incorrect email or password'); } @@ -124,8 +124,8 @@ export class AuthService extends BaseService { async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise { const { password, newPassword } = dto; const user = await this.userRepository.getForChangePassword(auth.user.id); - const valid = this.validateSecret(password, user.password); - if (!valid) { + const isValid = this.validateSecret(password, user.password); + if (!isValid) { throw new BadRequestException('Wrong password'); } @@ -271,7 +271,7 @@ export class AuthService extends BaseService { } getMobileRedirect(url: string) { - return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`; + return `${MOBILE_REDIRECT}?${url.split('?', 2)[1] || ''}`; } async authorize(dto: OAuthConfigDto) { @@ -625,7 +625,7 @@ export class AuthService extends BaseService { url: string, ) { if (mobileOverrideEnabled && mobileRedirectUri) { - return url.replace(/app\.immich:\/+oauth-callback/, mobileRedirectUri); + return url.replace(/app\.immich:\/+oauth-callback/, () => mobileRedirectUri); } return url; } diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 62a0de8b56..6d17410441 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -247,7 +247,7 @@ export class BaseService { ctx.workflowRepository, ); - service.logger.setContext(this.name); + service.logger.setContext(BaseService.name); return service as T; } diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index 22f06e2ed9..23d70b6860 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -45,7 +45,6 @@ export class CliService extends BaseService { if (!filesSet.has(name) && rowsSet.has(name)) { migrations.push({ name, status: 'deleted' }); - continue; } } @@ -87,9 +86,9 @@ export class CliService extends BaseService { } async disableMaintenanceMode(): Promise<{ alreadyDisabled: boolean }> { - const currentState = await this.systemMetadataRepository - .get(SystemMetadataKey.MaintenanceMode) - .then((state) => state ?? { isMaintenanceMode: false as const }); + const currentState = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) ?? { + isMaintenanceMode: false as const, + }; if (!currentState.isMaintenanceMode) { return { @@ -114,9 +113,9 @@ export class CliService extends BaseService { username: 'cli-admin', }; - const state = await this.systemMetadataRepository - .get(SystemMetadataKey.MaintenanceMode) - .then((state) => state ?? { isMaintenanceMode: false as const }); + const state = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) ?? { + isMaintenanceMode: false as const, + }; if (state.isMaintenanceMode) { return { @@ -182,11 +181,7 @@ export class CliService extends BaseService { this.userRepository.getFileSamples(), ]); - const paths = []; - - for (const person of people) { - paths.push(person.thumbnailPath); - } + const paths = Array.from(people, (person) => person.thumbnailPath); for (const user of users) { paths.push(user.profileImagePath); diff --git a/server/src/services/database-backup.service.ts b/server/src/services/database-backup.service.ts index 405b9d4d0e..99a3c5bf1f 100644 --- a/server/src/services/database-backup.service.ts +++ b/server/src/services/database-backup.service.ts @@ -143,7 +143,7 @@ export class DatabaseBackupService { databaseUsername = parsedUrl.username || parsedUrl.searchParams.get('user'); - url = parsedUrl.toString(); + url = parsedUrl.href; } // assume typical values if we can't parse URL or not present @@ -214,6 +214,7 @@ export class DatabaseBackupService { bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`, args, databaseUsername, + // eslint-disable-next-line unicorn/prefer-minimal-ternary databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password, databaseVersion, databaseMajorVersion, @@ -228,7 +229,7 @@ export class DatabaseBackupService { this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); - const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz`; + const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ', 1)[0]}.sql.gz`; const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename); const temporaryFilePath = `${backupFilePath}.tmp`; @@ -249,6 +250,7 @@ export class DatabaseBackupService { this.logger.error(`Database Backup Failure: ${error}`); await this.storageRepository .unlink(temporaryFilePath) + .catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`)); throw error; } @@ -354,7 +356,7 @@ export class DatabaseBackupService { ): Promise { this.logger.debug(`Database Restore Started`); - let complete = false; + let isComplete = false; try { if (!isValidDatabaseBackupName(filename)) { throw new Error('Invalid backup file format!'); @@ -399,7 +401,7 @@ export class DatabaseBackupService { }); const [progressSource, progressSink] = createSqlProgressStreams((progress) => { - if (complete) { + if (isComplete) { return; } @@ -437,7 +439,7 @@ export class DatabaseBackupService { }); const [progressSource, progressSink] = createSqlProgressStreams((progress) => { - if (complete) { + if (isComplete) { return; } @@ -453,7 +455,7 @@ export class DatabaseBackupService { this.logger.error(`Database Restore Failure: ${error}`); throw error; } finally { - complete = true; + isComplete = true; } this.logger.log(`Database Restore Success`); @@ -507,7 +509,7 @@ function createSqlProgressStreams(cb: (progress: number) => void) { const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin'); const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`); - let readingStdin = false; + let isReadingStdin = false; let sequenceIdx = 0; let linesSent = 0; @@ -532,19 +534,19 @@ function createSqlProgressStreams(cb: (progress: number) => void) { const source = new PassThrough({ transform(chunk, _encoding, callback) { for (const byte of chunk) { - if (!readingStdin && byte === 10 && lastByte !== 10) { + if (!isReadingStdin && byte === 10 && lastByte !== 10) { linesSent += 1; } lastByte = byte; - const sequence = readingStdin ? STDIN_END_MARKER : STDIN_START_MARKER; + const sequence = isReadingStdin ? STDIN_END_MARKER : STDIN_START_MARKER; if (sequence[sequenceIdx] === byte) { sequenceIdx += 1; if (sequence.length === sequenceIdx) { sequenceIdx = 0; - readingStdin = !readingStdin; + isReadingStdin = !isReadingStdin; } } else { sequenceIdx = 0; @@ -552,6 +554,7 @@ function createSqlProgressStreams(cb: (progress: number) => void) { } cbDebounced(); + // eslint-disable-next-line unicorn/no-this-outside-of-class this.push(chunk); callback(); }, @@ -633,6 +636,7 @@ function createSqlOwnerTransformStream(databaseUsername: string) { } } + // eslint-disable-next-line unicorn/no-this-outside-of-class this.push(result); callback(); }, diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 7ea300f88a..8cb04e29ba 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -220,15 +220,15 @@ export class DuplicateService extends BaseService { if (idsToTrash.length > 0) { // TODO: this is duplicated with AssetService.deleteAssets const { trash } = await this.getConfig({ withCache: true }); - const force = !trash.enabled; + const isForce = !trash.enabled; await this.assetRepository.updateAll(idsToTrash, { deletedAt: new Date(), - status: force ? AssetStatus.Deleted : AssetStatus.Trashed, + status: isForce ? AssetStatus.Deleted : AssetStatus.Trashed, duplicateId: null, }); - await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', { + await this.eventRepository.emit(isForce ? 'AssetDeleteAll' : 'AssetTrashAll', { assetIds: idsToTrash, userId: auth.user.id, }); diff --git a/server/src/services/hls.service.spec.ts b/server/src/services/hls.service.spec.ts index adfc3d5bcc..5badd6c93a 100644 --- a/server/src/services/hls.service.spec.ts +++ b/server/src/services/hls.service.spec.ts @@ -204,10 +204,12 @@ describe(HlsService.name, () => { mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset); mocks.crypto.randomUUID.mockReturnValue(sessionId); mocks.websocket.serverSend.mockImplementation((event, ...rest) => { - if (event === 'HlsSessionRequest') { - const { sessionId: id } = rest[0] as { sessionId: string }; - queueMicrotask(() => sut.onSessionResult({ sessionId: id })); + if (event !== 'HlsSessionRequest') { + return; } + + const { sessionId: id } = rest[0] as { sessionId: string }; + queueMicrotask(() => sut.onSessionResult({ sessionId: id })); }); }; diff --git a/server/src/services/integrity.service.ts b/server/src/services/integrity.service.ts index 2a9d869be3..a7a194b25f 100644 --- a/server/src/services/integrity.service.ts +++ b/server/src/services/integrity.service.ts @@ -312,12 +312,14 @@ export class IntegrityService extends BaseService { this.logger.log(`Processing batch of ${items.length} reports to check if they are out of date.`); const results = await Promise.all( - items.map(({ reportId, path }) => - this.storageRepository - .stat(path) - .then(() => void 0) - .catch(() => reportId), - ), + items.map(async ({ reportId, path }) => { + try { + await this.storageRepository.stat(path); + return; + } catch { + return reportId; + } + }), ); const reportIds = results.filter(Boolean) as string[]; @@ -383,12 +385,14 @@ export class IntegrityService extends BaseService { this.logger.log(`Processing batch of ${items.length} files to check if they are missing.`); const results = await Promise.all( - items.map((item) => - this.storageRepository - .stat(item.path) - .then(() => ({ ...item, exists: true })) - .catch(() => ({ ...item, exists: false })), - ), + items.map(async (item) => { + try { + await this.storageRepository.stat(item.path); + return { ...item, exists: true }; + } catch { + return { ...item, exists: false }; + } + }), ); const outdatedReports = results @@ -420,12 +424,14 @@ export class IntegrityService extends BaseService { this.logger.log(`Processing batch of ${paths.length} reports to check if they are out of date.`); const results = await Promise.all( - paths.map(({ reportId, path }) => - this.storageRepository - .stat(path) - .then(() => reportId) - .catch(() => void 0), - ), + paths.map(async ({ reportId, path }) => { + try { + await this.storageRepository.stat(path); + return reportId; + } catch { + return; + } + }), ); const reportIds = results.filter(Boolean) as string[]; diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 3807d1424b..f13a440f84 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -162,10 +162,12 @@ export class LibraryService extends BaseService { } async unwatch(id: string) { - if (this.watchers[id]) { - await this.watchers[id](); - delete this.watchers[id]; + if (!Object.hasOwn(this.watchers, id)) { + return; } + + await this.watchers[id](); + delete this.watchers[id]; } @OnEvent({ name: 'AppShutdown' }) @@ -252,18 +254,22 @@ export class LibraryService extends BaseService { if (!library) { this.logger.debug(`Library ${job.libraryId} not found, skipping file import`); return JobStatus.Failed; - } else if (library.deletedAt) { + } + if (library.deletedAt) { this.logger.debug(`Library ${job.libraryId} is deleted, won't import assets into it`); return JobStatus.Failed; } const assetImports: Insertable[] = []; await Promise.all( - job.paths.map((path) => - this.processEntity(path, library.ownerId, job.libraryId) - .then((asset) => assetImports.push(asset)) - .catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`)), - ), + job.paths.map(async (path) => { + try { + const asset = await this.processEntity(path, library.ownerId, job.libraryId); + assetImports.push(asset); + } catch (error) { + this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`); + } + }), ); const assetIds = await this.assetRepository.createAll(assetImports); @@ -316,9 +322,9 @@ export class LibraryService extends BaseService { return validation; } - const access = await this.storageRepository.checkFileExists(importPath, R_OK); + const isAccess = await this.storageRepository.checkFileExists(importPath, R_OK); - if (!access) { + if (!isAccess) { validation.message = 'Lacking read permission for folder'; return validation; } @@ -369,18 +375,20 @@ export class LibraryService extends BaseService { await this.assetRepository.updateByLibraryId(libraryId, { deletedAt: new Date() }); - let assetsFound = false; + let isAssetsFound = false; let chunk: string[] = []; const queueChunk = async () => { - if (chunk.length > 0) { - assetsFound = true; - this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`); - await this.jobRepository.queueAll( - chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })), - ); - chunk = []; + if (chunk.length === 0) { + return; } + + isAssetsFound = true; + this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`); + await this.jobRepository.queueAll( + chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })), + ); + chunk = []; }; this.logger.debug(`Will delete all assets in library ${libraryId}`); @@ -395,7 +403,7 @@ export class LibraryService extends BaseService { await queueChunk(); - if (!assetsFound) { + if (!isAssetsFound) { this.logger.log(`Deleting library ${libraryId}`); await this.libraryRepository.delete(libraryId); } @@ -520,7 +528,7 @@ export class LibraryService extends BaseService { break; } case AssetSyncResult.CHECK_OFFLINE: { - const isInImportPath = job.importPaths.find((path) => asset.originalPath.startsWith(path)); + const isInImportPath = job.importPaths.some((path) => asset.originalPath.startsWith(path)); if (!isInImportPath) { this.logger.verbose( @@ -742,28 +750,30 @@ export class LibraryService extends BaseService { let count = 0; const queueChunk = async () => { - if (chunk.length > 0) { - count += chunk.length; - - await this.jobRepository.queue({ - name: JobName.LibrarySyncAssets, - data: { - libraryId: library.id, - importPaths: library.importPaths, - exclusionPatterns: library.exclusionPatterns, - assetIds: chunk.map((id) => id), - progressCounter: count, - totalAssets: assetCount, - }, - }); - chunk = []; - - const completePercentage = ((100 * count) / assetCount).toFixed(1); - - this.logger.log( - `Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`, - ); + if (chunk.length === 0) { + return; } + + count += chunk.length; + + await this.jobRepository.queue({ + name: JobName.LibrarySyncAssets, + data: { + libraryId: library.id, + importPaths: library.importPaths, + exclusionPatterns: library.exclusionPatterns, + assetIds: chunk.map((id) => id), + progressCounter: count, + totalAssets: assetCount, + }, + }); + chunk = []; + + const completePercentage = ((100 * count) / assetCount).toFixed(1); + + this.logger.log( + `Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`, + ); }; this.logger.log(`Scanning library ${library.id} for assets missing from disk...`); diff --git a/server/src/services/maintenance.service.ts b/server/src/services/maintenance.service.ts index 8e711ef380..ca1e05d93f 100644 --- a/server/src/services/maintenance.service.ts +++ b/server/src/services/maintenance.service.ts @@ -26,6 +26,7 @@ export class MaintenanceService extends BaseService { getMaintenanceMode(): Promise { return this.systemMetadataRepository .get(SystemMetadataKey.MaintenanceMode) + .then((state) => state ?? { isMaintenanceMode: false }); } diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index a73eb3e22e..37817e0508 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -76,8 +76,11 @@ export class MediaService extends BaseService { jobs = []; }; - const fullsizeEnabled = config.image.fullsize.enabled; - for await (const asset of this.assetJobRepository.streamForThumbnailJob({ force, fullsizeEnabled })) { + const isFullsizeEnabled = config.image.fullsize.enabled; + for await (const asset of this.assetJobRepository.streamForThumbnailJob({ + force, + fullsizeEnabled: isFullsizeEnabled, + })) { if (force || !asset.isEdited) { jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } }); } @@ -272,13 +275,14 @@ export class MediaService extends BaseService { } private async extractOriginalImage(asset: ThumbnailAsset, image: SystemConfig['image'], useEdits = false) { - const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName); - const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null; - const generateFullsize = + const isExtractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName); + const extracted = isExtractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null; + const isGenerateFullsize = ((image.fullsize.enabled || asset.exifInfo.projectionType === 'EQUIRECTANGULAR') && !mimeTypes.isWebSupportedImage(asset.originalPath)) || useEdits; - const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`)); + const isConvertFullsize = + isGenerateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`)); const thumbSource = extracted ? extracted.buffer : asset.originalPath; const { data, info, colorspace } = await this.decodeImage( @@ -286,7 +290,7 @@ export class MediaService extends BaseService { // only specify orientation to extracted images which don't have EXIF orientation data // or it can double rotate the image extracted ? asset.exifInfo : { ...asset.exifInfo, orientation: null }, - convertFullsize ? undefined : image.preview.size, + isConvertFullsize ? undefined : image.preview.size, ); let isTransparent = false; @@ -299,8 +303,8 @@ export class MediaService extends BaseService { data, info, colorspace, - convertFullsize, - generateFullsize, + convertFullsize: isConvertFullsize, + generateFullsize: isGenerateFullsize, isTransparent, }; } @@ -620,20 +624,20 @@ export class MediaService extends BaseService { return JobStatus.Failed; } - let partialFallbackSuccess = false; + let isPartialFallbackSuccess = false; if (ffmpeg.accelDecode) { try { this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and software decoding`); ffmpeg = { ...ffmpeg, accelDecode: false }; const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream); await this.mediaRepository.transcode(input, output, command); - partialFallbackSuccess = true; + isPartialFallbackSuccess = true; } catch (error: any) { this.logger.error(`Error occurred during transcoding: ${error.message}`); } } - if (!partialFallbackSuccess) { + if (!isPartialFallbackSuccess) { this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`); ffmpeg = { ...ffmpeg, accel: TranscodeHardwareAcceleration.Disabled }; const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream); @@ -700,9 +704,9 @@ export class MediaService extends BaseService { } private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: VideoStreamInfo): boolean { - const scalingEnabled = ffmpegConfig.targetResolution !== 'original'; + const isScalingEnabled = ffmpegConfig.targetResolution !== 'original'; const targetRes = Number.parseInt(ffmpegConfig.targetResolution); - const isLargerThanTargetRes = scalingEnabled && Math.min(stream.height, stream.width) > targetRes; + const isLargerThanTargetRes = isScalingEnabled && Math.min(stream.height, stream.width) > targetRes; const maxBitrate = this.parseBitrateToBps(ffmpegConfig.maxBitrate); const isLargerThanTargetBitrate = maxBitrate > 0 && stream.bitrate > maxBitrate; @@ -757,13 +761,13 @@ export class MediaService extends BaseService { }): boolean { if (colorspace || profileDescription) { return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb')); - } else if (bitsPerSample) { + } + if (bitsPerSample) { // assume sRGB for 8-bit images with no color profile or colorspace metadata return bitsPerSample === 8; - } else { - // assume sRGB for images with no relevant metadata - return true; } + // assume sRGB for images with no relevant metadata + return true; } private parseBitrateToBps(bitrateString: string) { @@ -776,11 +780,11 @@ export class MediaService extends BaseService { if (bitrateString.toLowerCase().endsWith('k')) { return bitrateValue * 1000; // Kilobits per second to bits per second - } else if (bitrateString.toLowerCase().endsWith('m')) { - return bitrateValue * 1_000_000; // Megabits per second to bits per second - } else { - return bitrateValue; } + if (bitrateString.toLowerCase().endsWith('m')) { + return bitrateValue * 1_000_000; // Megabits per second to bits per second + } + return bitrateValue; } private async shouldUseExtractedImage(extractedPathOrBuffer: string | Buffer, targetSize: number) { diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index ac8f88ad87..41510056ff 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -136,7 +136,7 @@ export class MemoryService extends BaseService { const repos = { access: this.accessRepository, bulk: this.memoryRepository }; const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids }); - const hasSuccess = results.find(({ success }) => success); + const hasSuccess = results.some(({ success }) => success); if (hasSuccess) { await this.memoryRepository.update(id, { updatedAt: new Date() }); } @@ -154,7 +154,7 @@ export class MemoryService extends BaseService { canAlwaysRemove: Permission.MemoryDelete, }); - const hasSuccess = results.find(({ success }) => success); + const hasSuccess = results.some(({ success }) => success); if (hasSuccess) { await this.memoryRepository.update(id, { id, updatedAt: new Date() }); } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 8dc8e3a9eb..57c029961e 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1396,7 +1396,7 @@ describe(MetadataService.name, () => { expect(mocks.person.refreshFaces).toHaveBeenCalledWith( [ expect.objectContaining({ - boundingBoxX1: Math.floor((0.485_648_148_148_148_2 - 0.2 / 2) * 1000), + boundingBoxX1: Math.floor((0.4856481481481482 - 0.2 / 2) * 1000), }), ], [], diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 5c47b7eda6..171dcfe514 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -117,9 +117,7 @@ const validateRange = (value: number | undefined, min: number, max: number): Non }; const getLensModel = (exifTags: ImmichTags): string | null => { - const lensModel = String( - exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '', - ).trim(); + const lensModel = (exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '').trim(); if (lensModel === '----') { return null; } @@ -286,7 +284,7 @@ export class MetadataService extends BaseService { exifImageHeight: validate(height), exifImageWidth: validate(width), orientation: validate(exifTags.Orientation)?.toString() ?? null, - projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, + projectionType: exifTags.ProjectionType ? exifTags.ProjectionType.toUpperCase() : null, bitsPerSample: this.getBitsPerSample(exifTags), colorspace: exifTags.ColorSpace === undefined ? null : String(exifTags.ColorSpace), @@ -295,7 +293,7 @@ export class MetadataService extends BaseService { exifTags.Make ?? exifTags.Device?.Manufacturer ?? exifTags.AndroidMake ?? (exifTags.DeviceManufacturer || null), model: exifTags.Model ?? exifTags.Device?.ModelName ?? exifTags.AndroidModel ?? (exifTags.DeviceModelName || null), - fps: video?.frameRate ?? validate(Number.parseFloat(exifTags.VideoFrameRate!)), + fps: video?.frameRate ?? validate(Number(exifTags.VideoFrameRate!)), iso: validate(exifTags.ISO) as number, exposureTime: exifTags.ExposureTime ?? null, lensModel: getLensModel(exifTags), @@ -326,7 +324,7 @@ export class MetadataService extends BaseService { : undefined; const videoData = - format?.formatName && format?.formatLongName && video?.codecName && video?.timeBase + format?.formatName && format.formatLongName && video?.codecName && video?.timeBase ? { assetId: asset.id, bitrate: video.bitrate, @@ -362,8 +360,8 @@ export class MetadataService extends BaseService { : undefined; const isSidewards = exifTags.Orientation && this.isOrientationSidewards(exifTags.Orientation); - const assetWidth = isSidewards ? validate(height) : validate(width); - const assetHeight = isSidewards ? validate(width) : validate(height); + const assetWidth = validate(isSidewards ? height : width); + const assetHeight = validate(isSidewards ? width : height); const tasks = new Tasks(); @@ -445,8 +443,8 @@ export class MetadataService extends BaseService { let sidecarPath = null; for (const candidate of this.getSidecarCandidates(asset)) { - const exists = await this.storageRepository.checkFileExists(candidate, constants.R_OK); - if (!exists) { + const isExists = await this.storageRepository.checkFileExists(candidate, constants.R_OK); + if (!isExists) { continue; } @@ -806,8 +804,8 @@ export class MetadataService extends BaseService { } // write extracted motion video to disk, especially if the encoded-video folder has been deleted - const existsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath); - if (!existsOnDisk) { + const isExistsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath); + if (!isExistsOnDisk) { this.storageCore.ensureFolders(motionAsset.originalPath); await this.storageRepository.createFile(motionAsset.originalPath, video); this.logger.log(`Wrote motion photo video to ${motionAsset.originalPath}`); @@ -1087,6 +1085,7 @@ export class MetadataService extends BaseService { private getDuration(tags: ImmichTags): number | null { const duration = tags.Duration; + // eslint-disable-next-line unicorn/prefer-number-coercion const seconds = typeof duration === 'number' ? duration : Number.parseFloat(duration as string); return Number.isFinite(seconds) ? Math.round(Duration.fromObject({ seconds }).toMillis()) : null; } diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index bb17590795..8e0cd2ff01 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -120,7 +120,7 @@ export class PersonService extends BaseService { await this.createNewFeaturePhoto([face.person.id]); } - return await this.findOrFail(personId).then(mapPerson); + return mapPerson(await this.findOrFail(personId)); } async getFacesById(auth: AuthDto, dto: FaceDto): Promise { @@ -152,7 +152,7 @@ export class PersonService extends BaseService { async getById(auth: AuthDto, id: string): Promise { await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] }); - return this.findOrFail(id).then(mapPerson); + return mapPerson(await this.findOrFail(id)); } async getStatistics(auth: AuthDto, id: string): Promise { @@ -192,7 +192,7 @@ export class PersonService extends BaseService { const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto; // TODO: set by faceId directly - let faceId: string | undefined = undefined; + let faceId: string | undefined; if (assetId) { await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [assetId] }); const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId }); @@ -655,14 +655,12 @@ export class PersonService extends BaseService { topLeft = { x: topLeft.x * scaleFactor, y: topLeft.y * scaleFactor }; bottomRight = { x: bottomRight.x * scaleFactor, y: bottomRight.y * scaleFactor }; - const { - points: [invertedTopLeft, invertedBottomRight], - } = transformPoints( + const [invertedTopLeft, invertedBottomRight] = transformPoints( [topLeft, bottomRight], edits, { width: asset.width, height: asset.height }, { inverse: true }, - ); + ).points; // make sure topLeft is top-left and bottomRight is bottom-right topLeft = { diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index bf744aa01d..1f0bf6c76b 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -93,10 +93,7 @@ export class QueueService extends BaseService { private updateConcurrency(config: SystemConfig) { this.logger.debug(`Updating queue concurrency settings`); for (const queueName of Object.values(QueueName)) { - let concurrency = 1; - if (this.isConcurrentQueue(queueName)) { - concurrency = config.job[queueName].concurrency; - } + const concurrency = this.isConcurrentQueue(queueName) ? config.job[queueName].concurrency : 1; this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`); this.jobRepository.setConcurrency(queueName, concurrency); } @@ -161,9 +158,7 @@ export class QueueService extends BaseService { throw new BadRequestException(`The BackgroundTask queue cannot be paused`); } await this.jobRepository.pause(name); - } - - if (dto.isPaused === false) { + } else if (dto.isPaused === false) { await this.jobRepository.resume(name); } diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 3b66b677a5..ad212292ba 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -77,7 +77,7 @@ export class ServerService extends BaseService { serverInfo.diskAvailableRaw = diskInfo.available; serverInfo.diskSizeRaw = diskInfo.total; serverInfo.diskUseRaw = diskInfo.total - diskInfo.free; - serverInfo.diskUsagePercentage = Number.parseFloat(usagePercentage); + serverInfo.diskUsagePercentage = Number(usagePercentage); return serverInfo; } @@ -190,8 +190,12 @@ export class ServerService extends BaseService { throw new BadRequestException('Invalid license key'); } const { licensePublicKey } = this.configRepository.getEnv(); - const licenseValid = this.cryptoRepository.verifySha256(dto.licenseKey, dto.activationKey, licensePublicKey.server); - if (!licenseValid) { + const isLicenseValid = this.cryptoRepository.verifySha256( + dto.licenseKey, + dto.activationKey, + licensePublicKey.server, + ); + if (!isLicenseValid) { throw new BadRequestException('Invalid license key'); } diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 79e4b23e6a..41b1d5403c 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -20,6 +20,7 @@ export class SharedLinkService extends BaseService { async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise { return this.sharedLinkRepository .getAll({ userId: auth.user.id, id, albumId }) + .then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false }))); } @@ -200,7 +201,7 @@ export class SharedLinkService extends BaseService { const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { - const wasRemoved = removedAssetIds.find((id) => id === assetId); + const wasRemoved = removedAssetIds.includes(assetId); if (!wasRemoved) { results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND }); continue; diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index d484fe8b6a..93b8944099 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -43,12 +43,12 @@ export class SmartInfoService extends BaseService { const modelChange = oldConfig && oldConfig.machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName; - const dimSizeChange = dbDimSize !== dimSize; - if (!modelChange && !dimSizeChange) { + const isDimSizeChange = dbDimSize !== dimSize; + if (!modelChange && !isDimSizeChange) { return; } - if (dimSizeChange) { + if (isDimSizeChange) { this.logger.log( `Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`, ); diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index c84ec70fbf..2dc9f1547e 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -36,7 +36,7 @@ export class StackService extends BaseService { async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise { await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [id] }); const stack = await this.findOrFail(id); - if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) { + if (dto.primaryAssetId && stack.assets.every(({ id }) => id !== dto.primaryAssetId)) { throw new BadRequestException('Primary asset must be in the stack'); } diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index acdcc868b2..ae857870af 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -141,8 +141,8 @@ export class StorageTemplateService extends BaseService { @OnJob({ name: JobName.StorageTemplateMigrationSingle, queue: QueueName.StorageTemplateMigration }) async handleMigrationSingle({ id }: JobOf): Promise { const config = await this.getConfig({ withCache: true }); - const storageTemplateEnabled = config.storageTemplate.enabled; - if (!storageTemplateEnabled) { + const isStorageTemplateEnabled = config.storageTemplate.enabled; + if (!isStorageTemplateEnabled) { return JobStatus.Skipped; } @@ -372,8 +372,8 @@ export class StorageTemplateService extends BaseService { let duplicateCount = 0; while (true) { - const exists = await this.storageRepository.checkFileExists(destination); - if (!exists) { + const isExists = await this.storageRepository.checkFileExists(destination); + if (!isExists) { break; } diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index b443d31c7f..95926d6509 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -30,8 +30,8 @@ export class StorageService extends BaseService { const candidates = ['/data', '/usr/src/app/upload']; for (const candidate of candidates) { - const exists = this.storageRepository.existsSync(candidate); - if (exists) { + const isExists = this.storageRepository.existsSync(candidate); + if (isExists) { targets.push(candidate); } } @@ -56,7 +56,7 @@ export class StorageService extends BaseService { flags.mountChecks = {}; } - let updated = false; + let isUpdated = false; this.logger.log(`Verifying system mount folder checks, current state: ${JSON.stringify(flags)}`); @@ -73,11 +73,11 @@ export class StorageService extends BaseService { if (!flags.mountChecks[folder]) { flags.mountChecks[folder] = true; - updated = true; + isUpdated = true; } } - if (updated) { + if (isUpdated) { await this.systemMetadataRepository.set(SystemMetadataKey.SystemFlags, flags); this.logger.log('Successfully enabled system mount folders checks'); } @@ -166,7 +166,7 @@ export class StorageService extends BaseService { const { folderPath, internalPath, externalPath } = this.getMountFilePaths(folder); try { this.storageRepository.mkdirSync(folderPath); - await this.storageRepository.createFile(internalPath, Buffer.from(`${Date.now()}`)); + await this.storageRepository.createFile(internalPath, Buffer.from(Date.now().toString())); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'EEXIST') { this.logger.warn('Found existing mount file, skipping creation'); @@ -180,7 +180,7 @@ export class StorageService extends BaseService { private async verifyWriteAccess(folder: StorageFolder) { const { internalPath, externalPath } = this.getMountFilePaths(folder); try { - await this.storageRepository.overwriteFile(internalPath, Buffer.from(`${Date.now()}`)); + await this.storageRepository.overwriteFile(internalPath, Buffer.from(Date.now().toString())); } catch (error) { this.logger.error(`Failed to write ${internalPath}: ${error}`); throw new ImmichStartupError(`Failed to write "${externalPath} - ${docsMessage}"`); diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 376ba126e8..87fc40306f 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -192,7 +192,11 @@ export class SyncService extends BaseService { [SyncRequestType.AssetOcrV1]: () => this.syncAssetOcrV1(options, response, checkpointMap, auth), } as const; - for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) { + for (const type of SYNC_TYPES_ORDER) { + if (!dto.types.includes(type)) { + continue; + } + const handler = handlers[type as keyof typeof handlers]; await handler(); } @@ -573,16 +577,16 @@ export class SyncService extends BaseService { } const creates = this.syncRepository.albumAsset.getCreates({ ...options, ack: createCheckpoint }); - let first = true; + let isFirst = true; for await (const { updateId, ...data } of creates) { - if (first) { + if (isFirst) { send(response, { type: SyncEntityType.SyncAckV1, data: {}, ackType: SyncEntityType.AlbumAssetUpdateV2, ids: [options.nowId], }); - first = false; + isFirst = false; } send(response, { type: createType, ids: [updateId], data: mapSyncAssetV2(data) }); } @@ -644,16 +648,16 @@ export class SyncService extends BaseService { } const creates = this.syncRepository.albumAssetExif.getCreates({ ...options, ack: createCheckpoint }); - let first = true; + let isFirst = true; for await (const { updateId, ...data } of creates) { - if (first) { + if (isFirst) { send(response, { type: SyncEntityType.SyncAckV1, data: {}, ackType: SyncEntityType.AlbumAssetExifUpdateV1, ids: [options.nowId], }); - first = false; + isFirst = false; } send(response, { type: createType, ids: [updateId], data }); } diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 8b92d3abf8..1c7195e4c3 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -108,10 +108,12 @@ export class TagService extends BaseService { ); for (const { id: assetId, success } of results) { - if (success) { - await this.updateTags(assetId); - await this.eventRepository.emit('AssetTag', { assetId }); + if (!success) { + continue; } + + await this.updateTags(assetId); + await this.eventRepository.emit('AssetTag', { assetId }); } return results; @@ -127,10 +129,12 @@ export class TagService extends BaseService { ); for (const { id: assetId, success } of results) { - if (success) { - await this.updateTags(assetId); - await this.eventRepository.emit('AssetUntag', { assetId }); + if (!success) { + continue; } + + await this.updateTags(assetId); + await this.eventRepository.emit('AssetUntag', { assetId }); } return results; diff --git a/server/src/services/telemetry.service.ts b/server/src/services/telemetry.service.ts index 7c4fe43214..51945bede3 100644 --- a/server/src/services/telemetry.service.ts +++ b/server/src/services/telemetry.service.ts @@ -34,10 +34,12 @@ export class TelemetryService extends BaseService { @OnEvent({ name: 'JobSuccess' }) onJobSuccess({ job, response }: ArgOf<'JobSuccess'>) { - if (response && Object.values(JobStatus).includes(response as JobStatus)) { - const jobMetric = `immich.jobs.${snakeCase(job.name)}.${response}`; - this.telemetryRepository.jobs.addToCounter(jobMetric, 1); + if (!(response && Object.values(JobStatus).includes(response as JobStatus))) { + return; } + + const jobMetric = `immich.jobs.${snakeCase(job.name)}.${response}`; + this.telemetryRepository.jobs.addToCounter(jobMetric, 1); } @OnEvent({ name: 'JobError' }) diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index cb6813cf29..d043267745 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -27,7 +27,7 @@ export class TimelineService extends BaseService { private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise { const { userId, ...options } = dto; - let userIds: string[] | undefined = undefined; + let userIds: string[] | undefined; if (userId) { userIds = [userId]; @@ -52,7 +52,7 @@ export class TimelineService extends BaseService { if (dto.albumId) { await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] }); } else { - dto.userId = dto.userId || auth.user.id; + dto.userId ||= auth.user.id; } if (dto.userId) { @@ -74,12 +74,12 @@ export class TimelineService extends BaseService { } if (dto.withPartners) { - const requestedLocked = dto.visibility === AssetVisibility.Locked; - const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined; - const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; - const requestedTrash = dto.isTrashed === true; + const isRequestedLocked = dto.visibility === AssetVisibility.Locked; + const isRequestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined; + const isRequestedFavorite = dto.isFavorite === true || dto.isFavorite === false; + const isRequestedTrash = dto.isTrashed === true; - if (requestedLocked || requestedArchived || requestedFavorite || requestedTrash) { + if (isRequestedLocked || isRequestedArchived || isRequestedFavorite || isRequestedTrash) { throw new BadRequestException( 'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets', ); diff --git a/server/src/services/transcoding.service.spec.ts b/server/src/services/transcoding.service.spec.ts index 68e1700743..5980b7078a 100644 --- a/server/src/services/transcoding.service.spec.ts +++ b/server/src/services/transcoding.service.spec.ts @@ -490,18 +490,15 @@ describe(TranscodingService.name, () => { describe('FFmpeg seek per segment', () => { const eiffelSeeks = [ - 0, 1.987_15, 3.994_372_222_222_222, 6.001_594_444_444_444, 8.008_816_666_666_666, 10.016_038_888_888_888, - 12.023_261_111_111_111, 14.030_483_333_333_333, 16.037_705_555_555_554, 18.044_927_777_777_776, - 20.052_149_999_999_997, 22.059_372_222_222_223, + 0, 1.98715, 3.994372222222222, 6.001594444444444, 8.008816666666666, 10.016038888888888, 12.023261111111111, + 14.030483333333333, 16.037705555555554, 18.044927777777776, 20.052149999999997, 22.059372222222223, ]; const waterfallSeeks = [ - 0, 1.994_642_826_321_467, 4.006_047_357_065_803, 6.017_451_887_810_139_5, 8.028_856_418_554_476, - 10.040_260_949_298_812, + 0, 1.994642826321467, 4.006047357065803, 6.0174518878101395, 8.028856418554476, 10.040260949298812, ]; const trainSeeks = [ - 0, 1.991_666_666_666_666_7, 3.991_666_666_666_666_7, 5.991_666_666_666_666, 7.991_666_666_666_666, - 9.991_666_666_666_667, 11.991_666_666_666_667, 13.991_666_666_666_667, 15.991_666_666_666_667, - 17.991_666_666_666_667, 19.991_666_666_666_667, + 0, 1.9916666666666667, 3.9916666666666667, 5.991666666666666, 7.991666666666666, 9.991666666666667, + 11.991666666666667, 13.991666666666667, 15.991666666666667, 17.991666666666667, 19.991666666666667, ]; const cases = [ ...eiffelSeeks.map((expected, segmentIndex) => ({ diff --git a/server/src/services/transcoding.service.ts b/server/src/services/transcoding.service.ts index c79eec7884..8b083bfaf0 100644 --- a/server/src/services/transcoding.service.ts +++ b/server/src/services/transcoding.service.ts @@ -54,7 +54,7 @@ export class TranscodingService extends BaseService { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } - return Promise.all([...this.sessions.values()].map(({ id }) => this.onSessionEnd({ sessionId: id }))); + return Promise.all(this.sessions.values().map(({ id }) => this.onSessionEnd({ sessionId: id }))); } @OnJob({ name: JobName.HlsSessionCleanup, queue: QueueName.BackgroundTask }) @@ -139,9 +139,9 @@ export class TranscodingService extends BaseService { session.variantIndex ??= variantIndex; session.startSegment ??= segmentIndex; const curSegment = session.lastCompletedSegment === null ? session.startSegment : session.lastCompletedSegment + 1; - const needsRestart = + const isNeedsRestart = session.variantIndex !== variantIndex || segmentIndex < session.startSegment || segmentIndex > curSegment + 1; - if (needsRestart) { + if (isNeedsRestart) { this.stopTranscode(session); session.variantIndex = variantIndex; session.startSegment = segmentIndex; @@ -372,7 +372,7 @@ export class TranscodingService extends BaseService { private removeInactiveSessions() { const cutoff = Date.now() - HLS_INACTIVITY_TIMEOUT_MS; - const inactiveSessions = [...this.sessions.values()].filter((s) => s.lastActivityTime.getTime() < cutoff); + const inactiveSessions = this.sessions.values().filter((s) => s.lastActivityTime.getTime() < cutoff); return Promise.all( inactiveSessions.map(async (session) => { try { diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index c32c11879f..c7a5bc4e02 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -178,19 +178,19 @@ export class UserService extends BaseService { const { licensePublicKey } = this.configRepository.getEnv(); - const clientLicenseValid = this.cryptoRepository.verifySha256( + const isClientLicenseValid = this.cryptoRepository.verifySha256( license.licenseKey, license.activationKey, licensePublicKey.client, ); - const serverLicenseValid = this.cryptoRepository.verifySha256( + const isServerLicenseValid = this.cryptoRepository.verifySha256( license.licenseKey, license.activationKey, licensePublicKey.server, ); - if (!clientLicenseValid && !serverLicenseValid) { + if (!isClientLicenseValid && !isServerLicenseValid) { throw new BadRequestException('Invalid license key'); } @@ -294,12 +294,12 @@ export class UserService extends BaseService { await this.eventRepository.emit('UserDelete', user); } - private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, deleteDelay: number): boolean { + private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, delayUntilDeletion: number): boolean { if (!user.deletedAt) { return false; } - return DateTime.now().minus({ days: deleteDelay }) > DateTime.fromJSDate(user.deletedAt); + return DateTime.now().minus({ days: delayUntilDeletion }) > DateTime.fromJSDate(user.deletedAt); } private async findOrFail(id: string, options: UserFindOptions) { diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 37010db5e7..f1abeedb30 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -17,7 +17,7 @@ const asNotification = ( ): ReleaseEventV1 => { return { // can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483 - isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion.toString(), { + isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion, { includePrerelease: channel === ReleaseChannel.ReleaseCandidate, }), checkedAt, @@ -60,8 +60,8 @@ export class VersionService extends BaseService { this.logger.log(`Adding ${current} to upgrade history`); await this.versionRepository.create({ version: current }); - const needsNewMemories = semver.lt(previousVersion, '1.129.0'); - if (needsNewMemories) { + const isNeedsNewMemories = semver.lt(previousVersion, '1.129.0'); + if (isNeedsNewMemories) { await this.jobRepository.queue({ name: JobName.MemoryGenerate }); } } @@ -115,7 +115,7 @@ export class VersionService extends BaseService { // can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483 if ( - semver.intersects(`>${serverVersion}`, releaseVersion.toString(), { + semver.intersects(`>${serverVersion}`, releaseVersion, { includePrerelease: newVersionCheck.channel === ReleaseChannel.ReleaseCandidate, }) ) { diff --git a/server/src/services/workflow-execution.service.ts b/server/src/services/workflow-execution.service.ts index 372bf04e71..318f36a4b6 100644 --- a/server/src/services/workflow-execution.service.ts +++ b/server/src/services/workflow-execution.service.ts @@ -93,6 +93,7 @@ export class WorkflowExecutionService extends BaseService { for (const pattern of context.allowedHosts) { const regex = new RegExp(pattern.replaceAll('.', String.raw`\.`).replaceAll('*', '.*')); if (regex.test(hostname)) { + // eslint-disable-next-line unicorn/no-invalid-argument-count const res = await fetch(...args); return { @@ -124,8 +125,8 @@ export class WorkflowExecutionService extends BaseService { const plugins = await this.pluginRepository.getForLoad(); for (const { id, name, version, wasmBytes, methods } of plugins) { - const method = methods.some(({ hostFunctions }) => !hostFunctions); - if (method) { + const isMethod = methods.some(({ hostFunctions }) => !hostFunctions); + if (isMethod) { const label = `${name}@${version}`; const key = this.getPluginKey({ id, hostFunctions: false }); try { @@ -136,8 +137,8 @@ export class WorkflowExecutionService extends BaseService { } } - const methodWithFunction = methods.some(({ hostFunctions }) => hostFunctions); - if (methodWithFunction) { + const isMethodWithFunction = methods.some(({ hostFunctions }) => hostFunctions); + if (isMethodWithFunction) { const label = `${name}@${version}/worker`; const key = this.getPluginKey({ id, hostFunctions: true }); try { @@ -381,8 +382,8 @@ export class WorkflowExecutionService extends BaseService { // TODO infer from steps let type: T | undefined; for (const targetType of Object.values(WorkflowType)) { - const missing = workflow.steps.some((step) => !step.types.includes(targetType)); - if (!missing) { + const isMissing = workflow.steps.some((step) => !step.types.includes(targetType)); + if (!isMissing) { type = targetType as unknown as T; break; } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index b8e7733772..ce8e614565 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -3,7 +3,7 @@ import { AuthSharedLink } from 'src/database'; import { AuthDto } from 'src/dtos/auth.dto'; import { AlbumUserRole, Permission } from 'src/enum'; import { AccessRepository } from 'src/repositories/access.repository'; -import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set'; +import { areSetsEqual, isSetSuperset, setDifference, setUnion } from 'src/utils/set'; export type GrantedRequest = { requested: Permission[]; @@ -15,7 +15,7 @@ export const isGranted = ({ requested, current }: GrantedRequest) => { return true; } - return setIsSuperset(new Set(current), new Set(requested)); + return isSetSuperset(new Set(current), new Set(requested)); }; export type AccessRequest = { @@ -36,7 +36,7 @@ export const requireUploadAccess = (auth: AuthDto | null): AuthDto => { export const requireAccess = async (access: AccessRepository, request: AccessRequest) => { const allowedIds = await checkAccess(access, request); - if (!setIsEqual(new Set(request.ids), allowedIds)) { + if (!areSetsEqual(new Set(request.ids), allowedIds)) { throw new BadRequestException(`Not found or no ${request.permission} access`); } }; diff --git a/server/src/utils/config.ts b/server/src/utils/config.ts index 62f37128d8..a6073471d1 100644 --- a/server/src/utils/config.ts +++ b/server/src/utils/config.ts @@ -29,10 +29,12 @@ export const getConfig = async (repos: RepoDeps, { withCache }: { withCache: boo if (!withCache || !config) { const timestamp = lastUpdated; await asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => { - if (timestamp === lastUpdated) { - config = await buildConfig(repos); - lastUpdated = Date.now(); + if (timestamp !== lastUpdated) { + return; } + + config = await buildConfig(repos); + lastUpdated = Date.now(); }); } @@ -45,7 +47,7 @@ export const updateConfig = async (repos: RepoDeps, newConfig: SystemConfig): Pr const partialConfig: DeepPartial = {}; for (const property of getKeysDeep(defaults)) { const newValue = _.get(newConfig, property); - const isEmpty = newValue === undefined || newValue === null || newValue === ''; + const isEmpty = [undefined, null, ''].includes(newValue); const defaultValue = _.get(defaults, property); const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue); @@ -64,7 +66,7 @@ export const updateConfig = async (repos: RepoDeps, newConfig: SystemConfig): Pr const loadFromFile = async ({ metadataRepo, logger }: RepoDeps, filepath: string) => { try { const file = await metadataRepo.readFile(filepath); - return loadYaml(file.toString()) as unknown; + return loadYaml(file) as unknown; } catch (error: Error | any) { logger.error(`Unable to load configuration file: ${filepath}`); logger.error(error); @@ -107,9 +109,8 @@ const buildConfig = async (repos: RepoDeps) => { } if (configFile) { throw new Error(messages.join('\n')); - } else { - logger.error('Validation error', messages); } + logger.error('Validation error', messages); } const config = (result.success ? result.data : rawConfig) as SystemConfig; @@ -117,10 +118,10 @@ const buildConfig = async (repos: RepoDeps) => { if (config.server.externalDomain.length > 0) { const domain = new URL(config.server.externalDomain); - let externalDomain = domain.origin; - if (domain.password && domain.username) { - externalDomain = `${domain.protocol}//${domain.username}:${domain.password}@${domain.host}`; - } + const externalDomain = + domain.password && domain.username + ? `${domain.protocol}//${domain.username}:${domain.password}@${domain.host}` + : domain.origin; config.server.externalDomain = externalDomain; } diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 5c2c0da162..ffd5a603e4 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -36,18 +36,20 @@ export const getKyselyConfig = (connection: DatabaseConnectionParams): KyselyCon }), }), log(event) { - if (event.level === 'error') { - if (isAssetChecksumConstraint(event.error)) { - return; - } - - console.error('Query failed :', { - durationMs: event.queryDurationMillis, - error: event.error, - sql: event.query.sql, - params: event.query.parameters, - }); + if (event.level !== 'error') { + return; } + + if (isAssetChecksumConstraint(event.error)) { + return; + } + + console.error('Query failed :', { + durationMs: event.queryDurationMillis, + error: event.error, + sql: event.query.sql, + params: event.query.parameters, + }); }, }; }; @@ -523,6 +525,6 @@ export function vectorIndexQuery({ vectorExtension, table, indexName, lists }: V export const updateLockedColumns = & { lockedProperties?: LockableProperty[] }>( exif: T, ) => { - exif.lockedProperties = lockableProperties.filter((property) => property in exif); + exif.lockedProperties = lockableProperties.filter((property) => Object.hasOwn(exif, property)); return exif; }; diff --git a/server/src/utils/fetch.spec.ts b/server/src/utils/fetch.spec.ts index 2189e6f626..4cd532e889 100644 --- a/server/src/utils/fetch.spec.ts +++ b/server/src/utils/fetch.spec.ts @@ -4,15 +4,14 @@ import { configureUserAgent } from 'src/utils/fetch'; describe('fetch', () => { it('should set the default user-agent header', async () => { const spy = vi.fn().mockResolvedValue(new Response()); - const original = globalThis.fetch; - globalThis.fetch = spy; + vi.stubGlobal('fetch', spy); configureUserAgent(); - await globalThis.fetch('http://test.local'); + await fetch('https://test.local'); const headers: Headers = spy.mock.calls[0][1].headers; expect(headers.get('User-Agent')).toBe(`immich-server/${serverVersion}`); - globalThis.fetch = original; + vi.unstubAllGlobals(); }); }); diff --git a/server/src/utils/fetch.ts b/server/src/utils/fetch.ts index e753b29725..33bd087907 100644 --- a/server/src/utils/fetch.ts +++ b/server/src/utils/fetch.ts @@ -1,7 +1,8 @@ import { serverVersion } from 'src/constants'; export function configureUserAgent() { - const originalFetch = globalThis.fetch; + const originalFetch = fetch; + // eslint-disable-next-line unicorn/no-global-object-property-assignment globalThis.fetch = (input, init) => { const headers = new Headers(init?.headers); if (!headers.has('User-Agent')) { diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts index edd411dd40..c77f7bb7a7 100644 --- a/server/src/utils/file.ts +++ b/server/src/utils/file.ts @@ -74,7 +74,7 @@ export const sendFile = async ( } // log non-http errors - if (error instanceof HttpException === false) { + if (!(error instanceof HttpException)) { logger.error(`Unable to send file: ${error}`, error.stack); } diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index df321f24c2..bfb0202469 100644 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -2,7 +2,7 @@ import { HttpException } from '@nestjs/common'; import { Request } from 'express'; import { LoggingRepository } from 'src/repositories/logging.repository'; -const isRequestAborted = (request: Request) => request.destroyed === true && request.complete === false; +const isRequestAborted = (request: Request) => request.destroyed && !request.complete; export const isHttpException = (error: Error): error is HttpException => error instanceof HttpException; export const onRequestError = (req: Request, error: Error, logger: LoggingRepository) => { diff --git a/server/src/utils/maintenance.ts b/server/src/utils/maintenance.ts index 44b98f3f75..d970b5ebf2 100644 --- a/server/src/utils/maintenance.ts +++ b/server/src/utils/maintenance.ts @@ -38,23 +38,23 @@ export async function detectPriorInstall( const files = await storageRepository.readdir(path); const filename = join(StorageCore.getBaseFolder(folder), '.immich'); - let readable = false, - writable = false; + let isReadable = false, + isWritable = false; try { await storageRepository.readFile(filename); - readable = true; + isReadable = true; - await storageRepository.overwriteFile(filename, Buffer.from(`${Date.now()}`)); - writable = true; + await storageRepository.overwriteFile(filename, Buffer.from(Date.now().toString())); + isWritable = true; } catch { // no-op } return { folder, - readable, - writable, + readable: isReadable, + writable: isWritable, files: files.filter((fn) => fn !== '.immich').length, }; }), diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index aceb03baa4..aea821d6d5 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -68,9 +68,9 @@ export class BaseConfig implements VideoCodecSWConfig { static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces, tune?: VideoTuning) { if (config.accel === TranscodeHardwareAcceleration.Disabled) { - return this.getSWCodecConfig(config, tune); + return BaseConfig.getSWCodecConfig(config, tune); } - return this.getHWCodecConfig(config, interfaces, tune); + return BaseConfig.getHWCodecConfig(config, interfaces, tune); } private static getSWCodecConfig(config: SystemConfigFFmpegDto, tune?: VideoTuning): VideoCodecSWConfig { @@ -230,15 +230,15 @@ export class BaseConfig implements VideoCodecSWConfig { } } if (this.getBFrames() > -1) { - options.push('-bf', `${this.getBFrames()}`); + options.push('-bf', String(this.getBFrames())); } if (this.getRefs() > 0) { - options.push('-refs', `${this.getRefs()}`); + options.push('-refs', String(this.getRefs())); } if (this.getGopSize() > 0) { - options.push('-g', `${this.getGopSize()}`); + options.push('-g', String(this.getGopSize())); if (this.tune.strictGop) { - options.push('-keyint_min', `${this.getGopSize()}`); + options.push('-keyint_min', String(this.getGopSize())); } } const isHvc = @@ -285,19 +285,19 @@ export class BaseConfig implements VideoCodecSWConfig { '-maxrate', `${bitrates.max}${bitrates.unit}`, ]; - } else if (bitrates.max > 0) { + } + if (bitrates.max > 0) { // -bufsize is the peak possible bitrate at any moment, while -maxrate is the max rolling average bitrate return [ `-${this.useCQP() ? 'q:v' : 'crf'}`, - `${this.config.crf}`, + String(this.config.crf), '-maxrate', `${bitrates.max}${bitrates.unit}`, '-bufsize', `${bitrates.max * 2}${bitrates.unit}`, ]; - } else { - return [`-${this.useCQP() ? 'q:v' : 'crf'}`, `${this.config.crf}`]; } + return [`-${this.useCQP() ? 'q:v' : 'crf'}`, String(this.config.crf)]; } getInputThreadOptions(): Array { @@ -308,7 +308,7 @@ export class BaseConfig implements VideoCodecSWConfig { if (this.config.threads <= 0) { return []; } - return ['-threads', `${this.config.threads}`]; + return ['-threads', String(this.config.threads)]; } eligibleForTwoPass() { @@ -343,9 +343,9 @@ export class BaseConfig implements VideoCodecSWConfig { } shouldScale(videoStream: VideoStreamInfo) { - const oddDimensions = videoStream.height % 2 !== 0 || videoStream.width % 2 !== 0; - const largerThanTarget = Math.min(videoStream.height, videoStream.width) > this.getTargetResolution(videoStream); - return oddDimensions || largerThanTarget; + const isOddDimensions = videoStream.height % 2 !== 0 || videoStream.width % 2 !== 0; + const isLargerThanTarget = Math.min(videoStream.height, videoStream.width) > this.getTargetResolution(videoStream); + return isOddDimensions || isLargerThanTarget; } shouldToneMap(videoStream: VideoStreamInfo) { @@ -569,7 +569,7 @@ export class VP9Config extends BaseConfig { getPresetOptions() { const speed = Math.min(this.getPresetIndex(), 5); // values over 5 require realtime mode, which is its own can of worms since it overrides -crf and -threads if (speed >= 0) { - return ['-cpu-used', `${speed}`]; + return ['-cpu-used', String(speed)]; } return []; } @@ -587,7 +587,7 @@ export class VP9Config extends BaseConfig { ]; } - return [`-${this.useCQP() ? 'q:v' : 'crf'}`, `${this.config.crf}`, '-b:v', `${bitrates.max}${bitrates.unit}`]; + return [`-${this.useCQP() ? 'q:v' : 'crf'}`, String(this.config.crf), '-b:v', `${bitrates.max}${bitrates.unit}`]; } getEncoderOptions(): string[] { @@ -607,13 +607,13 @@ export class AV1Config extends BaseConfig { getPresetOptions() { const speed = this.getPresetIndex() + 4; // Use 4 as slowest, giving us an effective range of 4-12 which is far more useful than 0-8 if (speed >= 0) { - return ['-preset', `${speed}`]; + return ['-preset', String(speed)]; } return []; } getBitrateOptions() { - return ['-crf', `${this.config.crf}`]; + return ['-crf', String(this.config.crf)]; } getEncoderOptions(): string[] { @@ -699,18 +699,17 @@ export class NvencSwDecodeConfig extends BaseHWConfig { '-multipass', '2', ]; - } else if (bitrates.max > 0) { - return [ - '-cq:v', - `${this.config.crf}`, - '-maxrate', - `${bitrates.max}${bitrates.unit}`, - '-bufsize', - `${bitrates.target}${bitrates.unit}`, - ]; - } else { - return ['-cq:v', `${this.config.crf}`]; } + return bitrates.max > 0 + ? [ + '-cq:v', + String(this.config.crf), + '-maxrate', + `${bitrates.max}${bitrates.unit}`, + '-bufsize', + `${bitrates.target}${bitrates.unit}`, + ] + : ['-cq:v', String(this.config.crf)]; } getThreadOptions() { @@ -810,11 +809,11 @@ export class QsvSwDecodeConfig extends BaseHWConfig { return []; } presetIndex = Math.min(6, presetIndex) + 1; // 1 to 7 - return ['-preset', `${presetIndex}`]; + return ['-preset', String(presetIndex)]; } getBitrateOptions() { - const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'}`, `${this.config.crf}`]; + const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'}`, String(this.config.crf)]; const bitrates = this.getBitrateDistribution(); if (bitrates.max > 0) { // Workaround for https://github.com/immich-app/immich/issues/29220, to be revisited @@ -939,7 +938,7 @@ export class VaapiSwDecodeConfig extends BaseHWConfig { return []; } presetIndex = Math.min(6, presetIndex) + 1; // 1 to 7 - return ['-compression_level', `${presetIndex}`]; + return ['-compression_level', String(presetIndex)]; } getBitrateOptions() { @@ -963,9 +962,9 @@ export class VaapiSwDecodeConfig extends BaseHWConfig { '3', ); // variable bitrate } else if (this.useCQP()) { - options.push('-qp:v', `${this.config.crf}`, '-global_quality:v', `${this.config.crf}`, '-rc_mode', '1'); + options.push('-qp:v', String(this.config.crf), '-global_quality:v', String(this.config.crf), '-rc_mode', '1'); } else { - options.push('-global_quality:v', `${this.config.crf}`, '-rc_mode', '4'); + options.push('-global_quality:v', String(this.config.crf), '-rc_mode', '4'); } return options; @@ -1072,7 +1071,7 @@ export class RkmppSwDecodeConfig extends BaseHWConfig { return ['-rc_mode', 'AVBR', '-b:v', `${bitrate}${this.getBitrateUnit()}`]; } // use CRF value as QP value - return ['-rc_mode', 'CQP', '-qp_init', `${this.config.crf}`]; + return ['-rc_mode', 'CQP', '-qp_init', String(this.config.crf)]; } getVideoCodec(): string { @@ -1106,7 +1105,8 @@ export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig { `tonemapx=tonemap=${this.config.tonemap}:desat=0:p=${primaries}:t=${transfer}:m=${matrix}:r=pc:peak=100:format=yuv420p`, 'hwupload', ]; - } else if (this.shouldScale(videoStream)) { + } + if (this.shouldScale(videoStream)) { return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1:async_depth=4`]; } return []; diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index 346124df32..43e7f0565a 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -132,7 +132,8 @@ const sidecar: Record = { const types = { ...image, ...video, ...sidecar }; -const isType = (filename: string, r: Record) => extname(filename).toLowerCase() in r; +const isType = (filename: string, record: Record) => + Object.hasOwn(record, extname(filename).toLowerCase()); const lookup = (filename: string) => types[extname(filename).toLowerCase()]?.[0] ?? 'application/octet-stream'; const toExtension = (mimeType: string) => { diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index efcb509941..0e7a9e6cdc 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -152,11 +152,7 @@ export const routeToErrorMessage = (methodName: string) => 'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`); const isSchema = (schema: string | ReferenceObject | SchemaObject): schema is SchemaObject => { - if (typeof schema === 'string' || '$ref' in schema) { - return false; - } - - return true; + return !(typeof schema === 'string' || '$ref' in schema); }; const patchOpenAPI = (document: OpenAPIObject) => { @@ -195,20 +191,22 @@ const patchOpenAPI = (document: OpenAPIObject) => { document.components.schemas = sortKeys(schemas); for (const [schemaName, schema] of Object.entries(schemas)) { - if (schema.properties) { - schema.properties = sortKeys(schema.properties); - - for (const [key, value] of Object.entries(schema.properties)) { - if (typeof value === 'string') { - continue; - } - - if (isSchema(value) && value.type === 'number' && value.format === 'float') { - throw new Error(`Invalid number format: ${schemaName}.${key}=float (use double instead). `); - } - } - schema.required?.sort(); + if (!schema.properties) { + continue; } + + schema.properties = sortKeys(schema.properties); + + for (const [key, value] of Object.entries(schema.properties)) { + if (typeof value === 'string') { + continue; + } + + if (isSchema(value) && value.type === 'number' && value.format === 'float') { + throw new Error(`Invalid number format: ${schemaName}.${key}=float (use double instead). `); + } + } + schema.required?.sort(); } } diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index bfc17b291c..9cb294727c 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -72,7 +72,7 @@ export const getPreferencesPartial = (newPreferences: UserPreferences) => { const partial: DeepPartial = {}; for (const property of getKeysDeep(defaultPreferences)) { const newValue = _.get(newPreferences, property); - const isEmpty = newValue === undefined || newValue === null || newValue === ''; + const isEmpty = [undefined, null, ''].includes(newValue); const defaultValue = _.get(defaultPreferences, property); const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue); diff --git a/server/src/utils/set.ts b/server/src/utils/set.ts index 971d3f7e5d..cc6ded93cc 100644 --- a/server/src/utils/set.ts +++ b/server/src/utils/set.ts @@ -22,7 +22,7 @@ export const setDifference = (setA: Set, ...sets: Set[]): Set => { return difference; }; -export const setIsSuperset = (set: Set, subset: Set): boolean => { +export const isSetSuperset = (set: Set, subset: Set): boolean => { for (const element of subset) { if (!set.has(element)) { return false; @@ -31,6 +31,6 @@ export const setIsSuperset = (set: Set, subset: Set): boolean => { return true; }; -export const setIsEqual = (setA: Set, setB: Set): boolean => { - return setA.size === setB.size && setIsSuperset(setA, setB); +export const areSetsEqual = (setA: Set, setB: Set): boolean => { + return setA.size === setB.size && isSetSuperset(setA, setB); }; diff --git a/server/src/utils/transform.ts b/server/src/utils/transform.ts index 1c849322a1..aa1fe8b7fc 100644 --- a/server/src/utils/transform.ts +++ b/server/src/utils/transform.ts @@ -16,11 +16,13 @@ export const getOutputDimensions = ( } for (const edit of edits) { - if (edit.action === AssetEditAction.Rotate) { - const angleDegrees = edit.parameters.angle; - if (angleDegrees === 90 || angleDegrees === 270) { - [width, height] = [height, width]; - } + if (edit.action !== AssetEditAction.Rotate) { + continue; + } + + const angleDegrees = edit.parameters.angle; + if (angleDegrees === 90 || angleDegrees === 270) { + [width, height] = [height, width]; } } diff --git a/server/src/validation.ts b/server/src/validation.ts index 79d249b1c9..7188de1bed 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -12,10 +12,7 @@ function isIPOrRange(value: string, options?: IsIPRangeOptions): boolean { if (isIPRange(value)) { return true; } - if (!requireCIDR && isIP(value)) { - return true; - } - return false; + return !requireCIDR && isIP(value); } /** @@ -63,7 +60,7 @@ export function IsNotSiblingOf< TKey extends z.infer> & keyof z.infer, >(_schema: TSchema, property: TKey, siblings: TKey[]) { type T = z.infer; - const message = `${String(property)} cannot exist alongside ${siblings.join(' or ')}`; + const message = `${property} cannot exist alongside ${siblings.join(' or ')}`; return z.custom().refine( (data) => { if (data[property] === undefined) { diff --git a/server/test/factories/asset-exif.factory.ts b/server/test/factories/asset-exif.factory.ts index da4d689ebf..ada83d5099 100644 --- a/server/test/factories/asset-exif.factory.ts +++ b/server/test/factories/asset-exif.factory.ts @@ -30,8 +30,8 @@ export class AssetExifFactory { focalLength: 4.38, fps: null, iso: 947, - latitude: 30.267_334_570_570_195, - longitude: -97.789_833_534_282_07, + latitude: 30.267334570570195, + longitude: -97.78983353428207, lensModel: null, livePhotoCID: null, make: 'Google', diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts index 7edd1c2dee..77534fd6cb 100644 --- a/server/test/fixtures/media.stub.ts +++ b/server/test/fixtures/media.stub.ts @@ -502,7 +502,7 @@ export const eiffelTower = { profile: H264Profile.High, level: 40, frameCount: 557, - frameRate: 24.908_004_845_459_07, + frameRate: 24.90800484545907, timeBase: 90_000, bitrate: 5_128_622, pixelFormat: 'yuv420p', @@ -541,7 +541,7 @@ export const waterfall = { profile: HevcProfile.Main, level: 156, frameCount: 309, - frameRate: 29.829_901_982_867_92, + frameRate: 29.82990198286792, timeBase: 90_000, bitrate: 43_363_499, pixelFormat: 'yuvj420p', @@ -582,7 +582,7 @@ export const train = { profile: HevcProfile.Main10, level: 123, frameCount: 1229, - frameRate: 56.536_072_989_342_94, + frameRate: 56.53607298934294, timeBase: 600, bitrate: 12_595_191, pixelFormat: 'yuv420p10le', diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 7a3adc0437..c29bdf631a 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -1,3 +1,4 @@ +import { DateTime } from 'luxon'; import { MapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkType } from 'src/enum'; import { AssetFactory } from 'test/factories/asset.factory'; @@ -5,10 +6,8 @@ import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; const today = new Date(); -const tomorrow = new Date(); -const yesterday = new Date(); -tomorrow.setDate(today.getDate() + 1); -yesterday.setDate(yesterday.getDate() - 1); +const tomorrow = DateTime.now().plus({ days: 1 }).toJSDate(); +const yesterday = DateTime.now().minus({ days: 1 }).toJSDate(); const sharedLinkBytes = Buffer.from( '2c2b646895f84753bff43fb696ad124f3b0faf2a0bd547406f26fa4a76b5c71990092baa536275654b2ab7a191fb21a6d6cd', diff --git a/server/test/mappers.ts b/server/test/mappers.ts index 6ee7e52ac6..40ae78fe26 100644 --- a/server/test/mappers.ts +++ b/server/test/mappers.ts @@ -68,7 +68,6 @@ export const getDehydrated = >(entity: T) => { for (const [key, value] of Object.entries(copiedEntity)) { if (value instanceof Date) { Object.assign(copiedEntity, { [key]: value.toISOString() }); - continue; } } diff --git a/server/test/medium.factory.ts b/server/test/medium.factory.ts index 599cfe7cc2..4bbe8573fe 100644 --- a/server/test/medium.factory.ts +++ b/server/test/medium.factory.ts @@ -138,7 +138,7 @@ export class MediumTestContext { } get(key: ClassConstructor): T { - if (!this.repoCache[key.name]) { + if (!Object.hasOwn(this.repoCache, key.name)) { const real = newRealRepository(key, this.options.database); this.repoCache[key.name] = real; } @@ -313,11 +313,11 @@ export class SyncTestContext extends MediumTestContext { }); } - async syncStream(auth: AuthDto, types: SyncRequestType[], reset?: boolean) { + async syncStream(auth: AuthDto, types: SyncRequestType[], shouldReset?: boolean) { const stream = mediumFactory.syncStream(); // Wait for 2ms to ensure all updates are available and account for setTimeout inaccuracy await wait(2); - await this.sut.stream(auth, stream, { types, reset }); + await this.sut.stream(auth, stream, { types, reset: shouldReset }); return stream.getResponse(); } @@ -750,6 +750,8 @@ const tagInsert = (tag: Partial>) => { class CustomWritable extends Writable { private data = ''; + // determined by Writable interface + // eslint-disable-next-line unicorn/prefer-private-class-fields _write(chunk: any, encoding: string, callback: () => void) { this.data += chunk.toString(); callback(); diff --git a/server/test/medium/specs/repositories/asset-job.repository.spec.ts b/server/test/medium/specs/repositories/asset-job.repository.spec.ts index 023ed8774e..7b36958661 100644 --- a/server/test/medium/specs/repositories/asset-job.repository.spec.ts +++ b/server/test/medium/specs/repositories/asset-job.repository.spec.ts @@ -8,11 +8,7 @@ import { newMediumService } from 'test/medium.factory'; import { getKyselyDB } from 'test/utils'; const consume = async (generator: AsyncIterableIterator) => { - const values: T[] = []; - - for await (const value of generator) { - values.push(value); - } + const values: T[] = await Array.fromAsync(generator); return values; }; diff --git a/server/test/medium/specs/services/timeline.service.spec.ts b/server/test/medium/specs/services/timeline.service.spec.ts index d7d3286733..032305c262 100644 --- a/server/test/medium/specs/services/timeline.service.spec.ts +++ b/server/test/medium/specs/services/timeline.service.spec.ts @@ -184,6 +184,7 @@ describe(TimelineService.name, () => { await ctx.newExif({ assetId: result.asset.id, make: 'Canon' }); return result; }), + ctx.newUser().then(async ({ user }) => { const result = await ctx.newAsset({ ownerId: user.id, diff --git a/server/test/medium/specs/services/user.service.spec.ts b/server/test/medium/specs/services/user.service.spec.ts index c8c990a8da..2eac06d674 100644 --- a/server/test/medium/specs/services/user.service.spec.ts +++ b/server/test/medium/specs/services/user.service.spec.ts @@ -126,8 +126,8 @@ describe(UserService.name, () => { const after = await sut.setLicense(auth, license); expect(after.licenseKey).toEqual(license.licenseKey); expect(after.activationKey).toEqual(license.activationKey); - const getResponse = await sut.getLicense(auth); - expect(getResponse).toEqual(after); + const response = await sut.getLicense(auth); + expect(response).toEqual(after); }); }); diff --git a/server/test/medium/specs/sync/sync-types.spec.ts b/server/test/medium/specs/sync/sync-types.spec.ts index 737db068ba..24eadf70c8 100644 --- a/server/test/medium/specs/sync/sync-types.spec.ts +++ b/server/test/medium/specs/sync/sync-types.spec.ts @@ -4,7 +4,7 @@ import { SYNC_TYPES_ORDER } from 'src/services/sync.service'; describe('types', () => { it('should have all the types in the ordering variable', () => { for (const key in SyncRequestType) { - expect(SYNC_TYPES_ORDER).includes(key); + expect(SYNC_TYPES_ORDER.includes(key as SyncRequestType)).toBe(true); } expect(SYNC_TYPES_ORDER.length).toBe(Object.keys(SyncRequestType).length); diff --git a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts index 9721e4cb8f..a9a9e174e5 100644 --- a/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts +++ b/server/test/medium/specs/workflow/workflow-core-plugin.spec.ts @@ -21,7 +21,7 @@ import { MediumTestContext } from 'test/medium.factory'; import { mockEnvData } from 'test/repositories/config.repository.mock'; import { getKyselyDB } from 'test/utils'; -let initialized = false; +let isInitialized = false; class WorkflowTestContext extends MediumTestContext { constructor(database: Kysely) { @@ -44,7 +44,7 @@ class WorkflowTestContext extends MediumTestContext { } async init() { - if (initialized) { + if (isInitialized) { return; } @@ -57,7 +57,7 @@ class WorkflowTestContext extends MediumTestContext { await this.sut.onPluginSync(); await this.sut.onPluginLoad(); - initialized = true; + isInitialized = true; } } @@ -337,7 +337,7 @@ describe('core plugin', () => { it('should favorite an asset within a given radius', async () => { const { user } = await ctx.newUser(); const { asset } = await ctx.newAsset({ ownerId: user.id }); - await ctx.newExif({ assetId: asset.id, latitude: 49.273_353_221_145_36, longitude: -123.103_871_440_787_64 }); + await ctx.newExif({ assetId: asset.id, latitude: 49.27335322114536, longitude: -123.10387144078764 }); const workflow = await createWorkflow({ ownerId: user.id, @@ -345,7 +345,7 @@ describe('core plugin', () => { steps: [ { method: 'immich-plugin-core#assetLocationFilter', - config: { coordinate: { latitude: 49.288_821_679_949_29, longitude: -123.111_153_098_813_7, radius: 2 } }, + config: { coordinate: { latitude: 49.28882167994929, longitude: -123.1111530988137, radius: 2 } }, }, { method: 'immich-plugin-core#assetFavorite', @@ -360,7 +360,7 @@ describe('core plugin', () => { it('should not favorite asset outside a given radius', async () => { const { user } = await ctx.newUser(); const { asset } = await ctx.newAsset({ ownerId: user.id }); - await ctx.newExif({ assetId: asset.id, latitude: 49.261_266_052_570_35, longitude: -123.248_959_390_781_96 }); + await ctx.newExif({ assetId: asset.id, latitude: 49.26126605257035, longitude: -123.24895939078196 }); const workflow = await createWorkflow({ ownerId: user.id, @@ -368,7 +368,7 @@ describe('core plugin', () => { steps: [ { method: 'immich-plugin-core#assetLocationFilter', - config: { coordinate: { latitude: 49.288_821_679_949_29, longitude: -123.111_153_098_813_7, radius: 10 } }, + config: { coordinate: { latitude: 49.28882167994929, longitude: -123.1111530988137, radius: 10 } }, }, { method: 'immich-plugin-core#assetFavorite', diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index f05826a424..f810793ce8 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -13,7 +13,7 @@ export const makeMockWatcher = ({ items, close }: MockWatcherOptions) => (paths: string[], options: ChokidarOptions, events: Partial) => { events.onReady?.(); - for (const item of items || []) { + for (const item of items ?? []) { switch (item.event) { case 'add': { events.onAdd?.(item.value); diff --git a/server/test/small.factory.ts b/server/test/small.factory.ts index 42b7333dd0..1c17d955a9 100644 --- a/server/test/small.factory.ts +++ b/server/test/small.factory.ts @@ -6,17 +6,12 @@ import { v4, v7 } from 'uuid'; import { expect } from 'vitest'; export const newUuid = () => v4(); -export const newUuids = () => - Array.from({ length: 100 }) - .fill(0) - .map(() => newUuid()); +export const newUuids = () => Array.from({ length: 100 }, () => 0).map(() => newUuid()); export const newDate = () => new Date(); export const newUuidV7 = () => v7(); export const newSha1 = () => Buffer.from('this is a fake hash'); export const newEmbedding = () => { - const embedding = Array.from({ length: 512 }) - .fill(0) - .map(() => Math.random()); + const embedding = Array.from({ length: 512 }, () => 0).map(() => Math.random()); return '[' + embedding + ']'; }; diff --git a/server/test/utils.ts b/server/test/utils.ts index 1d87b71a2e..b633cbc4de 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,3 +1,4 @@ +/* eslint-disable unicorn/no-this-outside-of-class */ import { createPostgres, DatabaseConnectionParams } from '@immich/sql-tools'; import { CallHandler, ExecutionContext, Provider } from '@nestjs/common'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; @@ -107,6 +108,7 @@ export const controllerSetup = async (controller: new (...args: any[]) => unknow await new Promise((resolve, reject) => { const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve()); const maybePromise = handler(context.getRequest(), context.getResponse(), next); + Promise.resolve(maybePromise).catch((error) => reject(error)); }); @@ -176,7 +178,7 @@ export const automock = ( }, ): AutoMocked => { const mock: Record = {}; - const strict = options?.strict ?? true; + const isStrict = options?.strict ?? true; const args = options?.args ?? []; const mocks: Mock[] = []; @@ -197,7 +199,7 @@ export const automock = ( const target = instance[property as keyof T]; if (typeof target === 'function') { - const mockImplementation = mockFn(label, { strict }); + const mockImplementation = mockFn(label, { strict: isStrict }); mock[property] = mockImplementation; mocks.push(mockImplementation); continue; @@ -454,7 +456,7 @@ const pngFactory = newPngFactory(); const templateName = 'mich'; -const withDatabase = (url: string, name: string) => url.replace(`/${templateName}`, `/${name}`); +const withDatabase = (url: string, name: string) => url.replace(`/${templateName}`, () => `/${name}`); export const getKyselyDB = async (suffix?: string): Promise> => { const testUrl = process.env.IMMICH_TEST_POSTGRES_URL!; diff --git a/server/tsconfig.json b/server/tsconfig.json index c3aede3e5b..18ad4305f5 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -10,7 +10,7 @@ "resolveJsonModule": true, "target": "es2024", "moduleResolution": "node16", - "lib": ["dom", "es2024"], + "lib": ["dom", "esnext"], "sourceMap": true, "outDir": "./dist", "incremental": true, diff --git a/web/eslint.config.js b/web/eslint.config.js index 8fc49581a9..012a74b4fa 100644 --- a/web/eslint.config.js +++ b/web/eslint.config.js @@ -42,7 +42,7 @@ export default typescriptEslint.config( tsconfigRootDir: __dirname, }, }, - ignores: ['**/service-worker/**'], + // ignores: ['**/service-worker/**'], }, { plugins: { @@ -116,7 +116,7 @@ export default typescriptEslint.config( 'unicorn/no-useless-undefined': 'off', 'unicorn/prefer-spread': 'off', 'unicorn/no-null': 'off', - 'unicorn/prevent-abbreviations': 'off', + 'unicorn/name-replacements': 'off', 'unicorn/no-nested-ternary': 'off', 'unicorn/consistent-function-scoping': 'off', 'unicorn/filename-case': 'off', @@ -124,12 +124,37 @@ export default typescriptEslint.config( 'unicorn/import-style': 'off', 'unicorn/no-array-sort': 'off', 'unicorn/no-for-loop': 'off', - 'svelte/button-has-type': 'error', + 'unicorn/no-unreadable-for-of-expression': 'off', + 'unicorn/no-break-in-nested-loop': 'off', + 'unicorn/no-top-level-assignment-in-function': 'off', + 'unicorn/prefer-uint8array-base64': 'off', + 'unicorn/max-nested-calls': 'off', + 'unicorn/no-declarations-before-early-exit': 'off', + 'unicorn/no-unreadable-object-destructuring': 'off', + // not yet compatible with all our supported browsers + 'unicorn/prefer-promise-with-resolvers': 'off', + // not yet compatible with all our supported browsers + 'unicorn/prefer-iterator-to-array': 'off', + // not yet compatible with all our supported browsers + 'unicorn/prefer-array-from-async': 'off', + // maybe we do want to enable this later. TBD + 'unicorn/prefer-await': 'off', + 'unicorn/consistent-class-member-order': 'off', + 'unicorn/class-reference-in-static-methods': ['error', { preferThis: false, preferSuper: false }], + 'unicorn/no-unsafe-property-key': 'off', + 'unicorn/consistent-boolean-name': 'off', + 'unicorn/no-non-function-verb-prefix': 'off', + 'unicorn/prefer-minimal-ternary': 'off', + 'unicorn/no-empty-file': 'off', + // prefer the typescript-eslint type-aware version + 'unicorn/require-array-sort-compare': 'off', + '@typescript-eslint/require-array-sort-compare': 'error', '@typescript-eslint/await-thenable': 'error', '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/require-await': 'error', '@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }], + 'svelte/button-has-type': 'error', 'object-shorthand': ['error', 'always'], 'svelte/no-navigation-without-resolve': 'off', }, diff --git a/web/package.json b/web/package.json index f058f9622e..1899473664 100644 --- a/web/package.json +++ b/web/package.json @@ -97,7 +97,7 @@ "eslint-plugin-better-tailwindcss": "^4.5.0", "eslint-plugin-compat": "^7.0.0", "eslint-plugin-svelte": "^3.12.4", - "eslint-plugin-unicorn": "^64.0.0", + "eslint-plugin-unicorn": "^70.0.0", "factory.ts": "^1.4.1", "globals": "^17.0.0", "happy-dom": "^20.0.0", diff --git a/web/src/hooks.server.ts b/web/src/hooks.server.ts index 2fdae246e1..17b94eda1c 100644 --- a/web/src/hooks.server.ts +++ b/web/src/hooks.server.ts @@ -6,7 +6,7 @@ import GoogleSansCode from '$lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf? export const handle = (async ({ event, resolve }) => { return resolve(event, { transformPageChunk: ({ html }) => { - return html.replace('%app.font%', GoogleSans).replace('%app.monofont%', GoogleSansCode); + return html.replace('%app.font%', () => GoogleSans).replace('%app.monofont%', () => GoogleSansCode); }, }); }) satisfies Handle; diff --git a/web/src/lib/__mocks__/jsdom-url.mock.ts b/web/src/lib/__mocks__/jsdom-url.mock.ts index 5409cc6090..d65da98141 100644 --- a/web/src/lib/__mocks__/jsdom-url.mock.ts +++ b/web/src/lib/__mocks__/jsdom-url.mock.ts @@ -1,5 +1,6 @@ const createObjectURLMock = vi.fn(); +// eslint-disable-next-line unicorn/no-top-level-side-effects Object.defineProperty(URL, 'createObjectURL', { writable: true, value: createObjectURLMock, diff --git a/web/src/lib/__mocks__/sdk.mock.ts b/web/src/lib/__mocks__/sdk.mock.ts index bd94fdf021..b8bac3fc39 100644 --- a/web/src/lib/__mocks__/sdk.mock.ts +++ b/web/src/lib/__mocks__/sdk.mock.ts @@ -1,6 +1,7 @@ import * as sdk from '@immich/sdk'; import type { Mock, MockedObject } from 'vitest'; +// eslint-disable-next-line unicorn/no-top-level-side-effects vi.mock('@immich/sdk', async (originalImport) => { const module = await originalImport(); diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts index e1c95086dd..af4ec86956 100644 --- a/web/src/lib/actions/click-outside.ts +++ b/web/src/lib/actions/click-outside.ts @@ -35,8 +35,8 @@ export function clickOutside(node: HTMLElement, options: Options = {}): ActionRe } }; - document.addEventListener('mousedown', handleClick, false); - node.addEventListener('keydown', handleKey, false); + document.addEventListener('mousedown', handleClick, { capture: false }); + node.addEventListener('keydown', handleKey, { capture: false }); return { destroy() { diff --git a/web/src/lib/actions/drag-and-drop.ts b/web/src/lib/actions/drag-and-drop.ts deleted file mode 100644 index 04de6d9744..0000000000 --- a/web/src/lib/actions/drag-and-drop.ts +++ /dev/null @@ -1,118 +0,0 @@ -export interface DragAndDropOptions { - index: number; - onDragStart?: (index: number) => void; - onDragEnter?: (index: number) => void; - onDrop?: (e: DragEvent, index: number) => void; - onDragEnd?: () => void; - isDragging?: boolean; - isDragOver?: boolean; -} - -export function dragAndDrop(node: HTMLElement, options: DragAndDropOptions) { - let { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options; - - const isFormElement = (element: HTMLElement) => { - return element.tagName === 'INPUT' || element.tagName === 'TEXTAREA' || element.tagName === 'SELECT'; - }; - - const handleDragStart = (e: DragEvent) => { - // Prevent drag if it originated from an input, textarea, or select element - const target = e.target as HTMLElement; - if (isFormElement(target)) { - e.preventDefault(); - return; - } - onDragStart?.(index); - }; - - const handleDragEnter = () => { - onDragEnter?.(index); - }; - - const handleDragOver = (e: DragEvent) => { - e.preventDefault(); - }; - - const handleDrop = (e: DragEvent) => { - onDrop?.(e, index); - }; - - const handleDragEnd = () => { - onDragEnd?.(); - }; - - // Disable draggable when focusing on form elements (fixes Firefox input interaction) - const handleFocusIn = (e: FocusEvent) => { - const target = e.target as HTMLElement; - if (isFormElement(target)) { - node.setAttribute('draggable', 'false'); - } - }; - - const handleFocusOut = (e: FocusEvent) => { - const target = e.target as HTMLElement; - if (isFormElement(target)) { - node.setAttribute('draggable', 'true'); - } - }; - - node.setAttribute('draggable', 'true'); - node.setAttribute('role', 'button'); - node.setAttribute('tabindex', '0'); - - node.addEventListener('dragstart', handleDragStart); - node.addEventListener('dragenter', handleDragEnter); - node.addEventListener('dragover', handleDragOver); - node.addEventListener('drop', handleDrop); - node.addEventListener('dragend', handleDragEnd); - node.addEventListener('focusin', handleFocusIn); - node.addEventListener('focusout', handleFocusOut); - - // Update classes based on drag state - const updateClasses = (dragging: boolean, dragOver: boolean) => { - // Remove all drag-related classes first - node.classList.remove('opacity-50', 'border-gray-400', 'dark:border-gray-500', 'border-solid'); - - // Add back only the active ones - if (dragging) { - node.classList.add('opacity-50'); - } - - if (dragOver) { - node.classList.add('border-gray-400', 'dark:border-gray-500', 'border-solid'); - node.classList.remove('border-transparent'); - } else { - node.classList.add('border-transparent'); - } - }; - - updateClasses(isDragging || false, isDragOver || false); - - return { - update(newOptions: DragAndDropOptions) { - index = newOptions.index; - onDragStart = newOptions.onDragStart; - onDragEnter = newOptions.onDragEnter; - onDrop = newOptions.onDrop; - onDragEnd = newOptions.onDragEnd; - - const newIsDragging = newOptions.isDragging || false; - const newIsDragOver = newOptions.isDragOver || false; - - if (newIsDragging !== isDragging || newIsDragOver !== isDragOver) { - isDragging = newIsDragging; - isDragOver = newIsDragOver; - updateClasses(isDragging, isDragOver); - } - }, - destroy() { - node.removeEventListener('dragstart', handleDragStart); - node.removeEventListener('dragenter', handleDragEnter); - node.removeEventListener('dragover', handleDragOver); - node.removeEventListener('drop', handleDrop); - node.removeEventListener('dragend', handleDragEnd); - node.removeEventListener('focusin', handleFocusIn); - node.removeEventListener('focusout', handleFocusOut); - }, - }; -} diff --git a/web/src/lib/actions/focus-trap.ts b/web/src/lib/actions/focus-trap.ts index a1c3051b10..91c9f6a6dd 100644 --- a/web/src/lib/actions/focus-trap.ts +++ b/web/src/lib/actions/focus-trap.ts @@ -69,38 +69,44 @@ export function focusTrap(container: HTMLElement, options?: Options) { // Add focus event listeners to sentinel nodes const handleStartFocus = () => { - if (withDefaults(options).active) { - const [, lastElement] = getFocusableElements(); - // If no elements, stay on backup sentinel - if (lastElement) { - lastElement.focus(); - } else { - backupSentinel.focus(); - } + if (!withDefaults(options).active) { + return; + } + + const [, lastElement] = getFocusableElements(); + // If no elements, stay on backup sentinel + if (lastElement) { + lastElement.focus(); + } else { + backupSentinel.focus(); } }; const handleBackupFocus = () => { // Backup sentinel keeps focus when there are no other focusable elements - if (withDefaults(options).active) { - const [firstElement] = getFocusableElements(); - // Only move focus if there are actual focusable elements - if (firstElement) { - firstElement.focus(); - } - // Otherwise, focus stays on backup sentinel + if (!withDefaults(options).active) { + return; } + + const [firstElement] = getFocusableElements(); + // Only move focus if there are actual focusable elements + if (firstElement) { + firstElement.focus(); + } + // Otherwise, focus stays on backup sentinel }; const handleEndFocus = () => { - if (withDefaults(options).active) { - const [firstElement] = getFocusableElements(); - // If no elements, move to backup sentinel - if (firstElement) { - firstElement.focus(); - } else { - backupSentinel.focus(); - } + if (!withDefaults(options).active) { + return; + } + + const [firstElement] = getFocusableElements(); + // If no elements, move to backup sentinel + if (firstElement) { + firstElement.focus(); + } else { + backupSentinel.focus(); } }; diff --git a/web/src/lib/actions/scroll-memory.ts b/web/src/lib/actions/scroll-memory.ts index 9953bf00fb..9645e2d5d3 100644 --- a/web/src/lib/actions/scroll-memory.ts +++ b/web/src/lib/actions/scroll-memory.ts @@ -52,7 +52,7 @@ export function scrollMemory( const newScroll = sessionStorage.getItem(SessionStorageKey.SCROLL_POSITION); if (newScroll) { node.scroll({ - top: Number.parseFloat(newScroll), + top: Number(newScroll), behavior: 'instant', }); } @@ -71,10 +71,12 @@ export function scrollMemory( export function scrollMemoryClearer(_node: HTMLElement, { routeStartsWith, beforeClear }: Options) { const unsubscribeNavigating = navigating.subscribe((navigation) => { // Forget scroll position from main page if going somewhere else. - if (navigation?.to && !navigation?.to.url.pathname.startsWith(routeStartsWith)) { - beforeClear?.(); - sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION); + if (!navigation?.to || navigation?.to.url.pathname.startsWith(routeStartsWith)) { + return; } + + beforeClear?.(); + sessionStorage.removeItem(SessionStorageKey.SCROLL_POSITION); }); return { diff --git a/web/src/lib/actions/use-actions.ts b/web/src/lib/actions/use-actions.ts index 762cfdccf7..622615358d 100644 --- a/web/src/lib/actions/use-actions.ts +++ b/web/src/lib/actions/use-actions.ts @@ -3,10 +3,10 @@ * https://github.com/hperrin/svelte-material-ui/blob/master/packages/common/src/internal/useActions.ts */ -export type SvelteActionReturnType

= { +export type SvelteActionReturnType

= void | { update?: (newParams?: P) => void; destroy?: () => void; -} | void; +}; export type SvelteHTMLActionType

= (node: HTMLElement, params?: P) => SvelteActionReturnType

; @@ -46,13 +46,15 @@ export function useActions(node: HTMLElement | SVGElement, actions: ActionArray) if (actions) { for (const [i, returnEntry] of actionReturns.entries()) { - if (returnEntry && returnEntry.update) { - const actionEntry = actions[i]; - if (Array.isArray(actionEntry) && actionEntry.length > 1) { - returnEntry.update(actionEntry[1]); - } else { - returnEntry.update(); - } + if (!(returnEntry && returnEntry.update)) { + continue; + } + + const actionEntry = actions[i]; + if (Array.isArray(actionEntry) && actionEntry.length > 1) { + returnEntry.update(actionEntry[1]); + } else { + returnEntry.update(); } } } diff --git a/web/src/lib/attachments/drag-and-drop.svelte.ts b/web/src/lib/attachments/drag-and-drop.svelte.ts index 950e8e5b80..28afd25615 100644 --- a/web/src/lib/attachments/drag-and-drop.svelte.ts +++ b/web/src/lib/attachments/drag-and-drop.svelte.ts @@ -16,7 +16,7 @@ export function dragAndDrop(options: DragAndDropOptions): Attachment { const { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options; const isFormElement = (el: HTMLElement) => { - return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT'; + return ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName); }; const handleDragStart = (e: DragEvent) => { diff --git a/web/src/lib/commands.ts b/web/src/lib/commands.ts index 7498756ac0..549400d9ca 100644 --- a/web/src/lib/commands.ts +++ b/web/src/lib/commands.ts @@ -232,7 +232,7 @@ export const getSettingsProvider = ($t: MessageFormatter) => { { title: $t('my_immich_title'), description: $t('my_immich_description'), - onAction: () => copyToClipboard(getMyImmichLink().toString()), + onAction: () => copyToClipboard(getMyImmichLink().href), shortcuts: { ctrl: true, shift: true, key: 'm' }, }, ]; diff --git a/web/src/lib/components/ApiKeyPermissionsPicker.svelte b/web/src/lib/components/ApiKeyPermissionsPicker.svelte index 859c20da80..9f5ddc74d4 100644 --- a/web/src/lib/components/ApiKeyPermissionsPicker.svelte +++ b/web/src/lib/components/ApiKeyPermissionsPicker.svelte @@ -18,7 +18,7 @@ } const [group] = permission.split('.'); - if (!permissions[group]) { + if (!Object.hasOwn(permissions, group)) { permissions[group] = []; } permissions[group].push(permission); diff --git a/web/src/lib/components/AssetViewerEvents.svelte b/web/src/lib/components/AssetViewerEvents.svelte index b636908b76..98a7a661fc 100644 --- a/web/src/lib/components/AssetViewerEvents.svelte +++ b/web/src/lib/components/AssetViewerEvents.svelte @@ -13,10 +13,12 @@ const events: EventMap = {}; for (const [name, listener] of Object.entries(props)) { - if (listener) { - const event = name.slice(2) as keyof Events; - events[event] = listener as EventCallback; + if (!listener) { + continue; } + + const event = name.slice(2) as keyof Events; + events[event] = listener as EventCallback; } return assetViewerManager.on(events); diff --git a/web/src/lib/components/Image.svelte b/web/src/lib/components/Image.svelte index 7ad6dc3ab7..7a42cf33bb 100644 --- a/web/src/lib/components/Image.svelte +++ b/web/src/lib/components/Image.svelte @@ -19,12 +19,14 @@ let destroyed = false; $effect(() => { - if (src !== undefined && capturedSource === undefined) { - capturedSource = src; - untrack(() => { - onStart?.(); - }); + if (src === undefined || capturedSource !== undefined) { + return; } + + capturedSource = src; + untrack(() => { + onStart?.(); + }); }); onDestroy(() => { diff --git a/web/src/lib/components/OnEvents.svelte b/web/src/lib/components/OnEvents.svelte index fe8039cf38..122ef3c455 100644 --- a/web/src/lib/components/OnEvents.svelte +++ b/web/src/lib/components/OnEvents.svelte @@ -13,10 +13,12 @@ const events: EventMap = {}; for (const [name, listener] of Object.entries(props)) { - if (listener) { - const event = name.slice(2) as keyof Events; - events[event] = listener as EventCallback; + if (!listener) { + continue; } + + const event = name.slice(2) as keyof Events; + events[event] = listener as EventCallback; } return eventManager.on(events); diff --git a/web/src/lib/components/SchemaConfiguration.svelte b/web/src/lib/components/SchemaConfiguration.svelte index 249331a5e4..65d115179e 100644 --- a/web/src/lib/components/SchemaConfiguration.svelte +++ b/web/src/lib/components/SchemaConfiguration.svelte @@ -28,9 +28,9 @@ const label = $derived(schema.title ?? key); const description = $derived(schema.description); - const getValue = (defaultValue?: T) => (root === true ? config : (config?.[key] ?? defaultValue)) as T; + const getValue = (defaultValue?: T) => (root ? config : (config?.[key] ?? defaultValue)) as T; const setValue = (value: T) => { - if (root === true) { + if (root) { config = value; } else { if (config === undefined) { diff --git a/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte b/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte index 9d99e86a42..e22808bbf1 100644 --- a/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte +++ b/web/src/lib/components/admin-settings/StorageTemplateSettings.svelte @@ -128,7 +128,7 @@ title={$t('admin.storage_template_enable_description')} {disabled} bind:checked={configToEdit.storageTemplate.enabled} - isEdited={!(configToEdit.storageTemplate.enabled === config.storageTemplate.enabled)} + isEdited={configToEdit.storageTemplate.enabled !== config.storageTemplate.enabled} /> {#if !minified} @@ -137,9 +137,8 @@ {disabled} subtitle={$t('admin.storage_template_hash_verification_enabled_description')} bind:checked={configToEdit.storageTemplate.hashVerificationEnabled} - isEdited={!( - configToEdit.storageTemplate.hashVerificationEnabled === config.storageTemplate.hashVerificationEnabled - )} + isEdited={configToEdit.storageTemplate.hashVerificationEnabled !== + config.storageTemplate.hashVerificationEnabled} /> {/if} @@ -233,7 +232,7 @@ required inputType={SettingInputFieldType.TEXT} bind:value={configToEdit.storageTemplate.template} - isEdited={!(configToEdit.storageTemplate.template === config.storageTemplate.template)} + isEdited={configToEdit.storageTemplate.template !== config.storageTemplate.template} />

diff --git a/web/src/lib/components/album-page/AlbumMap.svelte b/web/src/lib/components/album-page/AlbumMap.svelte index eb9d72b89c..0a6a79eb6f 100644 --- a/web/src/lib/components/album-page/AlbumMap.svelte +++ b/web/src/lib/components/album-page/AlbumMap.svelte @@ -30,10 +30,12 @@ }); $effect(() => { - if (!assetViewerManager.isViewing && returnToMap) { - returnToMap = false; - void onClick(); + if (assetViewerManager.isViewing || !returnToMap) { + return; } + + returnToMap = false; + void onClick(); }); const loadMapMarkers = async () => { diff --git a/web/src/lib/components/album-page/AlbumViewer.svelte b/web/src/lib/components/album-page/AlbumViewer.svelte index 4e7d6bce1a..7f294e2c20 100644 --- a/web/src/lib/components/album-page/AlbumViewer.svelte +++ b/web/src/lib/components/album-page/AlbumViewer.svelte @@ -38,10 +38,12 @@ let timelineManager = $state() as TimelineManager; dragAndDropFilesStore.subscribe((value) => { - if (value.isDragging && value.files.length > 0) { - handlePromiseError(fileUploadHandler({ files: value.files, albumId: album.id })); - dragAndDropFilesStore.set({ isDragging: false, files: [] }); + if (!(value.isDragging && value.files.length > 0)) { + return; } + + handlePromiseError(fileUploadHandler({ files: value.files, albumId: album.id })); + dragAndDropFilesStore.set({ isDragging: false, files: [] }); }); const handleStartSlideshow = async () => { diff --git a/web/src/lib/components/album-page/AlbumsList.svelte b/web/src/lib/components/album-page/AlbumsList.svelte index 846e2b97a7..69ba212c19 100644 --- a/web/src/lib/components/album-page/AlbumsList.svelte +++ b/web/src/lib/components/album-page/AlbumsList.svelte @@ -81,11 +81,8 @@ // We make sure empty albums stay at the end of the list if (a === unknownYear) { return 1; - } else if (b === unknownYear) { - return -1; - } else { - return (Number.parseInt(a) - Number.parseInt(b)) * sortSign; } + return b === unknownYear ? -1 : (Number.parseInt(a) - Number.parseInt(b)) * sortSign; }); return sortedByYear.map(([year, albums]) => ({ @@ -106,13 +103,14 @@ // of the list if (ownerIdA === currentUserId) { return -sortSign; - } else if (ownerIdB === currentUserId) { - return sortSign; - } else { - const ownerA = albumsA[0].albumUsers[0].user; - const ownerB = albumsB[0].albumUsers[0].user; - return ownerA.name.localeCompare(ownerB.name, $locale) * sortSign; } + if (ownerIdB === currentUserId) { + return sortSign; + } + + const ownerA = albumsA[0].albumUsers[0].user; + const ownerB = albumsB[0].albumUsers[0].user; + return ownerA.name.localeCompare(ownerB.name, $locale) * sortSign; }); return sortedByOwnerNames.map(([ownerId, albums]) => ({ diff --git a/web/src/lib/components/album-page/__tests__/AlbumCard.spec.ts b/web/src/lib/components/album-page/__tests__/AlbumCard.spec.ts index 573c73b408..94f64fb929 100644 --- a/web/src/lib/components/album-page/__tests__/AlbumCard.spec.ts +++ b/web/src/lib/components/album-page/__tests__/AlbumCard.spec.ts @@ -96,7 +96,7 @@ describe('AlbumCard component', () => { }); it('dispatches "onShowContextMenu" event on context menu click with mouse coordinates', async () => { - const contextMenuButton = sut.getByTestId('context-button-parent').children[0]; + const contextMenuButton = sut.getByTestId('context-button-parent').firstElementChild!; expect(contextMenuButton).toBeDefined(); // Mock getBoundingClientRect to return a bounding rectangle that will result in the expected position diff --git a/web/src/lib/components/asset-viewer/AlbumListItem.svelte b/web/src/lib/components/asset-viewer/AlbumListItem.svelte index 403fdd98ca..af40deb27a 100644 --- a/web/src/lib/components/asset-viewer/AlbumListItem.svelte +++ b/web/src/lib/components/asset-viewer/AlbumListItem.svelte @@ -101,7 +101,7 @@ e.preventDefault(); }; element.addEventListener('click', click); - element.addEventListener('pointerdown', start, true); + element.addEventListener('pointerdown', start, { capture: true }); element.addEventListener('pointerup', clearLongPressTimer, { capture: true, passive: true }); return { destroy: () => { diff --git a/web/src/lib/components/asset-viewer/AssetViewer.svelte b/web/src/lib/components/asset-viewer/AssetViewer.svelte index 6538dc1a47..8f58dc97b9 100644 --- a/web/src/lib/components/asset-viewer/AssetViewer.svelte +++ b/web/src/lib/components/asset-viewer/AssetViewer.svelte @@ -173,10 +173,12 @@ }); const slideshowNavigationUnsubscribe = slideshowNavigation.subscribe((value) => { - if (value === SlideshowNavigation.Shuffle) { - slideshowHistory.reset(); - slideshowHistory.queue(toTimelineAsset(asset)); + if (value !== SlideshowNavigation.Shuffle) { + return; } + + slideshowHistory.reset(); + slideshowHistory.queue(toTimelineAsset(asset)); }); return () => { @@ -477,9 +479,7 @@ if (event.detail.direction === 'left') { navigateAsset('next'); - } - - if (event.detail.direction === 'right') { + } else if (event.detail.direction === 'right') { navigateAsset('previous'); } }; diff --git a/web/src/lib/components/asset-viewer/DetailPanel.svelte b/web/src/lib/components/asset-viewer/DetailPanel.svelte index 98b64d5aa0..1bc1caa778 100644 --- a/web/src/lib/components/asset-viewer/DetailPanel.svelte +++ b/web/src/lib/components/asset-viewer/DetailPanel.svelte @@ -191,9 +191,9 @@

{/if} - {#if (asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth) || asset.exifInfo?.fileSizeInByte} + {#if (asset.exifInfo?.exifImageHeight && asset.exifInfo.exifImageWidth) || asset.exifInfo?.fileSizeInByte}
- {#if asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth} + {#if asset.exifInfo?.exifImageHeight && asset.exifInfo.exifImageWidth} {#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}

{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP diff --git a/web/src/lib/components/asset-viewer/DetailPanelPeople.svelte b/web/src/lib/components/asset-viewer/DetailPanelPeople.svelte index 78265f9ea2..6290209e28 100644 --- a/web/src/lib/components/asset-viewer/DetailPanelPeople.svelte +++ b/web/src/lib/components/asset-viewer/DetailPanelPeople.svelte @@ -35,7 +35,8 @@ let formattedAge; if (ageInYears < 0) { return { formattedBirthDate: undefined, formattedAge: undefined, ...person }; - } else if (ageInMonths < 12) { + } + if (ageInMonths < 12) { formattedAge = $t('age_months', { values: { months: ageInMonths } }); } else if (ageInMonths > 12 && ageInMonths < 24) { formattedAge = $t('age_year_months', { values: { months: ageInMonths - 12 } }); diff --git a/web/src/lib/components/asset-viewer/OcrBoundingBox.svelte b/web/src/lib/components/asset-viewer/OcrBoundingBox.svelte index 34bde374e6..10470b43de 100644 --- a/web/src/lib/components/asset-viewer/OcrBoundingBox.svelte +++ b/web/src/lib/components/asset-viewer/OcrBoundingBox.svelte @@ -20,6 +20,7 @@ const handleSelectStart = (event: Event) => { const target = event.currentTarget as HTMLElement; requestAnimationFrame(() => { + // eslint-disable-next-line unicorn/no-unnecessary-global-this const selection = globalThis.getSelection(); if (selection) { selection.selectAllChildren(target); diff --git a/web/src/lib/components/asset-viewer/PhotoViewer.svelte b/web/src/lib/components/asset-viewer/PhotoViewer.svelte index 37844a5459..c0af733f3f 100644 --- a/web/src/lib/components/asset-viewer/PhotoViewer.svelte +++ b/web/src/lib/components/asset-viewer/PhotoViewer.svelte @@ -115,6 +115,7 @@ // TODO move to action + command palette const onCopyShortcut = (event: KeyboardEvent) => { + // eslint-disable-next-line unicorn/no-unnecessary-global-this if (globalThis.getSelection()?.type === 'Range') { return; } @@ -139,7 +140,7 @@ if (!url || !castManager.isCasting) { return; } - const fullUrl = new URL(url, globalThis.location.href); + const fullUrl = new URL(url, location.href); try { await castManager.loadMedia(fullUrl.href); diff --git a/web/src/lib/components/asset-viewer/SlideshowBar.svelte b/web/src/lib/components/asset-viewer/SlideshowBar.svelte index 8065b6b276..e8e6b8061e 100644 --- a/web/src/lib/components/asset-viewer/SlideshowBar.svelte +++ b/web/src/lib/components/asset-viewer/SlideshowBar.svelte @@ -61,10 +61,12 @@ const hideControlsAfterDelay = () => { timer = setTimeout(() => { - if (!isOverControls) { - showControls = false; - setCursorStyle('none'); + if (isOverControls) { + return; } + + showControls = false; + setCursorStyle('none'); }, 2500); }; @@ -77,10 +79,12 @@ }); unsubscribeStop = stopProgress.subscribe((value) => { - if (value) { - progressBar?.restart(); - stopControlsHideTimer(); + if (!value) { + return; } + + progressBar?.restart(); + stopControlsHideTimer(); }); }); diff --git a/web/src/lib/components/asset-viewer/SlideshowMetadataOverlay.svelte b/web/src/lib/components/asset-viewer/SlideshowMetadataOverlay.svelte index e1865e2c14..a321573592 100644 --- a/web/src/lib/components/asset-viewer/SlideshowMetadataOverlay.svelte +++ b/web/src/lib/components/asset-viewer/SlideshowMetadataOverlay.svelte @@ -18,7 +18,7 @@ const description = $derived(asset.exifInfo?.description?.trim() || ''); const dateTime = $derived( - asset.exifInfo?.timeZone && asset.exifInfo?.dateTimeOriginal + asset.exifInfo?.timeZone && asset.exifInfo.dateTimeOriginal ? fromISODateTime(asset.exifInfo.dateTimeOriginal, asset.exifInfo.timeZone) : fromISODateTimeUTC(asset.localDateTime), ); diff --git a/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte b/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte index d71e1682f0..2663be3f98 100644 --- a/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte +++ b/web/src/lib/components/asset-viewer/VideoNativeViewer.svelte @@ -152,13 +152,13 @@ }, useMediaCapabilities: false, xhrSetup: (xhr: XMLHttpRequest, url: string) => { - const authenticatedUrl = new URL(url, globalThis.location.origin); + const authenticatedUrl = new URL(url, location.origin); for (const [key, value] of Object.entries(authManager.params)) { if (value) { authenticatedUrl.searchParams.set(key, value as string); } } - xhr.open('GET', authenticatedUrl.toString()); + xhr.open('GET', authenticatedUrl.href); }, }; @@ -308,8 +308,7 @@ const onSwipe = (event: SwipeCustomEvent) => { if (event.detail.direction === 'left') { onNextAsset(); - } - if (event.detail.direction === 'right') { + } else if (event.detail.direction === 'right') { onPreviousAsset(); } }; @@ -389,10 +388,12 @@ onended={onVideoEnded} onseeking={onSeeking} onplaying={(e: Event) => { - if (!hasFocused) { - (e.currentTarget as HTMLElement).focus(); - hasFocused = true; + if (hasFocused) { + return; } + + (e.currentTarget as HTMLElement).focus(); + hasFocused = true; }} onclose={onClose} poster={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Preview, cacheKey })} @@ -412,10 +413,12 @@ onended={onVideoEnded} onseeking={onSeeking} onplaying={(e) => { - if (!hasFocused) { - e.currentTarget.focus(); - hasFocused = true; + if (hasFocused) { + return; } + + e.currentTarget.focus(); + hasFocused = true; }} onclose={onClose} poster={getAssetMediaUrl({ id: asset.id, size: AssetMediaSize.Preview, cacheKey })} diff --git a/web/src/lib/components/asset-viewer/VideoRemoteViewer.svelte b/web/src/lib/components/asset-viewer/VideoRemoteViewer.svelte index 851cd05f78..2ca123b879 100644 --- a/web/src/lib/components/asset-viewer/VideoRemoteViewer.svelte +++ b/web/src/lib/components/asset-viewer/VideoRemoteViewer.svelte @@ -51,7 +51,7 @@ if (!url || !castManager.isCasting) { return; } - const fullUrl = new URL(url, globalThis.location.href); + const fullUrl = new URL(url, location.href); try { await castManager.loadMedia(fullUrl.href, force); @@ -63,7 +63,7 @@ }; function handleSeek(event: Event) { - const newTime = Number.parseFloat((event.target as HTMLInputElement).value); + const newTime = Number((event.target as HTMLInputElement).value); castManager.seekTo(newTime); } diff --git a/web/src/lib/components/asset-viewer/editor/transform-tool/TransformTool.svelte b/web/src/lib/components/asset-viewer/editor/transform-tool/TransformTool.svelte index 29e4e09506..5bc50f9431 100644 --- a/web/src/lib/components/asset-viewer/editor/transform-tool/TransformTool.svelte +++ b/web/src/lib/components/asset-viewer/editor/transform-tool/TransformTool.svelte @@ -37,9 +37,8 @@ if (isRotated) { let [width, height] = ratio.value.split(':'); return `${height}:${width}`; - } else { - return ratio.value; } + return ratio.value; } function ratioSelected(ratio: AspectRatioOption): boolean { diff --git a/web/src/lib/components/asset-viewer/immich-time-range.ts b/web/src/lib/components/asset-viewer/immich-time-range.ts index a3de131e73..12d6200d51 100644 --- a/web/src/lib/components/asset-viewer/immich-time-range.ts +++ b/web/src/lib/components/asset-viewer/immich-time-range.ts @@ -49,6 +49,6 @@ class ImmichTimeRange extends MediaTimeRange { } } -if (!globalThis.customElements.get('immich-time-range')) { - globalThis.customElements.define('immich-time-range', ImmichTimeRange); +if (!customElements.get('immich-time-range')) { + customElements.define('immich-time-range', ImmichTimeRange); } diff --git a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte index 6e73952b18..5764965bdc 100644 --- a/web/src/lib/components/assets/thumbnail/Thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/Thumbnail.svelte @@ -167,7 +167,7 @@ e.preventDefault(); }; element.addEventListener('click', click); - element.addEventListener('pointerdown', start, true); + element.addEventListener('pointerdown', start, { capture: true }); element.addEventListener('pointerup', clearLongPressTimer, { capture: true, passive: true }); return { destroy: () => { @@ -215,8 +215,7 @@ onkeydown={(evt) => { if (evt.key === 'Enter') { callClickHandlers(); - } - if (evt.key === 'x') { + } else if (evt.key === 'x') { onSelect?.(asset); } if (document.activeElement === element && evt.key === 'Escape') { diff --git a/web/src/lib/components/assets/thumbnail/VideoThumbnail.svelte b/web/src/lib/components/assets/thumbnail/VideoThumbnail.svelte index c62abac3c2..2875d96a9c 100644 --- a/web/src/lib/components/assets/thumbnail/VideoThumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/VideoThumbnail.svelte @@ -85,10 +85,7 @@ }} ontimeupdate={({ currentTarget }) => { const remaining = currentTarget.duration - currentTarget.currentTime; - remainingSeconds = Math.min( - Math.ceil(Number.isNaN(remaining) ? Number.POSITIVE_INFINITY : remaining), - durationInSeconds, - ); + remainingSeconds = Math.min(Math.ceil(Number.isNaN(remaining) ? Infinity : remaining), durationInSeconds); }} > {/if} diff --git a/web/src/lib/components/faces-page/PersonSidePanel.svelte b/web/src/lib/components/faces-page/PersonSidePanel.svelte index eedd6766eb..eaeabcc921 100644 --- a/web/src/lib/components/faces-page/PersonSidePanel.svelte +++ b/web/src/lib/components/faces-page/PersonSidePanel.svelte @@ -93,10 +93,10 @@ }; const handleReset = (id: string) => { - if (selectedPersonToReassign[id]) { + if (Object.hasOwn(selectedPersonToReassign, id)) { delete selectedPersonToReassign[id]; } - if (selectedPersonToCreate[id]) { + if (Object.hasOwn(selectedPersonToCreate, id)) { delete selectedPersonToCreate[id]; } }; @@ -115,7 +115,7 @@ id: personId, faceDto: { id: personWithFace.id }, }); - } else if (selectedPersonToCreate[personWithFace.id]) { + } else if (Object.hasOwn(selectedPersonToCreate, personWithFace.id)) { const data = await createPerson({ personCreateDto: {} }); peopleToCreate.push(data.id); await reassignFacesById({ @@ -314,7 +314,7 @@ {/if}

- {#if !selectedPersonToCreate[face.id]} + {#if !Object.hasOwn(selectedPersonToCreate, face.id)}

{#if selectedPersonToReassign[face.id]?.id} {selectedPersonToReassign[face.id]?.name} @@ -349,7 +349,7 @@ {/if}

- {#if !selectedPersonToCreate[face.id] && !selectedPersonToReassign[face.id] && !face.person} + {#if !Object.hasOwn(selectedPersonToCreate, face.id) && !Object.hasOwn(selectedPersonToReassign, face.id) && !face.person}
diff --git a/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte b/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte index 8582806c94..b3b21a2f3e 100644 --- a/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte +++ b/web/src/lib/components/maintenance/MaintenanceBackupsList.svelte @@ -71,7 +71,7 @@ } // Sort by date descending (newest first), but put unknown date at the top - const sortedEntries = [...groups.entries()].sort((a, b) => { + const sortedEntries = [...groups].sort((a, b) => { if (a[0] === unknownDateKey) { return -1; } @@ -115,7 +115,7 @@
- {#each [...groupedBackups.entries()] as [dateGroup, groupBackups] (dateGroup)} + {#each [...groupedBackups] as [dateGroup, groupBackups] (dateGroup)}
diff --git a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte index ab2e6ce4f4..a9fb831e82 100644 --- a/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte +++ b/web/src/lib/components/server-statistics/ServerStatisticsCard.svelte @@ -21,7 +21,7 @@ let length = 13; if (data) { const valueLength = data.value.toString().length; - length = length - valueLength; + length -= valueLength; } return '0'.repeat(length); diff --git a/web/src/lib/components/share-page/IndividualSharedViewer.svelte b/web/src/lib/components/share-page/IndividualSharedViewer.svelte index a3cf76c78e..a119f4ffea 100644 --- a/web/src/lib/components/share-page/IndividualSharedViewer.svelte +++ b/web/src/lib/components/share-page/IndividualSharedViewer.svelte @@ -35,10 +35,12 @@ let assets = $derived(sharedLink.assets); dragAndDropFilesStore.subscribe((value) => { - if (value.isDragging && value.files.length > 0) { - handlePromiseError(handleUploadAssets(value.files)); - dragAndDropFilesStore.set({ isDragging: false, files: [] }); + if (!(value.isDragging && value.files.length > 0)) { + return; } + + handlePromiseError(handleUploadAssets(value.files)); + dragAndDropFilesStore.set({ isDragging: false, files: [] }); }); const downloadAssets = async () => { diff --git a/web/src/lib/components/shared-components/Combobox.svelte b/web/src/lib/components/shared-components/Combobox.svelte index 7b0e867343..388317a4ef 100644 --- a/web/src/lib/components/shared-components/Combobox.svelte +++ b/web/src/lib/components/shared-components/Combobox.svelte @@ -345,10 +345,12 @@ { shortcut: { key: 'Escape' }, onShortcut: (event) => { - if (isOpen) { - event.stopPropagation(); - closeDropdown(); + if (!isOpen) { + return; } + + event.stopPropagation(); + closeDropdown(); }, }, ]} @@ -399,7 +401,7 @@ aria-selected={selectedIndex === 0} aria-disabled={true} class="w-full cursor-default px-4 py-2 text-start hover:bg-gray-200 aria-selected:bg-gray-200 dark:hover:bg-gray-700 aria-selected:dark:bg-gray-700" - id={`${listboxId}-${0}`} + id={`${listboxId}-0`} onclick={closeDropdown} > {allowCreate ? searchQuery : $t('no_results')} diff --git a/web/src/lib/components/shared-components/SingleGridRow.svelte b/web/src/lib/components/shared-components/SingleGridRow.svelte index c4db34c152..b71086830c 100644 --- a/web/src/lib/components/shared-components/SingleGridRow.svelte +++ b/web/src/lib/components/shared-components/SingleGridRow.svelte @@ -18,7 +18,7 @@ }; }; - const parsePixels = (style: string) => Number.parseInt(style, 10) || 0; + const parsePixels = (style: string) => Math.trunc(Number(style)) || 0; const getItemCount = (container: HTMLElement, containerWidth: number) => { if (!container.firstElementChild) { diff --git a/web/src/lib/components/shared-components/context-menu/MenuOption.svelte b/web/src/lib/components/shared-components/context-menu/MenuOption.svelte index b4a6f060a0..fe39b36c8e 100644 --- a/web/src/lib/components/shared-components/context-menu/MenuOption.svelte +++ b/web/src/lib/components/shared-components/context-menu/MenuOption.svelte @@ -32,6 +32,7 @@ let isActive = $derived($selectedIdStore === id); const handleClick = () => { + // eslint-disable-next-line unicorn/no-optional-chaining-on-undeclared-variable $optionClickCallbackStore?.(); onClick(); }; diff --git a/web/src/lib/components/shared-components/context-menu/RightClickContextMenu.svelte b/web/src/lib/components/shared-components/context-menu/RightClickContextMenu.svelte index 6b685f6d2d..16b1df1716 100644 --- a/web/src/lib/components/shared-components/context-menu/RightClickContextMenu.svelte +++ b/web/src/lib/components/shared-components/context-menu/RightClickContextMenu.svelte @@ -57,11 +57,13 @@ onClose?.(); }; $effect(() => { - if (isOpen && menuContainer) { - triggerElement = document.activeElement as HTMLElement; - menuContainer.focus(); - $optionClickCallbackStore = closeContextMenu; + if (!(isOpen && menuContainer)) { + return; } + + triggerElement = document.activeElement as HTMLElement; + menuContainer.focus(); + $optionClickCallbackStore = closeContextMenu; }); const oncontextmenu = async (event: MouseEvent) => { diff --git a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte index 29e7e6458c..5a4b25225f 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/GalleryViewer.svelte @@ -109,12 +109,14 @@ let lastEndReachedHeight = 0; $effect(() => { - if (geometry.containerHeight - slidingWindow.bottom <= viewport.height) { - const contentHeight = geometry.containerHeight; - if (lastEndReachedHeight !== contentHeight) { - debouncedOnEndReached(); - lastEndReachedHeight = contentHeight; - } + if (geometry.containerHeight - slidingWindow.bottom > viewport.height) { + return; + } + + const contentHeight = geometry.containerHeight; + if (lastEndReachedHeight !== contentHeight) { + debouncedOnEndReached(); + lastEndReachedHeight = contentHeight; } }); diff --git a/web/src/lib/components/shared-components/map/Map.svelte b/web/src/lib/components/shared-components/map/Map.svelte index ade37d0322..52805d9b96 100644 --- a/web/src/lib/components/shared-components/map/Map.svelte +++ b/web/src/lib/components/shared-components/map/Map.svelte @@ -109,14 +109,16 @@ ); export function addClipMapMarker(lng: number, lat: number) { - if (map) { - if (marker) { - marker.remove(); - } - - center = { lng, lat }; - marker = new Marker().setLngLat([lng, lat]).addTo(map); + if (!map) { + return; } + + if (marker) { + marker.remove(); + } + + center = { lng, lat }; + marker = new Marker().setLngLat([lng, lat]).addTo(map); } function handleAssetClick(assetId: string, map: Map | null) { @@ -159,17 +161,19 @@ } function handleMapClick(event: MapMouseEvent) { - if (clickable) { - const { lng, lat } = event.lngLat; - onClickPoint({ lng, lat }); + if (!clickable) { + return; + } - if (marker) { - marker.remove(); - } + const { lng, lat } = event.lngLat; + onClickPoint({ lng, lat }); - if (map) { - marker = new Marker().setLngLat([lng, lat]).addTo(map); - } + if (marker) { + marker.remove(); + } + + if (map) { + marker = new Marker().setLngLat([lng, lat]).addTo(map); } } @@ -254,13 +258,16 @@ }; afterNavigate(() => { - if (map) { - map.resize(); + if (!map) { + return; + } - if (globalThis.location.hash) { - const hashChangeEvent = new HashChangeEvent('hashchange'); - globalThis.dispatchEvent(hashChangeEvent); - } + map.resize(); + + if (location.hash) { + const hashChangeEvent = new HashChangeEvent('hashchange'); + // eslint-disable-next-line unicorn/no-unnecessary-global-this + globalThis.dispatchEvent(hashChangeEvent); } }); diff --git a/web/src/lib/components/shared-components/search-bar/SearchBar.svelte b/web/src/lib/components/shared-components/search-bar/SearchBar.svelte index 339bfb64a2..b099020e77 100644 --- a/web/src/lib/components/shared-components/search-bar/SearchBar.svelte +++ b/web/src/lib/components/shared-components/search-bar/SearchBar.svelte @@ -156,10 +156,12 @@ }; const onEnter = (event: KeyboardEvent) => { - if (selectedId) { - event.preventDefault(); - searchHistoryBox?.selectActiveOption(); + if (!selectedId) { + return; } + + event.preventDefault(); + searchHistoryBox?.selectActiveOption(); }; const onInput = () => { diff --git a/web/src/lib/components/shared-components/search-bar/SearchHistoryBox.svelte b/web/src/lib/components/shared-components/search-bar/SearchHistoryBox.svelte index 24ceaeb380..a1e3d9328f 100644 --- a/web/src/lib/components/shared-components/search-bar/SearchHistoryBox.svelte +++ b/web/src/lib/components/shared-components/search-bar/SearchHistoryBox.svelte @@ -44,7 +44,8 @@ export function moveSelection(increment: 1 | -1) { if (!isSearchSuggestions) { return; - } else if (selectedIndex === undefined) { + } + if (selectedIndex === undefined) { selectedIndex = increment === 1 ? 0 : suggestionCount - 1; } else if (selectedIndex + increment < 0 || selectedIndex + increment >= suggestionCount) { clearSelection(); diff --git a/web/src/lib/components/shared-components/settings/SettingsLanguageSelector.svelte b/web/src/lib/components/shared-components/settings/SettingsLanguageSelector.svelte index 146c37b7ec..ca1e3059c3 100644 --- a/web/src/lib/components/shared-components/settings/SettingsLanguageSelector.svelte +++ b/web/src/lib/components/shared-components/settings/SettingsLanguageSelector.svelte @@ -18,11 +18,13 @@ const defaultLangOption = { label: defaultLang.name, value: defaultLang.code }; const handleLanguageChange = async (newLang: string | undefined) => { - if (newLang) { - $lang = newLang; - await i18nLocale.set(convertBCP47(newLang)); - await invalidateAll(); + if (!newLang) { + return; } + + $lang = newLang; + await i18nLocale.set(convertBCP47(newLang)); + await invalidateAll(); }; let closestLanguage = $derived(getClosestAvailableLocale([$lang], langCodes)); diff --git a/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte b/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte index 2d4be43a00..10c772d8b8 100644 --- a/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte +++ b/web/src/lib/components/shared-components/side-bar/RecentAlbums.svelte @@ -11,7 +11,7 @@ const refreshAlbums = async () => { try { const allAlbums = await getAllAlbums({}); - albums = allAlbums.sort((a, b) => (a.updatedAt > b.updatedAt ? -1 : 1)).slice(0, 3); + albums = allAlbums.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()).slice(0, 3); userInteraction.recentAlbums = albums; } catch (error) { handleError(error, $t('failed_to_load_assets')); diff --git a/web/src/lib/components/timeline/Scrubber.svelte b/web/src/lib/components/timeline/Scrubber.svelte index 03a3e43d8e..67ec87ff7d 100644 --- a/web/src/lib/components/timeline/Scrubber.svelte +++ b/web/src/lib/components/timeline/Scrubber.svelte @@ -99,13 +99,15 @@ ) => { if (scrubberMonth === 'lead-in') { return relativeTopOffset * scrubberMonthPercent; - } else if (scrubberMonth === 'lead-out') { + } + if (scrubberMonth === 'lead-out') { let offset = relativeTopOffset; for (const segment of segments) { offset += segment.height; } return offset + relativeBottomOffset * scrubberMonthPercent; - } else if (scrubberMonth) { + } + if (scrubberMonth) { let offset = relativeTopOffset; let match = false; for (const segment of segments) { @@ -120,9 +122,8 @@ offset += scrubberMonthPercent * relativeBottomOffset; } return offset; - } else { - return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM)); } + return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM)); }; const scrollY = $derived( toScrollFromTimelineMonthPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent), @@ -229,14 +230,14 @@ if (scrollY !== undefined) { if (scrollY < relativeTopOffset) { return segments.at(0)?.dateFormatted; - } else { - let offset = relativeTopOffset; - for (const segment of segments) { - offset += segment.height; - } - if (scrollY > offset) { - return segments.at(-1)?.dateFormatted; - } + } + + let offset = relativeTopOffset; + for (const segment of segments) { + offset += segment.height; + } + if (scrollY > offset) { + return segments.at(-1)?.dateFormatted; } } return scrollSegment?.dateFormatted || ''; @@ -338,7 +339,7 @@ overallScrollPercent: toTimelineY(hoverY), scrubberMonthScrollPercent: timelineMonthPercentY, }; - if (wasDragging === false && isDragging) { + if (!wasDragging && isDragging) { void startScrub?.(scrubData); void onScrub?.(scrubData); } diff --git a/web/src/lib/components/timeline/Timeline.svelte b/web/src/lib/components/timeline/Timeline.svelte index 7949e2ff1b..1fe6c7b097 100644 --- a/web/src/lib/components/timeline/Timeline.svelte +++ b/web/src/lib/components/timeline/Timeline.svelte @@ -201,7 +201,7 @@ export const scrollAfterNavigate = async () => { if (timelineManager.viewportHeight === 0 || timelineManager.viewportWidth === 0) { // this can happen if you do the following navigation order - // /photos?at=, /photos/, http://example.com, browser back, browser back + // /photos?at=, /photos/, https://example.com, browser back, browser back const rect = scrollableElement?.getBoundingClientRect(); if (rect) { timelineManager.viewportHeight = rect.height; @@ -209,10 +209,7 @@ } } const scrollTarget = assetViewerManager.gridScrollTarget?.at; - let scrolled = false; - if (scrollTarget) { - scrolled = await scrollAndLoadAsset(scrollTarget); - } + const scrolled = scrollTarget ? await scrollAndLoadAsset(scrollTarget) : false; if (!scrolled) { // if the asset is not found, scroll to the top timelineManager.scrollTo(0); @@ -503,10 +500,12 @@ }); $effect(() => { - if (assetViewerManager.asset && assetViewerManager.isViewing) { - const { localDateTime } = getTimes(assetViewerManager.asset.fileCreatedAt, DateTime.local().offset / 60); - void timelineManager.loadTimelineMonth({ year: localDateTime.year, month: localDateTime.month }); + if (!(assetViewerManager.asset && assetViewerManager.isViewing)) { + return; } + + const { localDateTime } = getTimes(assetViewerManager.asset.fileCreatedAt, DateTime.local().offset / 60); + void timelineManager.loadTimelineMonth({ year: localDateTime.year, month: localDateTime.month }); }); const assetSelectHandler = ( @@ -582,10 +581,7 @@ bind:scrubberWidth onScrubKeyDown={(evt) => { evt.preventDefault(); - let amount = 50; - if (keyboardManager.shift) { - amount = 500; - } + let amount = keyboardManager.shift ? 500 : 50; if (evt.key === 'ArrowUp') { amount = -amount; if (keyboardManager.shift) { diff --git a/web/src/lib/elements/FormatMessage.svelte b/web/src/lib/elements/FormatMessage.svelte index 3385ab3b56..53f72451cc 100644 --- a/web/src/lib/elements/FormatMessage.svelte +++ b/web/src/lib/elements/FormatMessage.svelte @@ -41,10 +41,12 @@ for (const option of Object.values(element.options)) { for (const pluralElement of option.value) { - if (pluralElement.type === TYPE.tag) { - const tag = pluralElement.value; - replacements[tag] = (...parts) => `<${tag}>${parts}`; + if (pluralElement.type !== TYPE.tag) { + continue; } + + const tag = pluralElement.value; + replacements[tag] = (...parts) => `<${tag}>${parts}`; } } diff --git a/web/src/lib/managers/asset-viewer-manager.svelte.ts b/web/src/lib/managers/asset-viewer-manager.svelte.ts index 551f50bc6d..94a6d00700 100644 --- a/web/src/lib/managers/asset-viewer-manager.svelte.ts +++ b/web/src/lib/managers/asset-viewer-manager.svelte.ts @@ -134,10 +134,12 @@ class AssetViewerManager extends BaseEventManager { } cancelZoomAnimation() { - if (this.#animationFrameId !== null) { - cancelAnimationFrame(this.#animationFrameId); - this.#animationFrameId = null; + if (this.#animationFrameId === null) { + return; } + + cancelAnimationFrame(this.#animationFrameId); + this.#animationFrameId = null; } animatedZoom(targetZoom: number, duration = 300) { diff --git a/web/src/lib/managers/auth-manager.svelte.ts b/web/src/lib/managers/auth-manager.svelte.ts index 38f71811ae..4b1743b9d2 100644 --- a/web/src/lib/managers/auth-manager.svelte.ts +++ b/web/src/lib/managers/auth-manager.svelte.ts @@ -109,7 +109,7 @@ class AuthManager { await goto(redirectUri); } else { - globalThis.location.href = redirectUri; + location.assign(redirectUri); } } diff --git a/web/src/lib/managers/download-manager.svelte.ts b/web/src/lib/managers/download-manager.svelte.ts index 107f80b8dc..f08e5eb929 100644 --- a/web/src/lib/managers/download-manager.svelte.ts +++ b/web/src/lib/managers/download-manager.svelte.ts @@ -16,7 +16,7 @@ class DownloadManager { return; } - if (!this.assets[key]) { + if (!Object.hasOwn(this.assets, key)) { this.assets[key] = { progress: 0, total: 0, percentage: 0, abort: null }; } diff --git a/web/src/lib/managers/edit/transform-manager.svelte.ts b/web/src/lib/managers/edit/transform-manager.svelte.ts index 8826d58d11..9379138a1b 100644 --- a/web/src/lib/managers/edit/transform-manager.svelte.ts +++ b/web/src/lib/managers/edit/transform-manager.svelte.ts @@ -193,6 +193,7 @@ class TransformManager implements EditToolManager { passive: true, }); + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('mousemove', (e: MouseEvent) => transformManager.handleMouseMove(e), { passive: true }); const transformEdits = edits.filter((e) => e.action === 'rotate' || e.action === 'mirror'); @@ -210,6 +211,7 @@ class TransformManager implements EditToolManager { } onDeactivate() { + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.removeEventListener('mousemove', transformManager.handleMouseMove); this.reset(); @@ -553,6 +555,7 @@ class TransformManager implements EditToolManager { } document.body.style.userSelect = 'none'; + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('mouseup', () => this.handleMouseUp(), { passive: true }); } @@ -571,6 +574,7 @@ class TransformManager implements EditToolManager { } handleMouseUp() { + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.removeEventListener('mouseup', this.handleMouseUp); document.body.style.userSelect = ''; diff --git a/web/src/lib/managers/language-manager.svelte.ts b/web/src/lib/managers/language-manager.svelte.ts index c690197e1f..cf5352a8d2 100644 --- a/web/src/lib/managers/language-manager.svelte.ts +++ b/web/src/lib/managers/language-manager.svelte.ts @@ -13,10 +13,11 @@ class LanguageManager { rtl = $state(false); init() { - if (!this.initialized) { - this.initialized = true; - lang.subscribe((lang) => this.setLanguage(lang)); + if (this.initialized) { + return; } + this.initialized = true; + lang.subscribe((lang) => this.setLanguage(lang)); } setLanguage(code: string) { diff --git a/web/src/lib/managers/media-capabilities-manager.svelte.ts b/web/src/lib/managers/media-capabilities-manager.svelte.ts index ccabc6680d..96e2a783cc 100644 --- a/web/src/lib/managers/media-capabilities-manager.svelte.ts +++ b/web/src/lib/managers/media-capabilities-manager.svelte.ts @@ -89,4 +89,5 @@ class MediaCapabilitiesManager { } export const mediaCapabilitiesManager = new MediaCapabilitiesManager(); +// eslint-disable-next-line unicorn/no-top-level-side-effects mediaCapabilitiesManager.init(); diff --git a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts index 5c373c0437..a363a05959 100644 --- a/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts +++ b/web/src/lib/managers/timeline-manager/group-insertion-cache.svelte.ts @@ -19,10 +19,10 @@ export class GroupInsertionCache { } setTimelineDay(timelineDay: TimelineDay, { year, month, day }: TimelineDate) { - if (!this.#lookupCache[year]) { + if (!Object.hasOwn(this.#lookupCache, year)) { this.#lookupCache[year] = {}; } - if (!this.#lookupCache[year][month]) { + if (!Object.hasOwn(this.#lookupCache[year], month)) { this.#lookupCache[year][month] = {}; } this.#lookupCache[year][month][day] = timelineDay; diff --git a/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts index fc2902bb63..3f9854161f 100644 --- a/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/layout-support.svelte.ts @@ -41,8 +41,6 @@ export function layoutTimelineMonth(timelineManager: TimelineManager, month: Tim timelineDay.col = timelineDayCol++; timelineDay.start = cumulativeWidth; timelineDay.top = cumulativeHeight; - - cumulativeWidth += timelineDay.width + timelineManager.gap; } else { // Move to next row cumulativeHeight += currentRowHeight; @@ -57,8 +55,8 @@ export function layoutTimelineMonth(timelineManager: TimelineManager, month: Tim timelineDay.top = cumulativeHeight; timelineDayCol++; - cumulativeWidth += timelineDay.width + timelineManager.gap; } + cumulativeWidth += timelineDay.width + timelineManager.gap; currentRowHeight = timelineDay.height + timelineManager.headerHeight; } diff --git a/web/src/lib/managers/timeline-manager/internal/utils.svelte.ts b/web/src/lib/managers/timeline-manager/internal/utils.svelte.ts index a1b580a966..0dcf7346f6 100644 --- a/web/src/lib/managers/timeline-manager/internal/utils.svelte.ts +++ b/web/src/lib/managers/timeline-manager/internal/utils.svelte.ts @@ -13,7 +13,7 @@ export function updateObject(target: any, source: any): boolean { } const isDate = target[key] instanceof Date; if (typeof target[key] === 'object' && !isDate) { - updated = updated || updateObject(target[key], source[key]); + updated ||= updateObject(target[key], source[key]); } else { if (target[key] !== source[key]) { target[key] = source[key]; diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 6defe1d498..0b9c6d4b07 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -357,10 +357,7 @@ export class TimelineManager extends VirtualScrollManager { } async loadTimelineMonth(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise { - let cancelable = true; - if (options) { - cancelable = options.cancelable; - } + const cancelable = options?.cancelable ?? true; const timelineMonth = getTimelineMonthByDate(this, yearMonth); if (!timelineMonth) { return; @@ -517,10 +514,7 @@ export class TimelineManager extends VirtualScrollManager { // eslint-disable-next-line svelte/prefer-svelte-reactivity const idsToUpdate = new Set(cache.keys()); const result = this.#runAssetCallback(idsToUpdate, (asset) => void updateObject(asset, cache.get(asset.id))); - const notUpdated: TimelineAsset[] = []; - for (const assetId of result.notUpdated) { - notUpdated.push(cache.get(assetId)!); - } + const notUpdated: TimelineAsset[] = Array.from(result.notUpdated, (assetId) => cache.get(assetId)!); return notUpdated; } diff --git a/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts index 30896a97a4..58b36769b9 100644 --- a/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-month.svelte.ts @@ -145,21 +145,23 @@ export class TimelineMonth { const combinedMoveAssets: MoveAsset[][] = []; let index = timelineDays.length; while (index--) { - if (idsToProcess.size > 0) { - const group = timelineDays[index]; - const { moveAssets, processedIds, changedGeometry } = group.runAssetCallback(ids, callback); - if (moveAssets.length > 0) { - combinedMoveAssets.push(moveAssets); - } - idsToProcess = setDifference(idsToProcess, processedIds); - for (const id of processedIds) { - idsProcessed.add(id); - } - combinedChangedGeometry = combinedChangedGeometry || changedGeometry; - if (group.viewerAssets.length === 0) { - timelineDays.splice(index, 1); - combinedChangedGeometry = true; - } + if (idsToProcess.size === 0) { + continue; + } + + const group = timelineDays[index]; + const { moveAssets, processedIds, changedGeometry } = group.runAssetCallback(ids, callback); + if (moveAssets.length > 0) { + combinedMoveAssets.push(moveAssets); + } + idsToProcess = setDifference(idsToProcess, processedIds); + for (const id of processedIds) { + idsProcessed.add(id); + } + combinedChangedGeometry ||= changedGeometry; + if (group.viewerAssets.length === 0) { + timelineDays.splice(index, 1); + combinedChangedGeometry = true; } } return { @@ -195,7 +197,7 @@ export class TimelineMonth { ownerId: bucketAssets.ownerId[i], projectionType: bucketAssets.projectionType[i], ratio: bucketAssets.ratio[i], - stack: bucketAssets.stack?.[i] + stack: bucketAssets.stack?.at(i) ? { id: bucketAssets.stack[i]![0], primaryAssetId: bucketAssets.id[i], @@ -206,7 +208,7 @@ export class TimelineMonth { people: null, // People are not included in the bucket assets }; - if (bucketAssets.latitude?.[i] && bucketAssets.longitude?.[i]) { + if (bucketAssets.latitude?.at(i) && bucketAssets.longitude?.at(i)) { timelineAsset.latitude = bucketAssets.latitude?.[i]; timelineAsset.longitude = bucketAssets.longitude?.[i]; } diff --git a/web/src/lib/modals/AlbumPickerModal.svelte b/web/src/lib/modals/AlbumPickerModal.svelte index 1d96a04eff..561deeac23 100644 --- a/web/src/lib/modals/AlbumPickerModal.svelte +++ b/web/src/lib/modals/AlbumPickerModal.svelte @@ -137,6 +137,7 @@ break; } case 'Control': { + // eslint-disable-next-line unicorn/no-late-event-control e.preventDefault(); handleMultiSelect(); break; diff --git a/web/src/lib/modals/GeolocationPointPickerModal.svelte b/web/src/lib/modals/GeolocationPointPickerModal.svelte index 069f62d3ac..657f919433 100644 --- a/web/src/lib/modals/GeolocationPointPickerModal.svelte +++ b/web/src/lib/modals/GeolocationPointPickerModal.svelte @@ -85,8 +85,8 @@ // Try to parse coordinate pair from search input in the format `LATITUDE, LONGITUDE` as floats const coordinateParts = searchWord.split(',').map((part) => part.trim()); if (coordinateParts.length === 2) { - const coordinateLat = Number.parseFloat(coordinateParts[0]); - const coordinateLng = Number.parseFloat(coordinateParts[1]); + const coordinateLat = Number(coordinateParts[0]); + const coordinateLng = Number(coordinateParts[1]); if ( !Number.isNaN(coordinateLat) && @@ -106,18 +106,22 @@ searchPlaces({ name: searchWord }) .then((searchResult) => { // skip result when a newer search is happening - if (latestSearchTimeout === searchTimeout) { - places = searchResult; - showLoadingSpinner = false; + if (latestSearchTimeout !== searchTimeout) { + return; } + + places = searchResult; + showLoadingSpinner = false; }) .catch((error) => { // skip error when a newer search is happening - if (latestSearchTimeout === searchTimeout) { - places = []; - handleError(error, $t('errors.cant_search_places')); - showLoadingSpinner = false; + if (latestSearchTimeout !== searchTimeout) { + return; } + + places = []; + handleError(error, $t('errors.cant_search_places')); + showLoadingSpinner = false; }); }, timeDebounceOnSearch); latestSearchTimeout = searchTimeout; diff --git a/web/src/lib/modals/PersonMergeSuggestionModal.svelte b/web/src/lib/modals/PersonMergeSuggestionModal.svelte index 1471bc4a51..c449ed35cf 100644 --- a/web/src/lib/modals/PersonMergeSuggestionModal.svelte +++ b/web/src/lib/modals/PersonMergeSuggestionModal.svelte @@ -28,6 +28,7 @@ const changePersonToMerge = (newPerson: PersonResponseDto) => { const index = potentialMergePeople.indexOf(newPerson); + // eslint-disable-next-line unicorn/no-unreadable-array-destructuring [potentialMergePeople[index], personToBeMergedInto] = [personToBeMergedInto, potentialMergePeople[index]]; choosePersonToMerge = false; }; diff --git a/web/src/lib/modals/SearchFilterModal.svelte b/web/src/lib/modals/SearchFilterModal.svelte index 1eb88f67b6..058cdc8e6f 100644 --- a/web/src/lib/modals/SearchFilterModal.svelte +++ b/web/src/lib/modals/SearchFilterModal.svelte @@ -45,10 +45,8 @@ } const asFilter = (searchQuery: SmartSearchDto | MetadataSearchDto): SearchFilter => { - let query = ''; - if ('query' in searchQuery && searchQuery.query) { - query = searchQuery.query; - } + let query = 'query' in searchQuery && searchQuery.query ? searchQuery.query : ''; + if ('originalFileName' in searchQuery && searchQuery.originalFileName) { query = searchQuery.originalFileName; } diff --git a/web/src/lib/modals/ServerAboutModal.svelte b/web/src/lib/modals/ServerAboutModal.svelte index 412dbe4e20..6fc3ff99bc 100644 --- a/web/src/lib/modals/ServerAboutModal.svelte +++ b/web/src/lib/modals/ServerAboutModal.svelte @@ -61,7 +61,7 @@ {/if} - {#if info.buildImage && info.buildImage} + {#if info.buildImage && info.buildImageUrl} { describe(Route.continue.name, () => { beforeEach(() => { // @ts-expect-error - override location for testing + // eslint-disable-next-line unicorn/no-global-object-property-assignment globalThis.location = new URL('https://my.immich.server'); vi.spyOn(document, 'baseURI', 'get').mockReturnValue('https://my.immich.server/'); }); diff --git a/web/src/lib/route.ts b/web/src/lib/route.ts index 49e51928f6..d47340a34b 100644 --- a/web/src/lib/route.ts +++ b/web/src/lib/route.ts @@ -31,11 +31,7 @@ const asQueryString = ( return false; } - if (skipEmptyStrings && value === '') { - return false; - } - - return true; + return !(skipEmptyStrings && value === ''); }) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`); diff --git a/web/src/lib/services/database-backups.service.ts b/web/src/lib/services/database-backups.service.ts index 4f2b97d72e..6a379ce287 100644 --- a/web/src/lib/services/database-backups.service.ts +++ b/web/src/lib/services/database-backups.service.ts @@ -91,7 +91,7 @@ export const handleDeleteDatabaseBackup = async (...filenames: string[]) => { }; export const handleDownloadDatabaseBackup = (filename: string) => { - location.href = getBaseUrl() + '/admin/database-backups/' + filename; + location.assign(getBaseUrl() + '/admin/database-backups/' + filename); }; export const handleUploadDatabaseBackup = async () => { diff --git a/web/src/lib/services/shared-link.service.ts b/web/src/lib/services/shared-link.service.ts index 3699c0ed75..885caee7d8 100644 --- a/web/src/lib/services/shared-link.service.ts +++ b/web/src/lib/services/shared-link.service.ts @@ -61,7 +61,7 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin export const asUrl = (sharedLink: SharedLinkResponseDto) => { const path = Route.viewSharedLink(sharedLink); - return new URL(path, serverConfigManager.value.externalDomain || globalThis.location.origin).href; + return new URL(path, serverConfigManager.value.externalDomain || location.origin).href; }; export const handleCreateSharedLink = async (dto: SharedLinkCreateDto) => { diff --git a/web/src/lib/services/system-config.service.ts b/web/src/lib/services/system-config.service.ts index cc7c961d1e..b8d7a0f1a7 100644 --- a/web/src/lib/services/system-config.service.ts +++ b/web/src/lib/services/system-config.service.ts @@ -64,7 +64,7 @@ export const handleSystemConfigSave = async (update: Partial) = }; export const handleUploadConfig = () => { - const input = globalThis.document.createElement('input'); + const input = document.createElement('input'); input.setAttribute('type', 'file'); input.setAttribute('accept', '.json'); input.setAttribute('style', 'display: none'); @@ -83,6 +83,6 @@ export const handleUploadConfig = () => { .catch((error) => console.error('Error handling JSON config upload', error)) .finally(() => input.remove()); }); - globalThis.document.body.append(input); + document.body.append(input); input.click(); }; diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts index f386ba9c86..51b39b9d22 100644 --- a/web/src/lib/services/user-admin.service.ts +++ b/web/src/lib/services/user-admin.service.ts @@ -159,11 +159,11 @@ export const handleNavigateUserAdmin = async (user: UserAdminResponseDto) => { const generatePassword = (length: number = 16) => { let generatedPassword = ''; - const characterSet = '0123456789' + 'abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + ',.-{}+!#$%/()=?'; + const characterSet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,.-{}+!#$%/()=?'; for (let i = 0; i < length; i++) { let randomNumber = crypto.getRandomValues(new Uint32Array(1))[0]; - randomNumber = randomNumber / 2 ** 32; + randomNumber /= 2 ** 32; randomNumber = Math.floor(randomNumber * characterSet.length); generatedPassword += characterSet[randomNumber]; diff --git a/web/src/lib/services/workflow.service.ts b/web/src/lib/services/workflow.service.ts index c81b1b2706..1fba144604 100644 --- a/web/src/lib/services/workflow.service.ts +++ b/web/src/lib/services/workflow.service.ts @@ -3,8 +3,6 @@ import { deleteWorkflow, updateWorkflow, WorkflowTrigger, - type AlbumResponseDto, - type PersonResponseDto, type WorkflowCreateDto, type WorkflowResponseDto, type WorkflowUpdateDto, @@ -32,9 +30,6 @@ import { copyToClipboard, downloadJson } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; -export type PickerSubType = 'album-picker' | 'people-picker'; -export type PickerMetadata = AlbumResponseDto | PersonResponseDto | AlbumResponseDto[] | PersonResponseDto[]; - export const getWorkflowsActions = ($t: MessageFormatter) => { const Create: ActionItem = { title: $t('create_workflow'), diff --git a/web/src/lib/stores/keyboard-manager.svelte.ts b/web/src/lib/stores/keyboard-manager.svelte.ts index e2ed5bae8c..774acc6874 100644 --- a/web/src/lib/stores/keyboard-manager.svelte.ts +++ b/web/src/lib/stores/keyboard-manager.svelte.ts @@ -8,8 +8,11 @@ class KeyboardManager { if (globalThis.window === undefined) { return; } + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('keydown', this.#update); + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('keyup', this.#update); + // eslint-disable-next-line unicorn/no-unnecessary-global-this globalThis.addEventListener('blur', this.#clear); } diff --git a/web/src/lib/stores/slideshow.store.ts b/web/src/lib/stores/slideshow.store.ts index a576d8f37b..17ba266ee1 100644 --- a/web/src/lib/stores/slideshow.store.ts +++ b/web/src/lib/stores/slideshow.store.ts @@ -58,10 +58,12 @@ function createSlideshowStore() { set: (value: boolean) => { // Trigger an action whenever the restartProgress is set to true. Automatically // reset the restart state after that - if (value) { - restartState.set(true); - restartState.set(false); + if (!value) { + return; } + + restartState.set(true); + restartState.set(false); }, }, stopProgress: { @@ -69,10 +71,12 @@ function createSlideshowStore() { set: (value: boolean) => { // Trigger an action whenever the stopProgress is set to true. Automatically // reset the stop state after that - if (value) { - stopState.set(true); - stopState.set(false); + if (!value) { + return; } + + stopState.set(true); + stopState.set(false); }, }, slideshowNavigation, diff --git a/web/src/lib/stores/upload.ts b/web/src/lib/stores/upload.ts index bcbf7792e8..04a8a45bee 100644 --- a/web/src/lib/stores/upload.ts +++ b/web/src/lib/stores/upload.ts @@ -23,7 +23,7 @@ function createUploadStore() { const addItem = (newAsset: UploadAsset) => { uploadAssets.update(($assets) => { - const duplicate = $assets.find((asset) => asset.id === newAsset.id); + const duplicate = $assets.some((asset) => asset.id === newAsset.id); if (duplicate) { return $assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset)); } diff --git a/web/src/lib/stores/user.svelte.ts b/web/src/lib/stores/user.svelte.ts index 98454e4249..e5bb4dfa27 100644 --- a/web/src/lib/stores/user.svelte.ts +++ b/web/src/lib/stores/user.svelte.ts @@ -30,6 +30,7 @@ const reset = () => { Object.assign(userInteraction, defaultUserInteraction); }; +// eslint-disable-next-line unicorn/no-top-level-side-effects eventManager.on({ AlbumCreate: () => resetRecentAlbums(), AlbumUpdate: () => resetRecentAlbums(), diff --git a/web/src/lib/stores/websocket.ts b/web/src/lib/stores/websocket.ts index fc33812973..6c4ac1cfd6 100644 --- a/web/src/lib/stores/websocket.ts +++ b/web/src/lib/stores/websocket.ts @@ -60,6 +60,7 @@ export const websocketStore = { export const websocketEvents = createEventEmitter(websocket); +// eslint-disable-next-line unicorn/no-top-level-side-effects websocket .on('connect', () => { eventManager.emit('WebsocketConnect'); @@ -113,16 +114,18 @@ export const waitForWebsocketEvent = ( return new Promise((resolve, reject) => { // @ts-expect-error: The typings are weird on this? const cleanup = websocketEvents.on(event, (...args: Parameters) => { - if (!predicate || predicate(...args)) { - cleanup(); - clearTimeout(timer); - resolve(args); + if (predicate && !predicate(...args)) { + return; } + + cleanup(); + clearTimeout(timer); + resolve(args); }); const timer = setTimeout(() => { cleanup(); - reject(new Error(`Timeout waiting for event: ${String(event)}`)); + reject(new Error(`Timeout waiting for event: ${event}`)); }, timeout); }); }; diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 0e31782164..3aecb5df59 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -109,11 +109,10 @@ export const uploadRequest = async (options: UploadRequestOptions): Promise<{ }); xhr.addEventListener('load', () => { + unsubscribe(); if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) { - unsubscribe(); resolve({ data: xhr.response as T, status: xhr.status }); } else { - unsubscribe(); reject(new ApiError(xhr.statusText, xhr.status, xhr.response)); } }); @@ -326,9 +325,9 @@ export const oauth = { authorize: async (location: Location) => { const $t = get(t); try { - const redirectUri = location.href.split('?')[0]; + const redirectUri = location.href.split('?', 1)[0]; const { url } = await startOAuth({ oAuthConfigDto: { redirectUri } }); - globalThis.location.href = url; + globalThis.location.assign(url); return true; } catch (error) { handleError(error, $t('errors.unable_to_login_with_oauth')); @@ -430,7 +429,8 @@ export const isEnabled = ({ $if }: IfLike) => $if?.() ?? true; export const transformToTitleCase = (text: string) => { if (text.length === 0) { return text; - } else if (text.length === 1) { + } + if (text.length === 1) { return text.charAt(0).toUpperCase(); } diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index 59c431557b..3624b34f93 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -68,19 +68,21 @@ const undoDeleteAssets = async (onUndoDelete: OnUndoDelete, assets: TimelineAsse * @param {StackResponse} stackResponse - The stack response containing the stack and assets to delete. */ export function updateStackedAssetInTimeline(timelineManager: TimelineManager, { stack, toDeleteIds }: StackResponse) { - if (stack != undefined) { - timelineManager.update( - [stack.primaryAssetId], - (asset) => - (asset.stack = { - id: stack.id, - primaryAssetId: stack.primaryAssetId, - assetCount: stack.assets.length, - }), - ); - - timelineManager.removeAssets(toDeleteIds); + if (stack == undefined) { + return; } + + timelineManager.update( + [stack.primaryAssetId], + (asset) => + (asset.stack = { + id: stack.id, + primaryAssetId: stack.primaryAssetId, + assetCount: stack.assets.length, + }), + ); + + timelineManager.removeAssets(toDeleteIds); } /** diff --git a/web/src/lib/utils/adaptive-image-loader.svelte.ts b/web/src/lib/utils/adaptive-image-loader.svelte.ts index 8d9a5f79f4..c82d17ec98 100644 --- a/web/src/lib/utils/adaptive-image-loader.svelte.ts +++ b/web/src/lib/utils/adaptive-image-loader.svelte.ts @@ -86,6 +86,7 @@ export class AdaptiveImageLoader { const config = this.qualityConfigs[quality]; + // eslint-disable-next-line unicorn/no-computed-property-existence-check if (!this.status.urls[quality]) { return; } @@ -129,6 +130,7 @@ export class AdaptiveImageLoader { return false; } + // eslint-disable-next-line unicorn/no-computed-property-existence-check if (this.status.urls[quality]) { return true; } diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index fdfc15e636..5b8de3cfb0 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -92,13 +92,13 @@ export const downloadArchive = async (fileName: string, options: Omit 1 ? `+${index + 1}` : ''; - const archiveName = fileName.replace('.zip', `${suffix}-${DateTime.now().toFormat('yyyyLLdd_HHmmss')}.zip`); + const archiveName = fileName.replace('.zip', () => `${suffix}-${DateTime.now().toFormat('yyyyLLdd_HHmmss')}.zip`); const queryParams = asQueryString(authManager.params); - let downloadKey = `${archiveName} `; - if (downloadInfo.archives.length > 1) { - downloadKey = `${archiveName} (${index + 1}/${downloadInfo.archives.length})`; - } + const downloadKey = + downloadInfo.archives.length > 1 + ? `${archiveName} (${index + 1}/${downloadInfo.archives.length})` + : `${archiveName} `; const abort = new AbortController(); downloadManager.add(downloadKey, archive.size, abort); @@ -131,7 +131,7 @@ export const downloadArchive = async (fileName: string, options: Omit { heicImg.src = 'data:image/heic;base64,AAAAGGZ0eXBoZWljAAAAAG1pZjFoZWljAAABrW1ldGEAAAAAAAAAIWhkbHIAAAAAAAAAAHBpY3QAAAAAAAAAAAAAAAAAAAAADnBpdG0AAAAAAAIAAAAQaWRhdAAAAAAAAQABAAAAOGlsb2MBAAAAREAAAgABAAAAAAAAAc0AAQAAAAAAAAAsAAIAAQAAAAAAAAABAAAAAAAAAAgAAAA4aWluZgAAAAAAAgAAABVpbmZlAgAAAQABAABodmMxAAAAABVpbmZlAgAAAAACAABncmlkAAAAANhpcHJwAAAAtmlwY28AAAB2aHZjQwEDcAAAAAAAAAAAAB7wAPz9+PgAAA8DIAABABhAAQwB//8DcAAAAwCQAAADAAADAB66AkAhAAEAKkIBAQNwAAADAJAAAAMAAAMAHqAggQWW6q6a5uBAQMCAAAADAIAAAAMAhCIAAQAGRAHBc8GJAAAAFGlzcGUAAAAAAAAAAQAAAAEAAAAUaXNwZQAAAAAAAABAAAAAQAAAABBwaXhpAAAAAAMICAgAAAAaaXBtYQAAAAAAAAACAAECgQMAAgIChAAAABppcmVmAAAAAAAAAA5kaW1nAAIAAQABAAAANG1kYXQAAAAoKAGvCchMZYA50NoPIfzz81Qfsm577GJt3lf8kLAr+NbNIoeRR7JeYA=='; // Small valid HEIC/HEIF image } +// eslint-disable-next-line unicorn/no-top-level-side-effects void addSupportedMimeTypes(); /** diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index 13a50f33ec..aa6ffccdbf 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -28,10 +28,12 @@ export const authenticate = async (url: URL, options?: AuthOptions) => { }; export const requestServerInfo = async () => { - if (authManager.authenticated) { - const data = await getStorage(); - userInteraction.serverInfo = data; + if (!authManager.authenticated) { + return; } + + const data = await getStorage(); + userInteraction.serverInfo = data; }; export const getAccountAge = (): number => { diff --git a/web/src/lib/utils/byte-units.ts b/web/src/lib/utils/byte-units.ts index 7a5576d7f5..e903c7829e 100644 --- a/web/src/lib/utils/byte-units.ts +++ b/web/src/lib/utils/byte-units.ts @@ -23,7 +23,7 @@ const byteUnits = [ByteUnit.B, ByteUnit.KiB, ByteUnit.MiB, ByteUnit.GiB, ByteUni export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, ByteUnit] { const magnitude = Math.floor(Math.log(bytes === 0 ? 1 : bytes) / Math.log(1024)); - return [Number.parseFloat((bytes / 1024 ** magnitude).toFixed(maxPrecision)), byteUnits[magnitude]]; + return [Number((bytes / 1024 ** magnitude).toFixed(maxPrecision)), byteUnits[magnitude]]; } /** diff --git a/web/src/lib/utils/cast/gcast-destination.svelte.ts b/web/src/lib/utils/cast/gcast-destination.svelte.ts index f2a51c5c6a..71766366e1 100644 --- a/web/src/lib/utils/cast/gcast-destination.svelte.ts +++ b/web/src/lib/utils/cast/gcast-destination.svelte.ts @@ -41,11 +41,12 @@ export class GCastDestination implements ICastDestination { return; } + // eslint-disable-next-line unicorn/no-global-object-property-assignment window['__onGCastApiAvailable'] = (isAvailable: boolean) => { resolve(isAvailable); }; - if (!document.querySelector(`script[src="${FRAMEWORK_LINK}"]`)) { + if (!document.querySelector(`script[src="${CSS.escape(FRAMEWORK_LINK)}"]`)) { const script = document.createElement('script'); script.src = FRAMEWORK_LINK; document.body.append(script); diff --git a/web/src/lib/utils/date-time.ts b/web/src/lib/utils/date-time.ts index 17704aff22..7cfeadb9dc 100644 --- a/web/src/lib/utils/date-time.ts +++ b/web/src/lib/utils/date-time.ts @@ -27,23 +27,23 @@ export const getShortDateRange = (startTimestamp: string, endTimestamp: string) // Same year and month. // e.g.: aug. 2024 return endDateLocalized; - } else { - // Same year but different month. - // e.g.: jul. - sept. 2024 - const startMonthLocalized = startDate.toLocaleString({ - month: 'short', - }); - return `${startMonthLocalized} - ${endDateLocalized}`; } - } else { - // Different year. - // e.g.: feb. 2021 - sept. 2024 - const startDateLocalized = startDate.toLocaleString({ + + // Same year but different month. + // e.g.: jul. - sept. 2024 + const startMonthLocalized = startDate.toLocaleString({ month: 'short', - year: 'numeric', }); - return `${startDateLocalized} - ${endDateLocalized}`; + return `${startMonthLocalized} - ${endDateLocalized}`; } + + // Different year. + // e.g.: feb. 2021 - sept. 2024 + const startDateLocalized = startDate.toLocaleString({ + month: 'short', + year: 'numeric', + }); + return `${startDateLocalized} - ${endDateLocalized}`; }; const formatDate = (date?: string) => { diff --git a/web/src/lib/utils/duplicate-utils.ts b/web/src/lib/utils/duplicate-utils.ts index dac4e9dc54..86b221906f 100644 --- a/web/src/lib/utils/duplicate-utils.ts +++ b/web/src/lib/utils/duplicate-utils.ts @@ -209,7 +209,7 @@ const metadataFields = [ icon: mdiPhoneRotateLandscape, titleKey: 'orientation', keys: ['orientation'], - render: (asset, $t) => String(asset.exifInfo?.orientation || $t('unknown')), + render: (asset, $t) => asset.exifInfo?.orientation || $t('unknown'), }, { icon: mdiPanorama, @@ -240,7 +240,7 @@ const normalizeForComparison = (key: MetadataFieldKey, value: unknown): unknown return value; } - if (key === 'fileCreatedAt' || key === 'fileModifiedAt' || key === 'dateTimeOriginal' || key === 'modifyDate') { + if (['fileCreatedAt', 'fileModifiedAt', 'dateTimeOriginal', 'modifyDate'].includes(key)) { const dateTime = DateTime.fromISO(String(value)); return dateTime.isValid ? dateTime.toISO() : String(value); } @@ -273,7 +273,7 @@ const getValueForAsset = (asset: AssetResponseDto, key: MetadataFieldKey): unkno return getAssetResolution(asset); } default: { - if (asset.exifInfo && key in asset.exifInfo) { + if (asset.exifInfo && Object.hasOwn(asset.exifInfo, key)) { return asset.exifInfo[key as keyof typeof asset.exifInfo]; } return undefined; diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 0d0630a017..10ed7b08d3 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -131,7 +131,7 @@ export const fileUploadHandler = async ({ }; function getDeviceAssetId(asset: File) { - return 'web' + '-' + asset.name + '-' + asset.lastModified; + return 'web-' + asset.name + '-' + asset.lastModified; } function hashFile(file: File): Promise { diff --git a/web/src/lib/utils/handle-error.ts b/web/src/lib/utils/handle-error.ts index 6a1634b711..fe1f7a6a0c 100644 --- a/web/src/lib/utils/handle-error.ts +++ b/web/src/lib/utils/handle-error.ts @@ -47,7 +47,7 @@ export function handleError(error: unknown, localizedMessage: string, options?: try { let serverMessage = getServerErrorMessage(error); if (serverMessage) { - serverMessage = `${String(serverMessage).slice(0, 75)}\n(Immich Server Error)`; + serverMessage = `${serverMessage.slice(0, 75)}\n(Immich Server Error)`; } const errorMessage = serverMessage || localizedMessage; diff --git a/web/src/lib/utils/invocationTracker.ts b/web/src/lib/utils/invocationTracker.ts index 88c395a4ad..ada3b12d0e 100644 --- a/web/src/lib/utils/invocationTracker.ts +++ b/web/src/lib/utils/invocationTracker.ts @@ -30,10 +30,7 @@ export class InvocationTracker { * @throws {Error} If the invocation is no longer valid */ isStillValid: () => { - if (invocation !== this.invocationsStarted) { - return false; - } - return true; + return invocation === this.invocationsStarted; }, /** diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index cfeb22f560..91513911f1 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -8,7 +8,7 @@ export type AssetGridRouteSearchParams = { at: string | null | undefined; }; export const isExternalUrl = (url: string): boolean => { - return new URL(url, globalThis.location.href).origin !== globalThis.location.origin; + return new URL(url, location.href).origin !== location.origin; }; export const isPhotosRoute = (route?: string | null) => !!route?.startsWith('/(user)/photos/[[assetId=id]]'); @@ -33,11 +33,10 @@ function currentUrlWithoutAsset() { // off / instead of a subpath, unlike every other asset-containing route. if (isPhotosRoute(page.route.id)) { return Route.photos() + page.url.search; - } else if (isSharedLinkSlugRoute(page.route.id)) { - return Route.viewSharedLink({ slug: page.data.slug, key: page.data.key }) + page.url.search; - } else { - return page.url.pathname.replace(/(\/photos.*)$/, '') + page.url.search; } + return isSharedLinkSlugRoute(page.route.id) + ? Route.viewSharedLink({ slug: page.data.slug, key: page.data.key }) + page.url.search + : page.url.pathname.replace(/(\/photos.*)$/, '') + page.url.search; } export function currentUrlReplaceAssetId(assetId: string) { @@ -133,7 +132,8 @@ async function navigateAssetGridRoute(route: AssetGridRoute, options?: NavOption export function navigate(change: ImmichRoute, options?: NavOptions): Promise { if (isAssetGridRoute(change)) { return navigateAssetGridRoute(change, options); - } else if (isAssetRoute(change)) { + } + if (isAssetRoute(change)) { return navigateAssetRoute(change, options); } // future navigation requests here @@ -141,20 +141,22 @@ export function navigate(change: ImmichRoute, options?: NavOptions): Promise { - if (url.searchParams.has(queryParam)) { - url.searchParams.delete(queryParam); - await goto(url, { keepFocus: true }); + if (!url.searchParams.has(queryParam)) { + return; } + + url.searchParams.delete(queryParam); + await goto(url, { keepFocus: true }); }; export const getQueryValue = (queryKey: string) => { - const url = globalThis.location.href; + const url = location.href; const urlObject = new URL(url); return urlObject.searchParams.get(queryKey); }; export const setQueryValue = async (queryKey: string, queryValue: string) => { - const url = globalThis.location.href; + const url = location.href; const urlObject = new URL(url); urlObject.searchParams.set(queryKey, queryValue); await goto(urlObject, { keepFocus: true }); diff --git a/web/src/lib/utils/ocr-utils.ts b/web/src/lib/utils/ocr-utils.ts index 997027179b..e21cfb3fb2 100644 --- a/web/src/lib/utils/ocr-utils.ts +++ b/web/src/lib/utils/ocr-utils.ts @@ -17,7 +17,7 @@ export type OcrBox = { }; const CJK_PATTERN = - /[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uAC00-\uD7AF\uFF00-\uFFEF]/; + /[\u{3000}-\u{303F}\u{3040}-\u{309F}\u{30A0}-\u{30FF}\u{3400}-\u{4DBF}\u{4E00}-\u{9FFF}\u{F900}-\u{FAFF}\u{AC00}-\u{D7AF}\u{FF00}-\u{FFEF}]/u; const VERTICAL_ASPECT_RATIO = 1.5; diff --git a/web/src/lib/utils/string-utils.ts b/web/src/lib/utils/string-utils.ts index 0170c34737..d92ad0a923 100644 --- a/web/src/lib/utils/string-utils.ts +++ b/web/src/lib/utils/string-utils.ts @@ -1,5 +1,5 @@ export const removeAccents = (str: string) => { - return str.normalize('NFD').replaceAll(/[\u0300-\u036F]/g, ''); + return str.normalize('NFD').replaceAll(/[\u{300}-\u{36F}]/gu, ''); }; export const normalizeSearchString = (str: string) => { diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index a54a9a6e26..018d7d34ef 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -95,8 +95,8 @@ export const fromTimelinePlainYearMonth = (timelineYearMonth: TimelineYearMonth) ) as DateTime; export const toISOYearMonthUTC = ({ year, month }: TimelineYearMonth): string => { - const yearFull = `${year}`.padStart(4, '0'); - const monthFull = `${month}`.padStart(2, '0'); + const yearFull = String(year).padStart(4, '0'); + const monthFull = String(month).padStart(2, '0'); return `${yearFull}-${monthFull}-01T00:00:00.000Z`; }; diff --git a/web/src/lib/utils/wakelock.svelte.ts b/web/src/lib/utils/wakelock.svelte.ts index 4c69a397a5..78e0ee1daa 100644 --- a/web/src/lib/utils/wakelock.svelte.ts +++ b/web/src/lib/utils/wakelock.svelte.ts @@ -29,14 +29,16 @@ export async function acquireWakeLock() { } export async function releaseWakeLock() { - if (sentinel) { - const toReleaseSentinel = sentinel; - // Unset first to avoid race condition after await - sentinel = undefined; - - // eslint-disable-next-line tscompat/tscompat - await toReleaseSentinel.release(); + if (!sentinel) { + return; } + + const toReleaseSentinel = sentinel; + // Unset first to avoid race condition after await + sentinel = undefined; + + // eslint-disable-next-line tscompat/tscompat + await toReleaseSentinel.release(); } if (isSupported) { diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 42072c122b..032a58ad52 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -178,11 +178,13 @@ }; const updateThumbnailUsingCurrentSelection = async () => { - if (assetMultiSelectManager.assets.length === 1) { - const [firstAsset] = assetMultiSelectManager.assets; - assetMultiSelectManager.clear(); - await updateThumbnail(firstAsset.id); + if (assetMultiSelectManager.assets.length !== 1) { + return; } + + const [firstAsset] = assetMultiSelectManager.assets; + assetMultiSelectManager.clear(); + await updateThumbnail(firstAsset.id); }; const updateThumbnail = async (assetId: string) => { @@ -274,10 +276,12 @@ }; const onAlbumDelete = async ({ id }: AlbumResponseDto) => { - if (id === album.id) { - await goto(Route.albums()); - viewMode = AlbumPageViewMode.VIEW; + if (id !== album.id) { + return; } + + await goto(Route.albums()); + viewMode = AlbumPageViewMode.VIEW; }; const onAlbumAddAssets = async ({ albumIds }: { albumIds: string[] }) => { diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8d06d68758..dd4e776d33 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -31,10 +31,12 @@ const options = { visibility: AssetVisibility.Archive }; const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; const handleSetVisibility = (assetIds: string[]) => { diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index c69bd75ea9..597a1e43e1 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -34,10 +34,12 @@ const options = { isFavorite: true, withStacked: true }; const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; const handleSetVisibility = (assetIds: string[]) => { diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte index 460f84f572..ec0afaeba0 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -32,10 +32,12 @@ const options = { visibility: AssetVisibility.Locked }; const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; const handleMoveOffLockedFolder = (assetIds: string[]) => { diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8e355d3efb..7b1c4d97a7 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -27,10 +27,12 @@ }); const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 9a672e6a27..733c1f556a 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -71,7 +71,7 @@ if (pagesToLoad) { handlePromiseError( Promise.all( - Array.from({ length: pagesToLoad }).map((_, i) => { + Array.from({ length: pagesToLoad }, (_, i) => { return getAllPeople({ withHidden: true, page: startingPage + i }); }), ).then((pages) => { diff --git a/web/src/routes/(user)/people/PeopleInfiniteScroll.svelte b/web/src/routes/(user)/people/PeopleInfiniteScroll.svelte index 54182018c4..0fc93b625a 100644 --- a/web/src/routes/(user)/people/PeopleInfiniteScroll.svelte +++ b/web/src/routes/(user)/people/PeopleInfiniteScroll.svelte @@ -20,10 +20,12 @@ }); $effect(() => { - if (lastPersonContainer) { - intersectionObserver.disconnect(); - intersectionObserver.observe(lastPersonContainer); + if (!lastPersonContainer) { + return; } + + intersectionObserver.disconnect(); + intersectionObserver.observe(lastPersonContainer); }); diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/MergeFaceSelector.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/MergeFaceSelector.svelte index 2fac6d0d11..35a30a472d 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/MergeFaceSelector.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/MergeFaceSelector.svelte @@ -37,6 +37,7 @@ onMount(handleSearch); const handleSwapPeople = async () => { + // eslint-disable-next-line unicorn/no-unreadable-array-destructuring [person, selectedPeople[0]] = [selectedPeople[0], person]; await goto(Route.viewPerson(person, { previousRoute: Route.people(), action: 'merge' })); }; diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/PeopleList.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/PeopleList.svelte index ed2a088f6e..9d3a174e9d 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/PeopleList.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/PeopleList.svelte @@ -20,8 +20,8 @@ let name = $state(''); const showPeople = $derived( - (name ? searchedPeopleLocal : people).filter( - (person) => !peopleToNotShow.some((unselectedPerson) => unselectedPerson.id === person.id), + (name ? searchedPeopleLocal : people).filter((person) => + peopleToNotShow.every((unselectedPerson) => unselectedPerson.id !== person.id), ), ); diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/UnmergeFaceSelector.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/UnmergeFaceSelector.svelte index 67a5114adb..672c4a94b7 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/UnmergeFaceSelector.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/UnmergeFaceSelector.svelte @@ -39,11 +39,10 @@ let peopleToNotShow = $derived(selectedPerson ? [personAssets, selectedPerson] : [personAssets]); - const selectedPeople: AssetFaceUpdateItem[] = []; - - for (const assetId of assetIds) { - selectedPeople.push({ assetId, personId: personAssets.id }); - } + const selectedPeople: AssetFaceUpdateItem[] = Array.from(assetIds, (assetId) => ({ + assetId, + personId: personAssets.id, + })); onMount(async () => { const data = await getAllPeople({ withHidden: false }); diff --git a/web/src/routes/(user)/people/manage/+page.svelte b/web/src/routes/(user)/people/manage/+page.svelte index 4c311f638c..692d41ebe6 100644 --- a/web/src/routes/(user)/people/manage/+page.svelte +++ b/web/src/routes/(user)/people/manage/+page.svelte @@ -30,11 +30,8 @@ const getNextVisibility = (toggleVisibility: ToggleVisibility) => { if (toggleVisibility === ToggleVisibility.SHOW_ALL) { return ToggleVisibility.HIDE_UNNANEMD; - } else if (toggleVisibility === ToggleVisibility.HIDE_UNNANEMD) { - return ToggleVisibility.HIDE_ALL; - } else { - return ToggleVisibility.SHOW_ALL; } + return toggleVisibility === ToggleVisibility.HIDE_UNNANEMD ? ToggleVisibility.HIDE_ALL : ToggleVisibility.SHOW_ALL; }; const handleToggleVisibility = () => { diff --git a/web/src/routes/(user)/places/PlacesList.svelte b/web/src/routes/(user)/places/PlacesList.svelte index e815c1c236..2f2dfed9ca 100644 --- a/web/src/routes/(user)/places/PlacesList.svelte +++ b/web/src/routes/(user)/places/PlacesList.svelte @@ -56,11 +56,8 @@ // We make sure empty albums stay at the end of the list if (a === unknownCountry) { return 1; - } else if (b === unknownCountry) { - return -1; - } else { - return a.localeCompare(b); } + return b === unknownCountry ? -1 : a.localeCompare(b); }); return sortedByCountryName.map(([country, places]) => ({ diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 27b9eeea61..228cd58c3a 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -32,10 +32,12 @@ } const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; const { Empty, RestoreAll } = $derived(getTrashActions($t)); diff --git a/web/src/routes/(user)/user-settings/DownloadSettings.svelte b/web/src/routes/(user)/user-settings/DownloadSettings.svelte index f201991ef1..e4cb1f0d63 100644 --- a/web/src/routes/(user)/user-settings/DownloadSettings.svelte +++ b/web/src/routes/(user)/user-settings/DownloadSettings.svelte @@ -11,7 +11,7 @@ import { fade } from 'svelte/transition'; let archiveSize = $state(convertFromBytes(authManager.preferences.download.archiveSize || 4, ByteUnit.GiB)); - let includeEmbeddedVideos = $state(authManager.preferences.download.includeEmbeddedVideos || false); + let includeEmbeddedVideos = $state(authManager.preferences.download.includeEmbeddedVideos); const handleSave = async () => { try { diff --git a/web/src/routes/(user)/user-settings/OauthSettings.svelte b/web/src/routes/(user)/user-settings/OauthSettings.svelte index 6e63ef38c7..0da42b2b2d 100644 --- a/web/src/routes/(user)/user-settings/OauthSettings.svelte +++ b/web/src/routes/(user)/user-settings/OauthSettings.svelte @@ -12,10 +12,10 @@ let loading = $state(true); onMount(async () => { - if (oauth.isCallback(globalThis.location)) { + if (oauth.isCallback(location)) { try { loading = true; - const response = await oauth.link(globalThis.location); + const response = await oauth.link(location); authManager.setUser(response); toastManager.primary($t('linked_oauth_account')); } catch (error) { @@ -50,9 +50,7 @@ {#if authManager.user.oauthId} {:else} - + {/if} {/if}
diff --git a/web/src/routes/(user)/user-settings/UserSettingsList.svelte b/web/src/routes/(user)/user-settings/UserSettingsList.svelte index 97e2b366c6..91c32560d8 100644 --- a/web/src/routes/(user)/user-settings/UserSettingsList.svelte +++ b/web/src/routes/(user)/user-settings/UserSettingsList.svelte @@ -43,8 +43,7 @@ let { keys = $bindable([]), sessions = $bindable([]) }: Props = $props(); let oauthOpen = - oauth.isCallback(globalThis.location) || - $page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenQueryParam.OAUTH; + oauth.isCallback(location) || $page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenQueryParam.OAUTH; { const indexParam = page.url.searchParams.get('index') ?? '0'; - const parsedIndex = Number.parseInt(indexParam, 10); + const parsedIndex = Math.trunc(Number(indexParam)); return correctDuplicatesIndex(Number.isNaN(parsedIndex) ? 0 : parsedIndex); })(), ); diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte index f517b6f8f5..d6a5901fcf 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/DuplicatesCompareControl.svelte @@ -56,7 +56,7 @@ }); const onRandom = async () => { - if (assets.length <= 0) { + if (assets.length === 0) { return; } const index = Math.floor(Math.random() * assets.length); diff --git a/web/src/routes/(user)/utilities/geolocation/+page.svelte b/web/src/routes/(user)/utilities/geolocation/+page.svelte index d7c83bb042..4613a214e9 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.svelte +++ b/web/src/routes/(user)/utilities/geolocation/+page.svelte @@ -98,10 +98,12 @@ point = selected; }; const handleEscape = () => { - if (assetMultiSelectManager.selectionActive) { - assetMultiSelectManager.clear(); + if (!assetMultiSelectManager.selectionActive) { return; } + + assetMultiSelectManager.clear(); + return; }; type AssetPoint = { latitude: number; longitude: number }; diff --git a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte index 21ebd40f37..f4c0a93655 100644 --- a/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/large-files/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -28,7 +28,7 @@ }); const onRandom = async () => { - if (assets.length <= 0) { + if (assets.length === 0) { return undefined; } const index = Math.floor(Math.random() * assets.length); diff --git a/web/src/routes/(user)/workflows/[workflowId]/+page.svelte b/web/src/routes/(user)/workflows/[workflowId]/+page.svelte index 63942153d5..13bc645979 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/+page.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/+page.svelte @@ -189,11 +189,13 @@ }; const onWorkflowUpdate = async (response: WorkflowResponseDto) => { - if (id === response.id) { - data.workflow = response; - savedWorkflow = cloneDeep(response); - await invalidate('workflow:data'); + if (id !== response.id) { + return; } + + data.workflow = response; + savedWorkflow = cloneDeep(response); + await invalidate('workflow:data'); }; const onWorkflowDelete = async (response: WorkflowResponseDto) => { @@ -252,10 +254,12 @@ } void confirmNavigation().then((confirmed) => { - if (confirmed) { - allowNavigation = true; - void goto(to.url); + if (!confirmed) { + return; } + + allowNavigation = true; + void goto(to.url); }); }); diff --git a/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte b/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte index 3d6e29e896..16e9e1b041 100644 --- a/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte +++ b/web/src/routes/(user)/workflows/[workflowId]/WorkflowSummary.svelte @@ -53,7 +53,7 @@ const asciiSummary = $derived.by(() => { const lines: string[] = []; const title = workflow.name ?? $t('no_name'); - lines.push(`${title}`); + lines.push(title); if (workflow.description) { lines.push(workflow.description); } diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index ec20a1d866..f07b2774a5 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -250,10 +250,7 @@ {#if page.data.meta.imageUrl} {/if} {/if} diff --git a/web/src/routes/admin/maintenance/+page.svelte b/web/src/routes/admin/maintenance/+page.svelte index 44f53c8b6a..f7af961cbb 100644 --- a/web/src/routes/admin/maintenance/+page.svelte +++ b/web/src/routes/admin/maintenance/+page.svelte @@ -98,10 +98,12 @@ }); const onJobCreate = ({ dto }: { dto: JobCreateDto }) => { - if ((Object.values(jobNames).includes(dto.name) || Object.values(refreshJobNames).includes(dto.name)) && jobs) { - activeJobs.add(dto.name); - jobs.integrityCheck.queueStatus.isActive = true; + if (!((Object.values(jobNames).includes(dto.name) || Object.values(refreshJobNames).includes(dto.name)) && jobs)) { + return; } + + activeJobs.add(dto.name); + jobs.integrityCheck.queueStatus.isActive = true; }; @@ -165,12 +167,7 @@ disabled={activeJobs.has(refreshJobNames[reportType])}>{$t('refresh')} - + {/snippet} diff --git a/web/src/routes/admin/system-settings/AuthSettings.svelte b/web/src/routes/admin/system-settings/AuthSettings.svelte index 651f7b3803..22118d97a8 100644 --- a/web/src/routes/admin/system-settings/AuthSettings.svelte +++ b/web/src/routes/admin/system-settings/AuthSettings.svelte @@ -24,7 +24,7 @@ // click runs before bind const previouslyEnabled = configToEdit.oauth.mobileOverrideEnabled; if (!previouslyEnabled && !configToEdit.oauth.mobileRedirectUri) { - configToEdit.oauth.mobileRedirectUri = globalThis.location.origin + '/api/oauth/mobile-redirect'; + configToEdit.oauth.mobileRedirectUri = location.origin + '/api/oauth/mobile-redirect'; } }; @@ -102,7 +102,7 @@ bind:value={configToEdit.oauth.issuerUrl} required={true} disabled={disabled || !configToEdit.oauth.enabled} - isEdited={!(configToEdit.oauth.issuerUrl === config.oauth.issuerUrl)} + isEdited={configToEdit.oauth.issuerUrl !== config.oauth.issuerUrl} /> {#if configToEdit.oauth.clientSecret} @@ -128,7 +128,7 @@ label="token_endpoint_auth_method" bind:value={configToEdit.oauth.tokenEndpointAuthMethod} disabled={disabled || !configToEdit.oauth.enabled || !configToEdit.oauth.clientSecret} - isEdited={!(configToEdit.oauth.tokenEndpointAuthMethod === config.oauth.tokenEndpointAuthMethod)} + isEdited={configToEdit.oauth.tokenEndpointAuthMethod !== config.oauth.tokenEndpointAuthMethod} options={[ { value: OAuthTokenEndpointAuthMethod.ClientSecretPost, text: 'client_secret_post' }, { value: OAuthTokenEndpointAuthMethod.ClientSecretBasic, text: 'client_secret_basic' }, @@ -143,7 +143,7 @@ bind:value={configToEdit.oauth.scope} required={true} disabled={disabled || !configToEdit.oauth.enabled} - isEdited={!(configToEdit.oauth.scope === config.oauth.scope)} + isEdited={configToEdit.oauth.scope !== config.oauth.scope} /> {/if} {/if} diff --git a/web/src/routes/admin/system-settings/FFmpegSettings.svelte b/web/src/routes/admin/system-settings/FFmpegSettings.svelte index 2e173eaa7e..f0436e0bf7 100644 --- a/web/src/routes/admin/system-settings/FFmpegSettings.svelte +++ b/web/src/routes/admin/system-settings/FFmpegSettings.svelte @@ -178,7 +178,7 @@ onSelect={() => configToEdit.ffmpeg.acceptedAudioCodecs.includes(configToEdit.ffmpeg.targetAudioCodec) ? null - : configToEdit.ffmpeg.acceptedAudioCodecs.push(configToEdit.ffmpeg.targetAudioCodec)} + : void configToEdit.ffmpeg.acceptedAudioCodecs.push(configToEdit.ffmpeg.targetAudioCodec)} /> {#snippet descriptionSnippet()}

@@ -79,10 +77,8 @@ bind:value={configToEdit.integrityChecks.untrackedFiles.cronExpression} required={true} {disabled} - isEdited={!( - configToEdit.integrityChecks.untrackedFiles.cronExpression === - config.integrityChecks.untrackedFiles.cronExpression - )} + isEdited={configToEdit.integrityChecks.untrackedFiles.cronExpression !== + config.integrityChecks.untrackedFiles.cronExpression} > {#snippet descriptionSnippet()}

@@ -120,10 +116,8 @@ bind:value={configToEdit.integrityChecks.checksumFiles.cronExpression} required={true} {disabled} - isEdited={!( - configToEdit.integrityChecks.checksumFiles.cronExpression === - config.integrityChecks.checksumFiles.cronExpression - )} + isEdited={configToEdit.integrityChecks.checksumFiles.cronExpression !== + config.integrityChecks.checksumFiles.cronExpression} > {#snippet descriptionSnippet()}

diff --git a/web/src/routes/admin/system-settings/JobSettings.svelte b/web/src/routes/admin/system-settings/JobSettings.svelte index a2d51e0a4a..a43be5e9f4 100644 --- a/web/src/routes/admin/system-settings/JobSettings.svelte +++ b/web/src/routes/admin/system-settings/JobSettings.svelte @@ -27,7 +27,7 @@ ]; function isSystemConfigJobDto(jobName: string): jobName is keyof SystemConfigJobDto { - return jobName in configToEdit.job; + return Object.hasOwn(configToEdit.job, jobName); } const queueTitles: Record = $derived({ @@ -66,7 +66,7 @@ description="" bind:value={configToEdit.job[queueName].concurrency} required={true} - isEdited={!(configToEdit.job[queueName].concurrency == config.job[queueName].concurrency)} + isEdited={configToEdit.job[queueName].concurrency != config.job[queueName].concurrency} /> {:else} configToEdit.machineLearning.urls.push('')} + onclick={() => void configToEdit.machineLearning.urls.push('')} disabled={disabled || !configToEdit.machineLearning.enabled}>{$t('add_url')}

diff --git a/web/src/routes/admin/system-settings/NightlyTasksSettings.svelte b/web/src/routes/admin/system-settings/NightlyTasksSettings.svelte index 1d5debe818..88290a0f49 100644 --- a/web/src/routes/admin/system-settings/NightlyTasksSettings.svelte +++ b/web/src/routes/admin/system-settings/NightlyTasksSettings.svelte @@ -24,7 +24,7 @@ bind:value={configToEdit.nightlyTasks.startTime} required={true} {disabled} - isEdited={!(configToEdit.nightlyTasks.startTime === config.nightlyTasks.startTime)} + isEdited={configToEdit.nightlyTasks.startTime !== config.nightlyTasks.startTime} /> { - if (update.id === user.id) { - data.user = update; - await invalidateAll(); + if (update.id !== user.id) { + return; } + + data.user = update; + await invalidateAll(); }; const onUserAdminDeleted = async ({ id }: { id: string }) => { diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 1a73dfb890..78cd5fdba3 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -42,9 +42,9 @@ return; } - if (oauth.isCallback(globalThis.location)) { + if (oauth.isCallback(location)) { try { - const user = await oauth.login(globalThis.location); + const user = await oauth.login(location); if (!user.isOnboarded) { await onOnboarding(); @@ -63,11 +63,11 @@ try { if ( - (featureFlagsManager.value.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(globalThis.location)) || - oauth.isAutoLaunchEnabled(globalThis.location) + (featureFlagsManager.value.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(location)) || + oauth.isAutoLaunchEnabled(location) ) { await goto(Route.login({ autoLaunch: 0 }), { replaceState: true }); - await oauth.authorize(globalThis.location); + await oauth.authorize(location); return; } } catch (error) { @@ -113,7 +113,7 @@ const handleOAuthLogin = async () => { oauthLoading = true; oauthError = ''; - const success = await oauth.authorize(globalThis.location); + const success = await oauth.authorize(location); if (!success) { oauthLoading = false; oauthError = $t('errors.unable_to_login_with_oauth'); diff --git a/web/src/routes/maintenance/+page.svelte b/web/src/routes/maintenance/+page.svelte index c5fbde28ec..566231692d 100644 --- a/web/src/routes/maintenance/+page.svelte +++ b/web/src/routes/maintenance/+page.svelte @@ -1,4 +1,5 @@