diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 87170e5d5..07e07f422 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -58,7 +58,7 @@ jobs: uses: docker/setup-qemu-action@v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@v3.1.0 - name: Login to GitHub Container Registry uses: docker/login-action@v3 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a6a683588..dd1c53468 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -66,7 +66,7 @@ jobs: uses: docker/setup-qemu-action@v3.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.0.0 + uses: docker/setup-buildx-action@v3.1.0 # Workaround to fix error: # failed to push: failed to copy: io: read/write on closed pipe # See https://github.com/docker/build-push-action/issues/761 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3ede4466..121cd1d94 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ concurrency: jobs: server-e2e-api: name: Server (e2e-api) - runs-on: mich + runs-on: ubuntu-latest defaults: run: working-directory: ./server @@ -29,7 +29,7 @@ jobs: server-e2e-jobs: name: Server (e2e-jobs) - runs-on: mich + runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/cli/package-lock.json b/cli/package-lock.json index 22d858570..f48f74d51 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1325,9 +1325,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1340,9 +1340,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/ssh2": { @@ -1373,16 +1373,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz", - "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", + "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/type-utils": "7.0.1", - "@typescript-eslint/utils": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/type-utils": "7.0.2", + "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1408,15 +1408,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz", - "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", + "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4" }, "engines": { @@ -1436,13 +1436,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz", - "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", + "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1" + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1453,13 +1453,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz", - "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", + "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/utils": "7.0.1", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/utils": "7.0.2", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1480,9 +1480,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz", - "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", + "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1493,13 +1493,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz", - "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", + "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1545,17 +1545,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz", - "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", + "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", "semver": "^7.5.4" }, "engines": { @@ -1570,12 +1570,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz", - "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", + "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.1", + "@typescript-eslint/types": "7.0.2", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1593,9 +1593,9 @@ "dev": true }, "node_modules/@vitest/coverage-v8": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.2.tgz", - "integrity": "sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", + "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -1616,17 +1616,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "^1.0.0" + "vitest": "1.3.1" } }, "node_modules/@vitest/expect": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz", - "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", "dev": true, "dependencies": { - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "chai": "^4.3.10" }, "funding": { @@ -1634,12 +1634,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz", - "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", "dev": true, "dependencies": { - "@vitest/utils": "1.2.2", + "@vitest/utils": "1.3.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -1675,9 +1675,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz", - "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -1689,9 +1689,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz", - "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -1701,9 +1701,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz", - "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -1715,15 +1715,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -2814,6 +2805,15 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2823,6 +2823,29 @@ "node": ">=0.10.0" } }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3019,6 +3042,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -3157,6 +3192,15 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -3317,6 +3361,18 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -3683,6 +3739,18 @@ "node": ">=8.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -3845,6 +3913,33 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3854,6 +3949,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -4848,6 +4958,18 @@ "node": ">=8" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -4873,17 +4995,23 @@ } }, "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", + "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", "dev": true, "dependencies": { - "acorn": "^8.10.0" + "js-tokens": "^8.0.2" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", + "dev": true + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5041,9 +5169,9 @@ } }, "node_modules/tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true, "engines": { "node": ">=14.0.0" @@ -5240,9 +5368,9 @@ } }, "node_modules/vite": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", - "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", + "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", "dev": true, "dependencies": { "esbuild": "^0.19.3", @@ -5295,9 +5423,9 @@ } }, "node_modules/vite-node": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz", - "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -5317,18 +5445,17 @@ } }, "node_modules/vitest": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", - "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", "dev": true, "dependencies": { - "@vitest/expect": "1.2.2", - "@vitest/runner": "1.2.2", - "@vitest/snapshot": "1.2.2", - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", + "@vitest/expect": "1.3.1", + "@vitest/runner": "1.3.1", + "@vitest/snapshot": "1.3.1", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "acorn-walk": "^8.3.2", - "cac": "^6.7.14", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", @@ -5337,11 +5464,11 @@ "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", - "strip-literal": "^1.3.0", + "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.2.2", + "vite-node": "1.3.1", "why-is-node-running": "^2.2.2" }, "bin": { @@ -5356,8 +5483,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "^1.0.0", - "@vitest/ui": "^1.0.0", + "@vitest/browser": "1.3.1", + "@vitest/ui": "1.3.1", "happy-dom": "*", "jsdom": "*" }, @@ -5382,128 +5509,6 @@ } } }, - "node_modules/vitest/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/vitest/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/vitest/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vitest/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -6481,9 +6486,9 @@ } }, "@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -6496,9 +6501,9 @@ "dev": true }, "@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "@types/ssh2": { @@ -6531,16 +6536,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz", - "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", + "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/type-utils": "7.0.1", - "@typescript-eslint/utils": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/type-utils": "7.0.2", + "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -6550,54 +6555,54 @@ } }, "@typescript-eslint/parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz", - "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", + "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz", - "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", + "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1" + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2" } }, "@typescript-eslint/type-utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz", - "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", + "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/utils": "7.0.1", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/utils": "7.0.2", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz", - "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", + "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz", - "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", + "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -6627,27 +6632,27 @@ } }, "@typescript-eslint/utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz", - "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", + "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz", - "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", + "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.1", + "@typescript-eslint/types": "7.0.2", "eslint-visitor-keys": "^3.4.1" } }, @@ -6658,9 +6663,9 @@ "dev": true }, "@vitest/coverage-v8": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.2.tgz", - "integrity": "sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", + "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", "dev": true, "requires": { "@ampproject/remapping": "^2.2.1", @@ -6679,23 +6684,23 @@ } }, "@vitest/expect": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz", - "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", "dev": true, "requires": { - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "chai": "^4.3.10" } }, "@vitest/runner": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz", - "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", "dev": true, "requires": { - "@vitest/utils": "1.2.2", + "@vitest/utils": "1.3.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -6718,9 +6723,9 @@ } }, "@vitest/snapshot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz", - "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", "dev": true, "requires": { "magic-string": "^0.30.5", @@ -6729,35 +6734,24 @@ } }, "@vitest/spy": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz", - "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", "dev": true, "requires": { "tinyspy": "^2.2.0" } }, "@vitest/utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz", - "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", "dev": true, "requires": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" - }, - "dependencies": { - "estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "requires": { - "@types/estree": "^1.0.0" - } - } } }, "acorn": { @@ -7551,12 +7545,38 @@ } } }, + "estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0" + } + }, "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7712,6 +7732,12 @@ "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "dev": true }, + "get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true + }, "glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", @@ -7816,6 +7842,12 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7923,6 +7955,12 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -8226,6 +8264,12 @@ "picomatch": "^2.3.1" } }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8343,6 +8387,23 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + }, + "dependencies": { + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + } + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8352,6 +8413,15 @@ "wrappy": "1" } }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, "optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -9069,6 +9139,12 @@ "ansi-regex": "^5.0.1" } }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, "strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -9085,12 +9161,20 @@ "dev": true }, "strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", + "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", "dev": true, "requires": { - "acorn": "^8.10.0" + "js-tokens": "^8.0.2" + }, + "dependencies": { + "js-tokens": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", + "dev": true + } } }, "supports-color": { @@ -9224,9 +9308,9 @@ "dev": true }, "tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true }, "tmp": { @@ -9364,9 +9448,9 @@ } }, "vite": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", - "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", + "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", "dev": true, "requires": { "esbuild": "^0.19.3", @@ -9376,9 +9460,9 @@ } }, "vite-node": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz", - "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", "dev": true, "requires": { "cac": "^6.7.14", @@ -9389,18 +9473,17 @@ } }, "vitest": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", - "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", "dev": true, "requires": { - "@vitest/expect": "1.2.2", - "@vitest/runner": "1.2.2", - "@vitest/snapshot": "1.2.2", - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", + "@vitest/expect": "1.3.1", + "@vitest/runner": "1.3.1", + "@vitest/snapshot": "1.3.1", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "acorn-walk": "^8.3.2", - "cac": "^6.7.14", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", @@ -9409,85 +9492,12 @@ "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", - "strip-literal": "^1.3.0", + "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.2.2", + "vite-node": "1.3.1", "why-is-node-running": "^2.2.2" - }, - "dependencies": { - "execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - } - }, - "get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true - }, - "human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true - }, - "is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true - }, - "mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true - }, - "npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", - "dev": true, - "requires": { - "path-key": "^4.0.0" - } - }, - "onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "requires": { - "mimic-fn": "^4.0.0" - } - }, - "path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true - }, - "strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true - } } }, "webidl-conversions": { diff --git a/docker/hwaccel.transcoding.yml b/docker/hwaccel.transcoding.yml index f5585d815..2f6ae3ebd 100644 --- a/docker/hwaccel.transcoding.yml +++ b/docker/hwaccel.transcoding.yml @@ -38,12 +38,6 @@ services: - /dev/dri:/dev/dri - /dev/dma_heap:/dev/dma_heap - /dev/mpp_service:/dev/mpp_service - volumes: - - /usr/bin/ffmpeg:/usr/bin/ffmpeg_mpp:ro - - /lib/aarch64-linux-gnu:/lib/ffmpeg-mpp:ro - - /lib/aarch64-linux-gnu/libblas.so.3:/lib/ffmpeg-mpp/libblas.so.3:ro # symlink is resolved by mounting - - /lib/aarch64-linux-gnu/liblapack.so.3:/lib/ffmpeg-mpp/liblapack.so.3:ro # symlink is resolved by mounting - - /lib/aarch64-linux-gnu/pulseaudio/libpulsecommon-15.99.so:/lib/ffmpeg-mpp/libpulsecommon-15.99.so:ro vaapi: devices: diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index fb02aff2f..8228ff289 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -4,7 +4,6 @@ name: immich-e2e x-server-build: &server-common image: immich-server:latest - container_name: immich-e2e-server build: context: ../ dockerfile: server/Dockerfile @@ -23,14 +22,16 @@ x-server-build: &server-common services: immich-server: + container_name: immich-e2e-server command: [ "./start.sh", "immich" ] <<: *server-common ports: - 2283:3001 - # immich-microservices: - # command: [ "./start.sh", "microservices" ] - # <<: *server-common + immich-microservices: + container_name: immich-e2e-microservices + command: [ "./start.sh", "microservices" ] + <<: *server-common redis: image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 44155ac83..954d1cc3f 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -12,11 +12,14 @@ "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.41.2", + "@types/luxon": "^3.4.2", "@types/node": "^20.11.17", "@types/pg": "^8.11.0", "@types/supertest": "^6.0.2", "@vitest/coverage-v8": "^1.3.0", + "luxon": "^3.4.4", "pg": "^8.11.3", + "socket.io-client": "^4.7.4", "supertest": "^6.3.4", "typescript": "^5.3.3", "vitest": "^1.3.0" @@ -781,6 +784,12 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -799,6 +808,12 @@ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "dev": true + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -1263,6 +1278,28 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -1704,6 +1741,15 @@ "node": ">=10" } }, + "node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.30.7", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", @@ -2346,6 +2392,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io-client": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.4.tgz", + "integrity": "sha512-wh+OkeF0rAVCrABWQBaEjLfb7DVPotMbu0cgWgyR0v6eA4EoVnAwcIeIbcdTE3GT/H3kbdLl7OoH2+asoDRIIg==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2743,6 +2817,36 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/e2e/package.json b/e2e/package.json index ebd5b9aea..7bbdfd1d9 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -16,11 +16,14 @@ "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.41.2", + "@types/luxon": "^3.4.2", "@types/node": "^20.11.17", "@types/pg": "^8.11.0", "@types/supertest": "^6.0.2", "@vitest/coverage-v8": "^1.3.0", + "luxon": "^3.4.4", "pg": "^8.11.3", + "socket.io-client": "^4.7.4", "supertest": "^6.3.4", "typescript": "^5.3.3", "vitest": "^1.3.0" diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index 738411338..39c075dba 100644 --- a/e2e/src/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -1,7 +1,7 @@ import { ActivityCreateDto, AlbumResponseDto, - AssetResponseDto, + AssetFileUploadResponseDto, LoginResponseDto, ReactionType, createActivity as create, @@ -16,13 +16,13 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; describe('/activity', () => { let admin: LoginResponseDto; let nonOwner: LoginResponseDto; - let asset: AssetResponseDto; + let asset: AssetFileUploadResponseDto; let album: AlbumResponseDto; const createActivity = (dto: ActivityCreateDto, accessToken?: string) => create( { activityCreateDto: dto }, - { headers: asBearerAuth(accessToken || admin.accessToken) } + { headers: asBearerAuth(accessToken || admin.accessToken) }, ); beforeAll(async () => { @@ -40,7 +40,7 @@ describe('/activity', () => { sharedWithUserIds: [nonOwner.userId], }, }, - { headers: asBearerAuth(admin.accessToken) } + { headers: asBearerAuth(admin.accessToken) }, ); }); @@ -61,7 +61,7 @@ describe('/activity', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(400); expect(body).toEqual( - errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])) + errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])), ); }); @@ -72,7 +72,7 @@ describe('/activity', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(400); expect(body).toEqual( - errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])) + errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])), ); }); @@ -83,7 +83,7 @@ describe('/activity', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(400); expect(body).toEqual( - errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])) + errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])), ); }); @@ -104,7 +104,7 @@ describe('/activity', () => { assetIds: [asset.id], }, }, - { headers: asBearerAuth(admin.accessToken) } + { headers: asBearerAuth(admin.accessToken) }, ); const [reaction] = await Promise.all([ @@ -216,7 +216,7 @@ describe('/activity', () => { .send({ albumId: uuidDto.invalid }); expect(status).toEqual(400); expect(body).toEqual( - errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])) + errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])), ); }); @@ -230,7 +230,7 @@ describe('/activity', () => { errorDto.badRequest([ 'comment must be a string', 'comment should not be empty', - ]) + ]), ); }); @@ -357,7 +357,7 @@ describe('/activity', () => { describe('DELETE /activity/:id', () => { it('should require authentication', async () => { const { status, body } = await request(app).delete( - `/activity/${uuidDto.notFound}` + `/activity/${uuidDto.notFound}`, ); expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); @@ -421,7 +421,7 @@ describe('/activity', () => { expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest('Not found or no activity.delete access') + errorDto.badRequest('Not found or no activity.delete access'), ); }); @@ -432,7 +432,7 @@ describe('/activity', () => { type: ReactionType.Comment, comment: 'This is a test comment', }, - nonOwner.accessToken + nonOwner.accessToken, ); const { status } = await request(app) diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index c131edc49..3385e50f4 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -1,6 +1,6 @@ import { AlbumResponseDto, - AssetResponseDto, + AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, deleteUser, @@ -21,8 +21,8 @@ const user2NotShared = 'user2NotShared'; describe('/album', () => { let admin: LoginResponseDto; let user1: LoginResponseDto; - let user1Asset1: AssetResponseDto; - let user1Asset2: AssetResponseDto; + let user1Asset1: AssetFileUploadResponseDto; + let user1Asset2: AssetFileUploadResponseDto; let user1Albums: AlbumResponseDto[]; let user2: LoginResponseDto; let user2Albums: AlbumResponseDto[]; @@ -95,7 +95,7 @@ describe('/album', () => { await deleteUser( { id: user3.userId }, - { headers: asBearerAuth(admin.accessToken) } + { headers: asBearerAuth(admin.accessToken) }, ); }); @@ -112,7 +112,7 @@ describe('/album', () => { .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toEqual(400); expect(body).toEqual( - errorDto.badRequest(['shared must be a boolean value']) + errorDto.badRequest(['shared must be a boolean value']), ); }); @@ -148,7 +148,7 @@ describe('/album', () => { albumName: user2SharedUser, shared: true, }), - ]) + ]), ); }); @@ -175,7 +175,7 @@ describe('/album', () => { albumName: user1NotShared, shared: false, }), - ]) + ]), ); }); @@ -202,7 +202,7 @@ describe('/album', () => { albumName: user2SharedUser, shared: true, }), - ]) + ]), ); }); @@ -219,7 +219,7 @@ describe('/album', () => { albumName: user1NotShared, shared: false, }), - ]) + ]), ); }); @@ -251,7 +251,7 @@ describe('/album', () => { describe('GET /album/:id', () => { it('should require authentication', async () => { const { status, body } = await request(app).get( - `/album/${user1Albums[0].id}` + `/album/${user1Albums[0].id}`, ); expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); @@ -361,7 +361,7 @@ describe('/album', () => { describe('PUT /album/:id/assets', () => { it('should require authentication', async () => { const { status, body } = await request(app).put( - `/album/${user1Albums[0].id}/assets` + `/album/${user1Albums[0].id}/assets`, ); expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); @@ -519,7 +519,7 @@ describe('/album', () => { expect(body).toEqual( expect.objectContaining({ sharedUsers: [expect.objectContaining({ id: user2.userId })], - }) + }), ); }); diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts new file mode 100644 index 000000000..db1821260 --- /dev/null +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -0,0 +1,481 @@ +import { + AssetFileUploadResponseDto, + AssetResponseDto, + LoginResponseDto, + SharedLinkType, +} from '@immich/sdk'; +import { DateTime } from 'luxon'; +import { Socket } from 'socket.io-client'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { apiUtils, app, dbUtils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, describe, expect, it } from 'vitest'; + +const today = DateTime.fromObject({ + year: 2023, + month: 11, + day: 3, +}) as DateTime; +const yesterday = today.minus({ days: 1 }); + +describe('/asset', () => { + let admin: LoginResponseDto; + let user1: LoginResponseDto; + let user2: LoginResponseDto; + let userStats: LoginResponseDto; + let asset1: AssetFileUploadResponseDto; + let asset2: AssetFileUploadResponseDto; + let asset3: AssetFileUploadResponseDto; + let asset4: AssetFileUploadResponseDto; // user2 asset + let asset5: AssetFileUploadResponseDto; + let asset6: AssetFileUploadResponseDto; + let ws: Socket; + + beforeAll(async () => { + apiUtils.setup(); + await dbUtils.reset(); + admin = await apiUtils.adminSetup({ onboarding: false }); + [user1, user2, userStats] = await Promise.all([ + apiUtils.userSetup(admin.accessToken, createUserDto.user1), + apiUtils.userSetup(admin.accessToken, createUserDto.user2), + apiUtils.userSetup(admin.accessToken, createUserDto.user3), + ]); + + [asset1, asset2, asset3, asset4, asset5, asset6] = await Promise.all([ + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset( + user1.accessToken, + { + isFavorite: true, + isExternal: true, + isReadOnly: true, + fileCreatedAt: yesterday.toISO(), + fileModifiedAt: yesterday.toISO(), + }, + { filename: 'example.mp4' }, + ), + apiUtils.createAsset(user2.accessToken), + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), + + // stats + apiUtils.createAsset(userStats.accessToken), + apiUtils.createAsset(userStats.accessToken, { isFavorite: true }), + apiUtils.createAsset(userStats.accessToken, { isArchived: true }), + apiUtils.createAsset( + userStats.accessToken, + { + isArchived: true, + isFavorite: true, + }, + { filename: 'example.mp4' }, + ), + ]); + + const person1 = await apiUtils.createPerson(user1.accessToken, { + name: 'Test Person', + }); + await dbUtils.createFace({ assetId: asset1.id, personId: person1.id }); + }); + + describe('GET /asset/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get( + `/asset/${uuidDto.notFound}`, + ); + expect(body).toEqual(errorDto.unauthorized); + expect(status).toBe(401); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .get(`/asset/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require access', async () => { + const { status, body } = await request(app) + .get(`/asset/${asset4.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should get the asset info', async () => { + const { status, body } = await request(app) + .get(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(200); + expect(body).toMatchObject({ id: asset1.id }); + }); + + it('should work with a shared link', async () => { + const sharedLink = await apiUtils.createSharedLink(user1.accessToken, { + type: SharedLinkType.Individual, + assetIds: [asset1.id], + }); + + const { status, body } = await request(app).get( + `/asset/${asset1.id}?key=${sharedLink.key}`, + ); + expect(status).toBe(200); + expect(body).toMatchObject({ id: asset1.id }); + }); + + it('should not send people data for shared links for un-authenticated users', async () => { + const { status, body } = await request(app) + .get(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toEqual(200); + expect(body).toMatchObject({ + id: asset1.id, + isFavorite: false, + people: [ + { + birthDate: null, + id: expect.any(String), + isHidden: false, + name: 'Test Person', + thumbnailPath: '/my/awesome/thumbnail.jpg', + }, + ], + }); + + const sharedLink = await apiUtils.createSharedLink(user1.accessToken, { + type: SharedLinkType.Individual, + assetIds: [asset1.id], + }); + + const data = await request(app).get( + `/asset/${asset1.id}?key=${sharedLink.key}`, + ); + expect(data.status).toBe(200); + expect(data.body).toMatchObject({ people: [] }); + }); + }); + + describe('GET /asset/statistics', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/asset/statistics'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should return stats of all assets', async () => { + const { status, body } = await request(app) + .get('/asset/statistics') + .set('Authorization', `Bearer ${userStats.accessToken}`); + + expect(body).toEqual({ images: 3, videos: 1, total: 4 }); + expect(status).toBe(200); + }); + + it('should return stats of all favored assets', async () => { + const { status, body } = await request(app) + .get('/asset/statistics') + .set('Authorization', `Bearer ${userStats.accessToken}`) + .query({ isFavorite: true }); + + expect(status).toBe(200); + expect(body).toEqual({ images: 1, videos: 1, total: 2 }); + }); + + it('should return stats of all archived assets', async () => { + const { status, body } = await request(app) + .get('/asset/statistics') + .set('Authorization', `Bearer ${userStats.accessToken}`) + .query({ isArchived: true }); + + expect(status).toBe(200); + expect(body).toEqual({ images: 1, videos: 1, total: 2 }); + }); + + it('should return stats of all favored and archived assets', async () => { + const { status, body } = await request(app) + .get('/asset/statistics') + .set('Authorization', `Bearer ${userStats.accessToken}`) + .query({ isFavorite: true, isArchived: true }); + + expect(status).toBe(200); + expect(body).toEqual({ images: 0, videos: 1, total: 1 }); + }); + + it('should return stats of all assets neither favored nor archived', async () => { + const { status, body } = await request(app) + .get('/asset/statistics') + .set('Authorization', `Bearer ${userStats.accessToken}`) + .query({ isFavorite: false, isArchived: false }); + + expect(status).toBe(200); + expect(body).toEqual({ images: 1, videos: 0, total: 1 }); + }); + }); + + describe('GET /asset/random', () => { + beforeAll(async () => { + await Promise.all([ + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), + ]); + }); + + it('should require authentication', async () => { + const { status, body } = await request(app).get('/asset/random'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it.each(Array(10))('should return 1 random assets', async () => { + const { status, body } = await request(app) + .get('/asset/random') + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + + const assets: AssetResponseDto[] = body; + expect(assets.length).toBe(1); + expect(assets[0].ownerId).toBe(user1.userId); + // + // assets owned by user2 + expect(assets[0].id).not.toBe(asset4.id); + // assets owned by user1 + expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id); + }); + + it.each(Array(10))('should return 2 random assets', async () => { + const { status, body } = await request(app) + .get('/asset/random?count=2') + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + + const assets: AssetResponseDto[] = body; + expect(assets.length).toBe(2); + + for (const asset of assets) { + expect(asset.ownerId).toBe(user1.userId); + // assets owned by user1 + expect([asset1.id, asset2.id, asset3.id]).toContain(asset.id); + // assets owned by user2 + expect(asset.id).not.toBe(asset4.id); + } + }); + + it.each(Array(10))( + 'should return 1 asset if there are 10 assets in the database but user 2 only has 1', + async () => { + const { status, body } = await request(app) + .get('/[]asset/random') + .set('Authorization', `Bearer ${user2.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual([expect.objectContaining({ id: asset4.id })]); + }, + ); + + it('should return error', async () => { + const { status } = await request(app) + .get('/asset/random?count=ABC') + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(400); + }); + }); + + describe('PUT /asset/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put( + `/asset/:${uuidDto.notFound}`, + ); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .put(`/asset/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should require access', async () => { + const { status, body } = await request(app) + .put(`/asset/${asset4.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should favorite an asset', async () => { + const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id); + expect(before.isFavorite).toBe(false); + + const { status, body } = await request(app) + .put(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ isFavorite: true }); + expect(body).toMatchObject({ id: asset1.id, isFavorite: true }); + expect(status).toEqual(200); + }); + + it('should archive an asset', async () => { + const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id); + expect(before.isArchived).toBe(false); + + const { status, body } = await request(app) + .put(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ isArchived: true }); + expect(body).toMatchObject({ id: asset1.id, isArchived: true }); + expect(status).toEqual(200); + }); + + it('should update date time original', async () => { + const { status, body } = await request(app) + .put(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); + + expect(body).toMatchObject({ + id: asset1.id, + exifInfo: expect.objectContaining({ + dateTimeOriginal: '2023-11-20T01:11:00.000Z', + }), + }); + expect(status).toEqual(200); + }); + + it('should reject invalid gps coordinates', async () => { + for (const test of [ + { latitude: 12 }, + { longitude: 12 }, + { latitude: 12, longitude: 'abc' }, + { latitude: 'abc', longitude: 12 }, + { latitude: null, longitude: 12 }, + { latitude: 12, longitude: null }, + { latitude: 91, longitude: 12 }, + { latitude: -91, longitude: 12 }, + { latitude: 12, longitude: -181 }, + { latitude: 12, longitude: 181 }, + ]) { + const { status, body } = await request(app) + .put(`/asset/${asset1.id}`) + .send(test) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest()); + } + }); + + it('should update gps data', async () => { + const { status, body } = await request(app) + .put(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ latitude: 12, longitude: 12 }); + + expect(body).toMatchObject({ + id: asset1.id, + exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }), + }); + expect(status).toEqual(200); + }); + + it('should set the description', async () => { + const { status, body } = await request(app) + .put(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ description: 'Test asset description' }); + expect(body).toMatchObject({ + id: asset1.id, + exifInfo: expect.objectContaining({ + description: 'Test asset description', + }), + }); + expect(status).toEqual(200); + }); + + it('should return tagged people', async () => { + const { status, body } = await request(app) + .put(`/asset/${asset1.id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ isFavorite: true }); + expect(status).toEqual(200); + expect(body).toMatchObject({ + id: asset1.id, + isFavorite: true, + people: [ + { + birthDate: null, + id: expect.any(String), + isHidden: false, + name: 'Test Person', + thumbnailPath: '/my/awesome/thumbnail.jpg', + }, + ], + }); + }); + }); + + describe('DELETE /asset', () => { + it('should require authentication', async () => { + const { status, body } = await request(app) + .delete(`/asset`) + .send({ ids: [uuidDto.notFound] }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .delete(`/asset`) + .send({ ids: [uuidDto.invalid] }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest(['each value in ids must be a UUID']), + ); + }); + + it('should throw an error when the id is not found', async () => { + const { status, body } = await request(app) + .delete(`/asset`) + .send({ ids: [uuidDto.notFound] }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest('Not found or no asset.delete access'), + ); + }); + + it('should move an asset to the trash', async () => { + const { id: assetId } = await apiUtils.createAsset(admin.accessToken); + + const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); + expect(before.isTrashed).toBe(false); + + const { status } = await request(app) + .delete('/asset') + .send({ ids: [assetId] }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(204); + + const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); + expect(after.isTrashed).toBe(true); + }); + }); +}); diff --git a/e2e/src/api/specs/download.e2e-spec.ts b/e2e/src/api/specs/download.e2e-spec.ts index f1d7bd112..22d66baf0 100644 --- a/e2e/src/api/specs/download.e2e-spec.ts +++ b/e2e/src/api/specs/download.e2e-spec.ts @@ -1,4 +1,4 @@ -import { AssetResponseDto, LoginResponseDto } from '@immich/sdk'; +import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk'; import { errorDto } from 'src/responses'; import { apiUtils, app, dbUtils } from 'src/utils'; import request from 'supertest'; @@ -6,7 +6,7 @@ import { beforeAll, describe, expect, it } from 'vitest'; describe('/download', () => { let admin: LoginResponseDto; - let asset1: AssetResponseDto; + let asset1: AssetFileUploadResponseDto; beforeAll(async () => { apiUtils.setup(); @@ -35,7 +35,7 @@ describe('/download', () => { expect(body).toEqual( expect.objectContaining({ archives: [expect.objectContaining({ assetIds: [asset1.id] })], - }) + }), ); }); }); @@ -43,7 +43,7 @@ describe('/download', () => { describe('POST /download/asset/:id', () => { it('should require authentication', async () => { const { status, body } = await request(app).post( - `/download/asset/${asset1.id}` + `/download/asset/${asset1.id}`, ); expect(status).toBe(401); diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index e791c447a..0bb760fbc 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -1,11 +1,9 @@ import { AlbumResponseDto, - AssetResponseDto, + AssetFileUploadResponseDto, LoginResponseDto, - SharedLinkCreateDto, SharedLinkResponseDto, SharedLinkType, - createSharedLink as create, createAlbum, deleteUser, } from '@immich/sdk'; @@ -17,8 +15,8 @@ import { beforeAll, describe, expect, it } from 'vitest'; describe('/shared-link', () => { let admin: LoginResponseDto; - let asset1: AssetResponseDto; - let asset2: AssetResponseDto; + let asset1: AssetFileUploadResponseDto; + let asset2: AssetFileUploadResponseDto; let user1: LoginResponseDto; let user2: LoginResponseDto; let album: AlbumResponseDto; @@ -50,11 +48,11 @@ describe('/shared-link', () => { [album, deletedAlbum, metadataAlbum] = await Promise.all([ createAlbum( { createAlbumDto: { albumName: 'album' } }, - { headers: asBearerAuth(user1.accessToken) } + { headers: asBearerAuth(user1.accessToken) }, ), createAlbum( { createAlbumDto: { albumName: 'deleted album' } }, - { headers: asBearerAuth(user2.accessToken) } + { headers: asBearerAuth(user2.accessToken) }, ), createAlbum( { @@ -63,7 +61,7 @@ describe('/shared-link', () => { assetIds: [asset1.id], }, }, - { headers: asBearerAuth(user1.accessToken) } + { headers: asBearerAuth(user1.accessToken) }, ), ]); @@ -106,7 +104,7 @@ describe('/shared-link', () => { await deleteUser( { id: user2.userId }, - { headers: asBearerAuth(admin.accessToken) } + { headers: asBearerAuth(admin.accessToken) }, ); }); @@ -132,7 +130,7 @@ describe('/shared-link', () => { expect.objectContaining({ id: linkWithPassword.id }), expect.objectContaining({ id: linkWithMetadata.id }), expect.objectContaining({ id: linkWithoutMetadata.id }), - ]) + ]), ); }); @@ -166,7 +164,7 @@ describe('/shared-link', () => { album, userId: user1.userId, type: SharedLinkType.Album, - }) + }), ); }); @@ -208,7 +206,7 @@ describe('/shared-link', () => { album, userId: user1.userId, type: SharedLinkType.Album, - }) + }), ); }); @@ -225,7 +223,7 @@ describe('/shared-link', () => { localDateTime: expect.any(String), fileCreatedAt: expect.any(String), exifInfo: expect.any(Object), - }) + }), ); expect(body.album).toBeDefined(); }); @@ -250,7 +248,7 @@ describe('/shared-link', () => { describe('GET /shared-link/:id', () => { it('should require authentication', async () => { const { status, body } = await request(app).get( - `/shared-link/${linkWithAlbum.id}` + `/shared-link/${linkWithAlbum.id}`, ); expect(status).toBe(401); @@ -268,7 +266,7 @@ describe('/shared-link', () => { album, userId: user1.userId, type: SharedLinkType.Album, - }) + }), ); }); @@ -279,7 +277,7 @@ describe('/shared-link', () => { expect(status).toBe(400); expect(body).toEqual( - expect.objectContaining({ message: 'Shared link not found' }) + expect.objectContaining({ message: 'Shared link not found' }), ); }); }); @@ -311,7 +309,7 @@ describe('/shared-link', () => { expect(status).toBe(400); expect(body).toEqual( - expect.objectContaining({ message: 'Invalid albumId' }) + expect.objectContaining({ message: 'Invalid albumId' }), ); }); @@ -323,7 +321,7 @@ describe('/shared-link', () => { expect(status).toBe(400); expect(body).toEqual( - expect.objectContaining({ message: 'Invalid assetIds' }) + expect.objectContaining({ message: 'Invalid assetIds' }), ); }); @@ -338,7 +336,7 @@ describe('/shared-link', () => { expect.objectContaining({ type: SharedLinkType.Album, userId: user1.userId, - }) + }), ); }); }); @@ -375,7 +373,7 @@ describe('/shared-link', () => { type: SharedLinkType.Album, userId: user1.userId, description: 'foo', - }) + }), ); }); }); @@ -427,7 +425,7 @@ describe('/shared-link', () => { describe('DELETE /shared-link/:id', () => { it('should require authentication', async () => { const { status, body } = await request(app).delete( - `/shared-link/${linkWithAlbum.id}` + `/shared-link/${linkWithAlbum.id}`, ); expect(status).toBe(401); diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts new file mode 100644 index 000000000..2de838f98 --- /dev/null +++ b/e2e/src/api/specs/trash.e2e-spec.ts @@ -0,0 +1,107 @@ +import { LoginResponseDto, getAllAssets } from '@immich/sdk'; +import { Socket } from 'socket.io-client'; +import { errorDto } from 'src/responses'; +import { apiUtils, app, asBearerAuth, dbUtils, wsUtils } from 'src/utils'; +import request from 'supertest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +describe('/trash', () => { + let admin: LoginResponseDto; + let ws: Socket; + + beforeAll(async () => { + apiUtils.setup(); + await dbUtils.reset(); + admin = await apiUtils.adminSetup({ onboarding: false }); + ws = await wsUtils.connect(admin.accessToken); + }); + + afterAll(() => { + wsUtils.disconnect(ws); + }); + + describe('POST /trash/empty', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/trash/empty'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should empty the trash', async () => { + const { id: assetId } = await apiUtils.createAsset(admin.accessToken); + await apiUtils.deleteAssets(admin.accessToken, [assetId]); + + const before = await getAllAssets( + {}, + { headers: asBearerAuth(admin.accessToken) }, + ); + + expect(before.length).toBeGreaterThanOrEqual(1); + + const { status } = await request(app) + .post('/trash/empty') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(204); + + await wsUtils.once(ws, 'on_asset_delete'); + + const after = await getAllAssets( + {}, + { headers: asBearerAuth(admin.accessToken) }, + ); + expect(after.length).toBe(0); + }); + }); + + describe('POST /trash/restore', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/trash/restore'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should restore all trashed assets', async () => { + const { id: assetId } = await apiUtils.createAsset(admin.accessToken); + await apiUtils.deleteAssets(admin.accessToken, [assetId]); + + const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); + expect(before.isTrashed).toBe(true); + + const { status } = await request(app) + .post('/trash/restore') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(204); + + const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); + expect(after.isTrashed).toBe(false); + }); + }); + + describe('POST /trash/restore/assets', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/trash/restore/assets'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should restore a trashed asset by id', async () => { + const { id: assetId } = await apiUtils.createAsset(admin.accessToken); + await apiUtils.deleteAssets(admin.accessToken, [assetId]); + + const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); + expect(before.isTrashed).toBe(true); + + const { status } = await request(app) + .post('/trash/restore/assets') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ ids: [assetId] }); + expect(status).toBe(204); + + const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); + expect(after.isTrashed).toBe(false); + }); + }); +}); diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/cli/specs/server-info.e2e-spec.ts index e9e89befd..038a2c2ca 100644 --- a/e2e/src/cli/specs/server-info.e2e-spec.ts +++ b/e2e/src/cli/specs/server-info.e2e-spec.ts @@ -1,12 +1,9 @@ import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils'; -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { beforeAll, describe, expect, it } from 'vitest'; describe(`immich server-info`, () => { - beforeAll(() => { + beforeAll(async () => { apiUtils.setup(); - }); - - beforeEach(async () => { await dbUtils.reset(); await cliUtils.login(); }); diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts index 6dd664e1e..908118d77 100644 --- a/e2e/src/cli/specs/upload.e2e-spec.ts +++ b/e2e/src/cli/specs/upload.e2e-spec.ts @@ -1,4 +1,5 @@ import { getAllAlbums, getAllAssets } from '@immich/sdk'; +import { mkdir, readdir, rm, symlink } from 'fs/promises'; import { apiUtils, asKeyAuth, @@ -8,18 +9,18 @@ import { testAssetDir, } from 'src/utils'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { mkdir, readdir, rm, symlink } from 'fs/promises'; describe(`immich upload`, () => { let key: string; - beforeAll(() => { + beforeAll(async () => { apiUtils.setup(); + await dbUtils.reset(); + key = await cliUtils.login(); }); beforeEach(async () => { - await dbUtils.reset(); - key = await cliUtils.login(); + await dbUtils.reset(['assets', 'albums']); }); describe('immich upload --recursive', () => { @@ -33,7 +34,7 @@ describe(`immich upload`, () => { expect(stdout.split('\n')).toEqual( expect.arrayContaining([ expect.stringContaining('Successfully uploaded 9 assets'), - ]) + ]), ); expect(exitCode).toBe(0); @@ -55,7 +56,7 @@ describe(`immich upload`, () => { expect.stringContaining('Successfully uploaded 9 assets'), expect.stringContaining('Successfully created 1 new album'), expect.stringContaining('Successfully updated 9 assets'), - ]) + ]), ); expect(stderr).toBe(''); expect(exitCode).toBe(0); @@ -77,7 +78,7 @@ describe(`immich upload`, () => { expect(response1.stdout.split('\n')).toEqual( expect.arrayContaining([ expect.stringContaining('Successfully uploaded 9 assets'), - ]) + ]), ); expect(response1.stderr).toBe(''); expect(response1.exitCode).toBe(0); @@ -97,10 +98,10 @@ describe(`immich upload`, () => { expect(response2.stdout.split('\n')).toEqual( expect.arrayContaining([ expect.stringContaining( - 'All assets were already uploaded, nothing to do.' + 'All assets were already uploaded, nothing to do.', ), expect.stringContaining('Successfully updated 9 assets'), - ]) + ]), ); expect(response2.stderr).toBe(''); expect(response2.exitCode).toBe(0); @@ -127,7 +128,7 @@ describe(`immich upload`, () => { expect.stringContaining('Successfully uploaded 9 assets'), expect.stringContaining('Successfully created 1 new album'), expect.stringContaining('Successfully updated 9 assets'), - ]) + ]), ); expect(stderr).toBe(''); expect(exitCode).toBe(0); @@ -148,7 +149,7 @@ describe(`immich upload`, () => { for (const file of filesToLink) { await symlink( `${testAssetDir}/albums/nature/${file}`, - `/tmp/albums/nature/${file}` + `/tmp/albums/nature/${file}`, ); } @@ -166,7 +167,7 @@ describe(`immich upload`, () => { expect.arrayContaining([ expect.stringContaining('Successfully uploaded 9 assets'), expect.stringContaining('Deleting assets that have been uploaded'), - ]) + ]), ); expect(stderr).toBe(''); expect(exitCode).toBe(0); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index fbc0b43b3..428c88b45 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -1,5 +1,5 @@ import { - AssetResponseDto, + AssetFileUploadResponseDto, CreateAlbumDto, CreateAssetDto, CreateUserDto, @@ -11,6 +11,8 @@ import { createSharedLink, createUser, defaults, + deleteAssets, + getAssetInfo, login, setAdminOnboarding, signUpAdmin, @@ -23,6 +25,7 @@ import { access } from 'node:fs/promises'; import path from 'node:path'; import { promisify } from 'node:util'; import pg from 'pg'; +import { io, type Socket } from 'socket.io-client'; import { loginDto, signupDto } from 'src/fixtures'; import request from 'supertest'; @@ -39,15 +42,19 @@ const directoryExists = (directory: string) => export const testAssetDir = path.resolve(`./../server/test/assets/`); const serverContainerName = 'immich-e2e-server'; -const uploadMediaDir = '/usr/src/app/upload/upload'; +const mediaDir = '/usr/src/app/upload'; +const dirs = [ + `"${mediaDir}/thumbs"`, + `"${mediaDir}/upload"`, + `"${mediaDir}/library"`, +].join(' '); if (!(await directoryExists(`${testAssetDir}/albums`))) { throw new Error( - `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing` + `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`, ); } -const setBaseUrl = () => (defaults.baseUrl = app); export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}`, }); @@ -59,7 +66,7 @@ let client: pg.Client | null = null; export const fileUtils = { reset: async () => { await execPromise( - `docker exec -i "${serverContainerName}" rm -R "${uploadMediaDir}"` + `docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`, ); }, }; @@ -81,7 +88,7 @@ export const dbUtils = { await client.query( 'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', - [assetId, personId, embedding] + [assetId, personId, embedding], ); }, setPersonThumbnail: async (personId: string) => { @@ -91,14 +98,14 @@ export const dbUtils = { await client.query( `UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, - [personId] + [personId], ); }, reset: async (tables?: string[]) => { try { if (!client) { client = new pg.Client( - 'postgres://postgres:postgres@127.0.0.1:5433/immich' + 'postgres://postgres:postgres@127.0.0.1:5433/immich', ); await client.connect(); } @@ -170,10 +177,42 @@ export interface AdminSetupOptions { onboarding?: boolean; } +export const wsUtils = { + connect: async (accessToken: string) => { + const websocket = io('http://127.0.0.1:2283', { + path: '/api/socket.io', + transports: ['websocket'], + extraHeaders: { Authorization: `Bearer ${accessToken}` }, + autoConnect: false, + forceNew: true, + }); + + return new Promise((resolve) => { + websocket.on('connect', () => resolve(websocket)); + websocket.connect(); + }); + }, + disconnect: (ws: Socket) => { + if (ws?.connected) { + ws.disconnect(); + } + }, + once: (ws: Socket, event: string): Promise => { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('Timeout')), 4000); + ws.once(event, (data: T) => { + clearTimeout(timeout); + resolve(data); + }); + }); + }, +}; + export const apiUtils = { setup: () => { - setBaseUrl(); + defaults.baseUrl = app; }, + adminSetup: async (options?: AdminSetupOptions) => { options = options || { onboarding: true }; @@ -187,7 +226,7 @@ export const apiUtils = { userSetup: async (accessToken: string, dto: CreateUserDto) => { await createUser( { createUserDto: dto }, - { headers: asBearerAuth(accessToken) } + { headers: asBearerAuth(accessToken) }, ); return login({ loginCredentialDto: { email: dto.email, password: dto.password }, @@ -196,48 +235,74 @@ export const apiUtils = { createApiKey: (accessToken: string) => { return createApiKey( { apiKeyCreateDto: { name: 'e2e' } }, - { headers: asBearerAuth(accessToken) } + { headers: asBearerAuth(accessToken) }, ); }, createAlbum: (accessToken: string, dto: CreateAlbumDto) => createAlbum( { createAlbumDto: dto }, - { headers: asBearerAuth(accessToken) } + { headers: asBearerAuth(accessToken) }, ), createAsset: async ( accessToken: string, - dto?: Omit + dto?: Partial>, + data?: { + bytes?: Buffer; + filename?: string; + }, ) => { - dto = dto || { + const _dto = { deviceAssetId: 'test-1', deviceId: 'test', fileCreatedAt: new Date().toISOString(), fileModifiedAt: new Date().toISOString(), + ...(dto || {}), }; - const { body } = await request(app) + + const _assetData = { + bytes: randomBytes(32), + filename: 'example.jpg', + ...(data || {}), + }; + + const builder = request(app) .post(`/asset/upload`) - .field('deviceAssetId', dto.deviceAssetId) - .field('deviceId', dto.deviceId) - .field('fileCreatedAt', dto.fileCreatedAt) - .field('fileModifiedAt', dto.fileModifiedAt) - .attach('assetData', randomBytes(32), 'example.jpg') + .attach('assetData', _assetData.bytes, _assetData.filename) .set('Authorization', `Bearer ${accessToken}`); - return body as AssetResponseDto; + for (const [key, value] of Object.entries(_dto)) { + builder.field(key, String(value)); + } + + const { body } = await builder; + + return body as AssetFileUploadResponseDto; }, - createPerson: async (accessToken: string, dto: PersonUpdateDto) => { + getAssetInfo: (accessToken: string, id: string) => + getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), + deleteAssets: (accessToken: string, ids: string[]) => + deleteAssets( + { assetBulkDeleteDto: { ids } }, + { headers: asBearerAuth(accessToken) }, + ), + createPerson: async (accessToken: string, dto?: PersonUpdateDto) => { // TODO fix createPerson to accept a body - const { id } = await createPerson({ headers: asBearerAuth(accessToken) }); - await dbUtils.setPersonThumbnail(id); + let person = await createPerson({ headers: asBearerAuth(accessToken) }); + await dbUtils.setPersonThumbnail(person.id); + + if (!dto) { + return person; + } + return updatePerson( - { id, personUpdateDto: dto }, - { headers: asBearerAuth(accessToken) } + { id: person.id, personUpdateDto: dto }, + { headers: asBearerAuth(accessToken) }, ); }, createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) => createSharedLink( { sharedLinkCreateDto: dto }, - { headers: asBearerAuth(accessToken) } + { headers: asBearerAuth(accessToken) }, ), }; diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index faa7b3425..ce79ed545 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/web/specs/shared-link.e2e-spec.ts @@ -15,6 +15,7 @@ test.describe('Shared Links', () => { let asset: AssetResponseDto; let album: AlbumResponseDto; let sharedLink: SharedLinkResponseDto; + let sharedLinkPassword: SharedLinkResponseDto; test.beforeAll(async () => { apiUtils.setup(); @@ -29,17 +30,16 @@ test.describe('Shared Links', () => { }, }, { headers: asBearerAuth(admin.accessToken) } - // { headers: asBearerAuth(admin.accessToken)}, - ); - sharedLink = await createSharedLink( - { - sharedLinkCreateDto: { - type: SharedLinkType.Album, - albumId: album.id, - }, - }, - { headers: asBearerAuth(admin.accessToken) } ); + sharedLink = await apiUtils.createSharedLink(admin.accessToken, { + type: SharedLinkType.Album, + albumId: album.id, + }); + sharedLinkPassword = await apiUtils.createSharedLink(admin.accessToken, { + type: SharedLinkType.Album, + albumId: album.id, + password: 'test-password', + }); }); test.afterAll(async () => { @@ -55,4 +55,16 @@ test.describe('Shared Links', () => { await page.getByRole('button', { name: 'Download' }).click(); await page.getByText('DOWNLOADING').waitFor(); }); + + test('enter password for a shared link', async ({ page }) => { + await page.goto(`/share/${sharedLinkPassword.key}`); + await page.getByPlaceholder('Password').fill('test-password'); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.getByRole('heading', { name: 'Test Album' }).waitFor(); + }); + + test('show error for invalid shared link', async ({ page }) => { + await page.goto('/share/invalid'); + await page.getByRole('heading', { name: 'Invalid share key' }).waitFor(); + }); }); diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 782269c8c..d7cff9c10 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -2101,61 +2101,61 @@ numpy = [ [[package]] name = "orjson" -version = "3.9.14" +version = "3.9.15" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.9.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:793f6c9448ab6eb7d4974b4dde3f230345c08ca6c7995330fbceeb43a5c8aa5e"}, - {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bc7928d161840096adc956703494b5c0193ede887346f028216cac0af87500"}, - {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b36f54da759602d8e2f7dad958752d453dfe2c7122767bc7f765e17dc59959"}, - {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abcda41ecdc950399c05eff761c3de91485d9a70d8227cb599ad3a66afe93bcc"}, - {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df76ecd17b1b3627bddfd689faaf206380a1a38cc9f6c4075bd884eaedcf46c2"}, - {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d450a8e0656efb5d0fcb062157b918ab02dcca73278975b4ee9ea49e2fcf5bd5"}, - {file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95c03137b0cf66517c8baa65770507a756d3a89489d8ecf864ea92348e1beabe"}, - {file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20837e10835c98973673406d6798e10f821e7744520633811a5a3d809762d8cc"}, - {file = "orjson-3.9.14-cp310-none-win32.whl", hash = "sha256:1f7b6f3ef10ae8e3558abb729873d033dbb5843507c66b1c0767e32502ba96bb"}, - {file = "orjson-3.9.14-cp310-none-win_amd64.whl", hash = "sha256:ea890e6dc1711aeec0a33b8520e395c2f3d59ead5b4351a788e06bf95fc7ba81"}, - {file = "orjson-3.9.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c19009ff37f033c70acd04b636380379499dac2cba27ae7dfc24f304deabbc81"}, - {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19cdea0664aec0b7f385be84986d4defd3334e9c3c799407686ee1c26f7b8251"}, - {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135d518f73787ce323b1a5e21fb854fe22258d7a8ae562b81a49d6c7f826f2a3"}, - {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2cf1d0557c61c75e18cf7d69fb689b77896e95553e212c0cc64cf2087944b84"}, - {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7c11667421df2d8b18b021223505dcc3ee51be518d54e4dc49161ac88ac2b87"}, - {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eefc41ba42e75ed88bc396d8fe997beb20477f3e7efa000cd7a47eda452fbb2"}, - {file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:917311d6a64d1c327c0dfda1e41f3966a7fb72b11ca7aa2e7a68fcccc7db35d9"}, - {file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4dc1c132259b38d12c6587d190cd09cd76e3b5273ce71fe1372437b4cbc65f6f"}, - {file = "orjson-3.9.14-cp311-none-win32.whl", hash = "sha256:6f39a10408478f4c05736a74da63727a1ae0e83e3533d07b19443400fe8591ca"}, - {file = "orjson-3.9.14-cp311-none-win_amd64.whl", hash = "sha256:26280a7fcb62d8257f634c16acebc3bec626454f9ab13558bbf7883b9140760e"}, - {file = "orjson-3.9.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:08e722a8d06b13b67a51f247a24938d1a94b4b3862e40e0eef3b2e98c99cd04c"}, - {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2591faa0c031cf3f57e5bce1461cfbd6160f3f66b5a72609a130924917cb07d"}, - {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2450d87dd7b4f277f4c5598faa8b49a0c197b91186c47a2c0b88e15531e4e3e"}, - {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90903d2908158a2c9077a06f11e27545de610af690fb178fd3ba6b32492d4d1c"}, - {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6f095eef0026eae76fc212f20f786011ecf482fc7df2f4c272a8ae6dd7b1ef"}, - {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751250a31fef2bac05a2da2449aae7142075ea26139271f169af60456d8ad27a"}, - {file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a1af21160a38ee8be3f4fcf24ee4b99e6184cadc7f915d599f073f478a94d2c"}, - {file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:449bf090b2aa4e019371d7511a6ea8a5a248139205c27d1834bb4b1e3c44d936"}, - {file = "orjson-3.9.14-cp312-none-win_amd64.whl", hash = "sha256:a603161318ff699784943e71f53899983b7dee571b4dd07c336437c9c5a272b0"}, - {file = "orjson-3.9.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:814f288c011efdf8f115c5ebcc1ab94b11da64b207722917e0ceb42f52ef30a3"}, - {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88cafb100af68af3b9b29b5ccd09fdf7a48c63327916c8c923a94c336d38dd3"}, - {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba3518b999f88882ade6686f1b71e207b52e23546e180499be5bbb63a2f9c6e6"}, - {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978f416bbff9da8d2091e3cf011c92da68b13f2c453dcc2e8109099b2a19d234"}, - {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fc593cf836f631153d0e21beaeb8d26e144445c73645889335c2247fcd71a0"}, - {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d1528db3c7554f9d6eeb09df23cb80dd5177ec56eeb55cc5318826928de506"}, - {file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7183cc68ee2113b19b0b8714221e5e3b07b3ba10ca2bb108d78fd49cefaae101"}, - {file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df3266d54246cb56b8bb17fa908660d2a0f2e3f63fbc32451ffc1b1505051d07"}, - {file = "orjson-3.9.14-cp38-none-win32.whl", hash = "sha256:7913079b029e1b3501854c9a78ad938ed40d61fe09bebab3c93e60ff1301b189"}, - {file = "orjson-3.9.14-cp38-none-win_amd64.whl", hash = "sha256:29512eb925b620e5da2fd7585814485c67cc6ba4fe739a0a700c50467a8a8065"}, - {file = "orjson-3.9.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5bf597530544db27a8d76aced49cfc817ee9503e0a4ebf0109cd70331e7bbe0c"}, - {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac650d49366fa41fe702e054cb560171a8634e2865537e91f09a8d05ea5b1d37"}, - {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:236230433a9a4968ab895140514c308fdf9f607cb8bee178a04372b771123860"}, - {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3014ccbda9be0b1b5f8ea895121df7e6524496b3908f4397ff02e923bcd8f6dd"}, - {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac0c7eae7ad3a223bde690565442f8a3d620056bd01196f191af8be58a5248e1"}, - {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca33fdd0b38839b01912c57546d4f412ba7bfa0faf9bf7453432219aec2df07"}, - {file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f75823cc1674a840a151e999a7dfa0d86c911150dd6f951d0736ee9d383bf415"}, - {file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f52ac2eb49e99e7373f62e2a68428c6946cda52ce89aa8fe9f890c7278e2d3a"}, - {file = "orjson-3.9.14-cp39-none-win32.whl", hash = "sha256:0572f174f50b673b7df78680fb52cd0087a8585a6d06d295a5f790568e1064c6"}, - {file = "orjson-3.9.14-cp39-none-win_amd64.whl", hash = "sha256:ab90c02cb264250b8a58cedcc72ed78a4a257d956c8d3c8bebe9751b818dfad8"}, - {file = "orjson-3.9.14.tar.gz", hash = "sha256:06fb40f8e49088ecaa02f1162581d39e2cf3fd9dbbfe411eb2284147c99bad79"}, + {file = "orjson-3.9.15-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d61f7ce4727a9fa7680cd6f3986b0e2c732639f46a5e0156e550e35258aa313a"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"}, + {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"}, + {file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"}, + {file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"}, + {file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"}, + {file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"}, + {file = "orjson-3.9.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c8e8fe01e435005d4421f183038fc70ca85d2c1e490f51fb972db92af6e047c2"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"}, + {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"}, + {file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"}, + {file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"}, + {file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"}, + {file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"}, + {file = "orjson-3.9.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:82425dd5c7bd3adfe4e94c78e27e2fa02971750c2b7ffba648b0f5d5cc016a73"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"}, + {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"}, + {file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"}, + {file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"}, + {file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"}, + {file = "orjson-3.9.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b17f0f14a9c0ba55ff6279a922d1932e24b13fc218a3e968ecdbf791b3682b25"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"}, + {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"}, + {file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"}, + {file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"}, + {file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"}, + {file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"}, + {file = "orjson-3.9.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6fc2fe4647927070df3d93f561d7e588a38865ea0040027662e3e541d592811e"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"}, + {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"}, + {file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"}, + {file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"}, + {file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"}, + {file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"}, + {file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"}, ] [[package]] diff --git a/mobile/lib/modules/album/ui/album_thumbnail_card.dart b/mobile/lib/modules/album/ui/album_thumbnail_card.dart index 880312322..1da284572 100644 --- a/mobile/lib/modules/album/ui/album_thumbnail_card.dart +++ b/mobile/lib/modules/album/ui/album_thumbnail_card.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/store.dart'; -import 'package:immich_mobile/shared/ui/immich_image.dart'; +import 'package:immich_mobile/shared/ui/immich_thumbnail.dart'; class AlbumThumbnailCard extends StatelessWidget { final Function()? onTap; @@ -45,8 +45,8 @@ class AlbumThumbnailCard extends StatelessWidget { ); } - buildAlbumThumbnail() => ImmichImage.thumbnail( - album.thumbnail.value, + buildAlbumThumbnail() => ImmichThumbnail( + asset: album.thumbnail.value, width: cardSize, height: cardSize, ); diff --git a/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart b/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart index f70c706f3..5a27def4c 100644 --- a/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart +++ b/mobile/lib/modules/album/ui/shared_album_thumbnail_image.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/ui/immich_image.dart'; +import 'package:immich_mobile/shared/ui/immich_thumbnail.dart'; class SharedAlbumThumbnailImage extends HookConsumerWidget { final Asset asset; @@ -16,8 +16,8 @@ class SharedAlbumThumbnailImage extends HookConsumerWidget { }, child: Stack( children: [ - ImmichImage.thumbnail( - asset, + ImmichThumbnail( + asset: asset, width: 500, height: 500, ), diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart index 2e826e86d..e6b2ade6b 100644 --- a/mobile/lib/modules/album/views/sharing_page.dart +++ b/mobile/lib/modules/album/views/sharing_page.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/modules/partner/ui/partner_list.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; -import 'package:immich_mobile/shared/ui/immich_image.dart'; +import 'package:immich_mobile/shared/ui/immich_thumbnail.dart'; @RoutePage() class SharingPage extends HookConsumerWidget { @@ -72,8 +72,8 @@ class SharingPage extends HookConsumerWidget { contentPadding: const EdgeInsets.symmetric(horizontal: 12), leading: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(8)), - child: ImmichImage.thumbnail( - album.thumbnail.value, + child: ImmichThumbnail( + asset: album.thumbnail.value, width: 60, height: 60, ), diff --git a/mobile/lib/modules/asset_viewer/image_providers/immich_local_image_provider.dart b/mobile/lib/modules/asset_viewer/image_providers/immich_local_image_provider.dart index 4c1e9fc5c..3094c6907 100644 --- a/mobile/lib/modules/asset_viewer/image_providers/immich_local_image_provider.dart +++ b/mobile/lib/modules/asset_viewer/image_providers/immich_local_image_provider.dart @@ -11,7 +11,7 @@ import 'package:photo_manager/photo_manager.dart'; /// The local image provider for an asset /// Only viable -class ImmichLocalImageProvider extends ImageProvider { +class ImmichLocalImageProvider extends ImageProvider { final Asset asset; ImmichLocalImageProvider({ @@ -21,15 +21,18 @@ class ImmichLocalImageProvider extends ImageProvider { /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key /// that describes the precise image to load. @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(asset); + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); } @override - ImageStreamCompleter loadImage(Asset key, ImageDecoderCallback decode) { + ImageStreamCompleter loadImage( + ImmichLocalImageProvider key, + ImageDecoderCallback decode, + ) { final chunkEvents = StreamController(); return MultiImageStreamCompleter( - codec: _codec(key, decode, chunkEvents), + codec: _codec(key.asset, decode, chunkEvents), scale: 1.0, chunkEvents: chunkEvents.stream, informationCollector: () sync* { @@ -82,11 +85,6 @@ class ImmichLocalImageProvider extends ImageProvider { yield codec; } catch (error) { throw StateError("Loading asset ${asset.fileName} failed"); - } finally { - if (Platform.isIOS) { - // Clean up this file - await file.delete(); - } } } } diff --git a/mobile/lib/modules/asset_viewer/image_providers/immich_local_thumbnail_provider.dart b/mobile/lib/modules/asset_viewer/image_providers/immich_local_thumbnail_provider.dart new file mode 100644 index 000000000..bb86cfafd --- /dev/null +++ b/mobile/lib/modules/asset_viewer/image_providers/immich_local_thumbnail_provider.dart @@ -0,0 +1,86 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:cached_network_image/cached_network_image.dart'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:photo_manager/photo_manager.dart'; + +/// The local image provider for an asset +/// Only viable +class ImmichLocalThumbnailProvider extends ImageProvider { + final Asset asset; + final int height; + final int width; + + ImmichLocalThumbnailProvider({ + required this.asset, + this.height = 256, + this.width = 256, + }) : assert(asset.local != null, 'Only usable when asset.local is set'); + + /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key + /// that describes the precise image to load. + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(asset); + } + + @override + ImageStreamCompleter loadImage(Asset key, ImageDecoderCallback decode) { + final chunkEvents = StreamController(); + return MultiImageStreamCompleter( + codec: _codec(key, decode, chunkEvents), + scale: 1.0, + chunkEvents: chunkEvents.stream, + informationCollector: () sync* { + yield ErrorDescription(asset.fileName); + }, + ); + } + + // Streams in each stage of the image as we ask for it + Stream _codec( + Asset key, + ImageDecoderCallback decode, + StreamController chunkEvents, + ) async* { + // Load a small thumbnail + final thumbBytes = await asset.local?.thumbnailDataWithSize( + const ThumbnailSize.square(32), + quality: 75, + ); + if (thumbBytes != null) { + final buffer = await ui.ImmutableBuffer.fromUint8List(thumbBytes); + final codec = await decode(buffer); + yield codec; + } else { + debugPrint("Loading thumb for ${asset.fileName} failed"); + } + + final normalThumbBytes = + await asset.local?.thumbnailDataWithSize(ThumbnailSize(width, height)); + if (normalThumbBytes == null) { + throw StateError( + "Loading thumb for local photo ${asset.fileName} failed", + ); + } + final buffer = await ui.ImmutableBuffer.fromUint8List(normalThumbBytes); + final codec = await decode(buffer); + yield codec; + + chunkEvents.close(); + } + + @override + bool operator ==(Object other) { + if (other is! ImmichLocalThumbnailProvider) return false; + if (identical(this, other)) return true; + return asset == other.asset; + } + + @override + int get hashCode => asset.hashCode; +} diff --git a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart index 9f9af7ade..d9fbd8048 100644 --- a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart +++ b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_image_provider.dart @@ -13,10 +13,13 @@ import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; /// Our Image Provider HTTP client to make the request -final _httpClient = HttpClient()..autoUncompress = false; +final _httpClient = HttpClient() + ..autoUncompress = false + ..maxConnectionsPerHost = 10; /// The remote image provider -class ImmichRemoteImageProvider extends ImageProvider { +class ImmichRemoteImageProvider + extends ImageProvider { /// The [Asset.remoteId] of the asset to fetch final String assetId; @@ -32,16 +35,20 @@ class ImmichRemoteImageProvider extends ImageProvider { /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key /// that describes the precise image to load. @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture('$assetId,$isThumbnail'); + Future obtainKey( + ImageConfiguration configuration, + ) { + return SynchronousFuture(this); } @override - ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) { - final id = key.split(',').first; + ImageStreamCompleter loadImage( + ImmichRemoteImageProvider key, + ImageDecoderCallback decode, + ) { final chunkEvents = StreamController(); return MultiImageStreamCompleter( - codec: _codec(id, decode, chunkEvents), + codec: _codec(key, decode, chunkEvents), scale: 1.0, chunkEvents: chunkEvents.stream, ); @@ -61,14 +68,14 @@ class ImmichRemoteImageProvider extends ImageProvider { // Streams in each stage of the image as we ask for it Stream _codec( - String key, + ImmichRemoteImageProvider key, ImageDecoderCallback decode, StreamController chunkEvents, ) async* { // Load a preview to the chunk events - if (_loadPreview || isThumbnail) { + if (_loadPreview || key.isThumbnail) { final preview = getThumbnailUrlForRemoteId( - assetId, + key.assetId, type: api.ThumbnailFormat.WEBP, ); @@ -80,14 +87,14 @@ class ImmichRemoteImageProvider extends ImageProvider { } // Guard thumnbail rendering - if (isThumbnail) { + if (key.isThumbnail) { await chunkEvents.close(); return; } // Load the higher resolution version of the image final url = getThumbnailUrlForRemoteId( - assetId, + key.assetId, type: api.ThumbnailFormat.JPEG, ); final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); @@ -96,7 +103,7 @@ class ImmichRemoteImageProvider extends ImageProvider { // Load the final remote image if (_useOriginal) { // Load the original image - final url = getImageUrlFromId(assetId); + final url = getImageUrlFromId(key.assetId); final codec = await _loadFromUri(Uri.parse(url), decode, chunkEvents); yield codec; } @@ -137,7 +144,7 @@ class ImmichRemoteImageProvider extends ImageProvider { bool operator ==(Object other) { if (other is! ImmichRemoteImageProvider) return false; if (identical(this, other)) return true; - return assetId == other.assetId; + return assetId == other.assetId && isThumbnail == other.isThumbnail; } @override diff --git a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart index 8332d8d3d..92b85b347 100644 --- a/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart +++ b/mobile/lib/modules/asset_viewer/image_providers/immich_remote_thumbnail_provider.dart @@ -12,14 +12,17 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +/// Our HTTP client to make the request +final _httpClient = HttpClient() + ..autoUncompress = false + ..maxConnectionsPerHost = 100; + /// The remote image provider -class ImmichRemoteThumbnailProvider extends ImageProvider { +class ImmichRemoteThumbnailProvider + extends ImageProvider { /// The [Asset.remoteId] of the asset to fetch final String assetId; - /// Our HTTP client to make the request - final _httpClient = HttpClient()..autoUncompress = false; - ImmichRemoteThumbnailProvider({ required this.assetId, }); @@ -27,12 +30,17 @@ class ImmichRemoteThumbnailProvider extends ImageProvider { /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key /// that describes the precise image to load. @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(assetId); + Future obtainKey( + ImageConfiguration configuration, + ) { + return SynchronousFuture(this); } @override - ImageStreamCompleter loadImage(String key, ImageDecoderCallback decode) { + ImageStreamCompleter loadImage( + ImmichRemoteThumbnailProvider key, + ImageDecoderCallback decode, + ) { final chunkEvents = StreamController(); return MultiImageStreamCompleter( codec: _codec(key, decode, chunkEvents), @@ -43,13 +51,13 @@ class ImmichRemoteThumbnailProvider extends ImageProvider { // Streams in each stage of the image as we ask for it Stream _codec( - String key, + ImmichRemoteThumbnailProvider key, ImageDecoderCallback decode, StreamController chunkEvents, ) async* { // Load a preview to the chunk events final preview = getThumbnailUrlForRemoteId( - assetId, + key.assetId, type: api.ThumbnailFormat.WEBP, ); diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index a78c085de..48eb778c1 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'dart:math'; +import 'dart:ui' as ui; import 'package:easy_localization/easy_localization.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -10,6 +10,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/current_album.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; @@ -26,13 +27,13 @@ import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.da import 'package:immich_mobile/modules/home/ui/upload_dialog.dart'; import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; +import 'package:immich_mobile/shared/ui/immich_thumbnail.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart'; import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart'; @@ -481,15 +482,9 @@ class GalleryViewerPage extends HookConsumerWidget { ), child: ClipRRect( borderRadius: BorderRadius.circular(4), - child: CachedNetworkImage( + child: Image( fit: BoxFit.cover, - imageUrl: - '${Store.get(StoreKey.serverEndpoint)}/asset/thumbnail/$assetId', - httpHeaders: { - "x-immich-user-token": Store.get(StoreKey.accessToken), - }, - errorWidget: (context, url, error) => - const Icon(Icons.image_not_supported_outlined), + image: ImmichRemoteImageProvider(assetId: assetId!), ), ), ), @@ -740,9 +735,15 @@ class GalleryViewerPage extends HookConsumerWidget { isZoomed.value = state != PhotoViewScaleState.initial; ref.read(showControlsProvider.notifier).show = !isZoomed.value; }, - loadingBuilder: (context, event, index) => ImmichImage.thumbnail( - asset(), - fit: BoxFit.contain, + loadingBuilder: (context, event, index) => ImageFiltered( + imageFilter: ui.ImageFilter.blur( + sigmaX: 1, + sigmaY: 1, + ), + child: ImmichThumbnail( + asset: asset(), + fit: BoxFit.contain, + ), ), pageController: controller, scrollPhysics: isZoomed.value diff --git a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart index 0967bf52a..eb125f27f 100644 --- a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart @@ -40,7 +40,7 @@ class VideoViewerPage extends HookWidget { controlsSafeAreaMinimum: const EdgeInsets.only( bottom: 100, ), - placeholder: placeholder, + placeholder: SizedBox.expand(child: placeholder), showControls: showControls && !isMotionVideo, hideControlsTimer: hideControlsTimer, customControls: const VideoPlayerControls(), @@ -58,7 +58,7 @@ class VideoViewerPage extends HookWidget { if (controller == null) { return Stack( children: [ - if (placeholder != null) placeholder!, + if (placeholder != null) SizedBox.expand(child: placeholder!), const DelayedLoadingIndicator( fadeInDuration: Duration(milliseconds: 500), ), diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index 73b31617f..a194bc2ad 100644 --- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -4,7 +4,7 @@ import 'package:flutter/services.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/ui/immich_image.dart'; +import 'package:immich_mobile/shared/ui/immich_thumbnail.dart'; import 'package:immich_mobile/utils/storage_indicator.dart'; import 'package:isar/isar.dart'; @@ -134,10 +134,10 @@ class ThumbnailImage extends StatelessWidget { tag: isFromDto ? '${asset.remoteId}-$heroOffset' : asset.id + heroOffset, - child: ImmichImage.thumbnail( - asset, - height: 300, - width: 300, + child: ImmichThumbnail( + asset: asset, + height: 250, + width: 250, ), ), ); diff --git a/mobile/lib/modules/memories/ui/memory_card.dart b/mobile/lib/modules/memories/ui/memory_card.dart index 7a9355044..af57c272a 100644 --- a/mobile/lib/modules/memories/ui/memory_card.dart +++ b/mobile/lib/modules/memories/ui/memory_card.dart @@ -1,10 +1,11 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/models/store.dart'; +import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; class MemoryCard extends StatelessWidget { @@ -21,8 +22,6 @@ class MemoryCard extends StatelessWidget { super.key, }); - String get accessToken => Store.get(StoreKey.accessToken); - @override Widget build(BuildContext context) { return Card( @@ -37,20 +36,8 @@ class MemoryCard extends StatelessWidget { clipBehavior: Clip.hardEdge, child: Stack( children: [ - ImageFiltered( - imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), - child: Container( - decoration: BoxDecoration( - image: DecorationImage( - image: ImmichImage.imageProvider( - asset: asset, - isThumbnail: true, - ), - fit: BoxFit.cover, - ), - ), - child: Container(color: Colors.black.withOpacity(0.2)), - ), + SizedBox.expand( + child: _BlurredBackdrop(asset: asset), ), LayoutBuilder( builder: (context, constraints) { @@ -113,3 +100,50 @@ class MemoryCard extends StatelessWidget { ); } } + +class _BlurredBackdrop extends HookWidget { + final Asset asset; + + const _BlurredBackdrop({required this.asset}); + + @override + Widget build(BuildContext context) { + final blurhash = useBlurHashRef(asset).value; + if (blurhash != null) { + // Use a nice cheap blur hash image decoration + return Container( + decoration: BoxDecoration( + image: DecorationImage( + image: MemoryImage( + blurhash, + ), + fit: BoxFit.cover, + ), + ), + child: Container( + color: Colors.black.withOpacity(0.2), + ), + ); + } else { + // Fall back to using a more expensive image filtered + // Since the ImmichImage is already precached, we can + // safely use that as the image provider + return ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30), + child: Container( + decoration: BoxDecoration( + image: DecorationImage( + image: ImmichImage.imageProvider( + asset: asset, + ), + fit: BoxFit.cover, + ), + ), + child: Container( + color: Colors.black.withOpacity(0.2), + ), + ), + ); + } + } +} diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart index 199af835c..9308e812d 100644 --- a/mobile/lib/modules/memories/views/memory_page.dart +++ b/mobile/lib/modules/memories/views/memory_page.dart @@ -109,25 +109,13 @@ class MemoryPage extends HookConsumerWidget { asset = memories[nextMemoryIndex].assets.first; } - // Gets the thumbnail url and precaches it - final precaches = >[]; - - precaches.addAll([ - precacheImage( - ImmichImage.imageProvider( - asset: asset, - ), - context, + // Precache the asset + await precacheImage( + ImmichImage.imageProvider( + asset: asset, ), - precacheImage( - ImmichImage.imageProvider( - asset: asset, - isThumbnail: true, - ), - context, - ), - ]); - await Future.wait(precaches); + context, + ); } // Precache the next page right away if we are on the first page diff --git a/mobile/lib/shared/models/asset.dart b/mobile/lib/shared/models/asset.dart index dd38b050b..3c3c4df82 100644 --- a/mobile/lib/shared/models/asset.dart +++ b/mobile/lib/shared/models/asset.dart @@ -38,7 +38,8 @@ class Asset { // stack handling to properly handle it stackParentId = remote.stackParentId == remote.id ? null : remote.stackParentId, - stackCount = remote.stackCount; + stackCount = remote.stackCount, + thumbhash = remote.thumbhash; Asset.local(AssetEntity local, List hash) : localId = local.id, @@ -91,6 +92,7 @@ class Asset { this.stackCount = 0, this.isReadOnly = false, this.isOffline = false, + this.thumbhash, }); @ignore @@ -119,6 +121,8 @@ class Asset { /// because Isar cannot sort lists of byte arrays String checksum; + String? thumbhash; + @Index(unique: false, replace: false, type: IndexType.hash) String? remoteId; @@ -279,6 +283,7 @@ class Asset { a.exifInfo?.latitude != exifInfo?.latitude || a.exifInfo?.longitude != exifInfo?.longitude || // no local stack count or different count from remote + a.thumbhash != thumbhash || ((stackCount == null && a.stackCount != null) || (stackCount != null && a.stackCount != null && @@ -343,6 +348,7 @@ class Asset { isReadOnly: a.isReadOnly, isOffline: a.isOffline, exifInfo: a.exifInfo?.copyWith(id: id) ?? exifInfo, + thumbhash: a.thumbhash, ); } else { // add only missing values (and set isLocal to true) @@ -379,6 +385,7 @@ class Asset { ExifInfo? exifInfo, String? stackParentId, int? stackCount, + String? thumbhash, }) => Asset( id: id ?? this.id, @@ -403,6 +410,7 @@ class Asset { exifInfo: exifInfo ?? this.exifInfo, stackParentId: stackParentId ?? this.stackParentId, stackCount: stackCount ?? this.stackCount, + thumbhash: thumbhash ?? this.thumbhash, ); Future put(Isar db) async { diff --git a/mobile/lib/shared/models/asset.g.dart b/mobile/lib/shared/models/asset.g.dart index d845b5353..5912f291b 100644 --- a/mobile/lib/shared/models/asset.g.dart +++ b/mobile/lib/shared/models/asset.g.dart @@ -102,19 +102,24 @@ const AssetSchema = CollectionSchema( name: r'stackParentId', type: IsarType.string, ), - r'type': PropertySchema( + r'thumbhash': PropertySchema( id: 17, + name: r'thumbhash', + type: IsarType.string, + ), + r'type': PropertySchema( + id: 18, name: r'type', type: IsarType.byte, enumMap: _AssettypeEnumValueMap, ), r'updatedAt': PropertySchema( - id: 18, + id: 19, name: r'updatedAt', type: IsarType.dateTime, ), r'width': PropertySchema( - id: 19, + id: 20, name: r'width', type: IsarType.int, ) @@ -210,6 +215,12 @@ int _assetEstimateSize( bytesCount += 3 + value.length * 3; } } + { + final value = object.thumbhash; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } return bytesCount; } @@ -236,9 +247,10 @@ void _assetSerialize( writer.writeString(offsets[14], object.remoteId); writer.writeLong(offsets[15], object.stackCount); writer.writeString(offsets[16], object.stackParentId); - writer.writeByte(offsets[17], object.type.index); - writer.writeDateTime(offsets[18], object.updatedAt); - writer.writeInt(offsets[19], object.width); + writer.writeString(offsets[17], object.thumbhash); + writer.writeByte(offsets[18], object.type.index); + writer.writeDateTime(offsets[19], object.updatedAt); + writer.writeInt(offsets[20], object.width); } Asset _assetDeserialize( @@ -266,10 +278,11 @@ Asset _assetDeserialize( remoteId: reader.readStringOrNull(offsets[14]), stackCount: reader.readLongOrNull(offsets[15]), stackParentId: reader.readStringOrNull(offsets[16]), - type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ?? + thumbhash: reader.readStringOrNull(offsets[17]), + type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? AssetType.other, - updatedAt: reader.readDateTime(offsets[18]), - width: reader.readIntOrNull(offsets[19]), + updatedAt: reader.readDateTime(offsets[19]), + width: reader.readIntOrNull(offsets[20]), ); return object; } @@ -316,11 +329,13 @@ P _assetDeserializeProp

( case 16: return (reader.readStringOrNull(offset)) as P; case 17: + return (reader.readStringOrNull(offset)) as P; + case 18: return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? AssetType.other) as P; - case 18: - return (reader.readDateTime(offset)) as P; case 19: + return (reader.readDateTime(offset)) as P; + case 20: return (reader.readIntOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -2078,6 +2093,152 @@ extension AssetQueryFilter on QueryBuilder { }); } + QueryBuilder thumbhashIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'thumbhash', + )); + }); + } + + QueryBuilder thumbhashIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'thumbhash', + )); + }); + } + + QueryBuilder thumbhashEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'thumbhash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder thumbhashGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'thumbhash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder thumbhashLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'thumbhash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder thumbhashBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'thumbhash', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder thumbhashStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'thumbhash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder thumbhashEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'thumbhash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder thumbhashContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'thumbhash', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder thumbhashMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'thumbhash', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder thumbhashIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'thumbhash', + value: '', + )); + }); + } + + QueryBuilder thumbhashIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'thumbhash', + value: '', + )); + }); + } + QueryBuilder typeEqualTo( AssetType value) { return QueryBuilder.apply(this, (query) { @@ -2462,6 +2623,18 @@ extension AssetQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByThumbhash() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'thumbhash', Sort.asc); + }); + } + + QueryBuilder sortByThumbhashDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'thumbhash', Sort.desc); + }); + } + QueryBuilder sortByType() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'type', Sort.asc); @@ -2716,6 +2889,18 @@ extension AssetQuerySortThenBy on QueryBuilder { }); } + QueryBuilder thenByThumbhash() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'thumbhash', Sort.asc); + }); + } + + QueryBuilder thenByThumbhashDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'thumbhash', Sort.desc); + }); + } + QueryBuilder thenByType() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'type', Sort.asc); @@ -2864,6 +3049,13 @@ extension AssetQueryWhereDistinct on QueryBuilder { }); } + QueryBuilder distinctByThumbhash( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'thumbhash', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByType() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'type'); @@ -2992,6 +3184,12 @@ extension AssetQueryProperty on QueryBuilder { }); } + QueryBuilder thumbhashProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'thumbhash'); + }); + } + QueryBuilder typeProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'type'); diff --git a/mobile/lib/shared/ui/fade_in_placeholder_image.dart b/mobile/lib/shared/ui/fade_in_placeholder_image.dart new file mode 100644 index 000000000..e0620ea4f --- /dev/null +++ b/mobile/lib/shared/ui/fade_in_placeholder_image.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/shared/ui/transparent_image.dart'; + +class FadeInPlaceholderImage extends StatelessWidget { + final Widget placeholder; + final ImageProvider image; + final Duration duration; + final BoxFit fit; + + const FadeInPlaceholderImage({ + super.key, + required this.placeholder, + required this.image, + this.duration = const Duration(milliseconds: 100), + this.fit = BoxFit.cover, + }); + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: Stack( + fit: StackFit.expand, + children: [ + placeholder, + FadeInImage( + fadeInDuration: duration, + image: image, + fit: fit, + placeholder: MemoryImage(kTransparentImage), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/shared/ui/hooks/blurhash_hook.dart b/mobile/lib/shared/ui/hooks/blurhash_hook.dart new file mode 100644 index 000000000..24b3c25e1 --- /dev/null +++ b/mobile/lib/shared/ui/hooks/blurhash_hook.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:thumbhash/thumbhash.dart' as thumbhash; + +ObjectRef useBlurHashRef(Asset? asset) { + if (asset?.thumbhash == null) { + return useRef(null); + } + + final rbga = thumbhash.thumbHashToRGBA( + base64Decode(asset!.thumbhash!), + ); + + return useRef(thumbhash.rgbaToBmp(rbga)); +} diff --git a/mobile/lib/shared/ui/immich_image.dart b/mobile/lib/shared/ui/immich_image.dart index 280f7de17..3137f6301 100644 --- a/mobile/lib/shared/ui/immich_image.dart +++ b/mobile/lib/shared/ui/immich_image.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -9,8 +7,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.d import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:octo_image/octo_image.dart'; -import 'package:photo_manager/photo_manager.dart'; -import 'package:photo_manager_image_provider/photo_manager_image_provider.dart'; class ImmichImage extends StatelessWidget { const ImmichImage( @@ -19,8 +15,6 @@ class ImmichImage extends StatelessWidget { this.height, this.fit = BoxFit.cover, this.placeholder = const ThumbnailPlaceholder(), - this.isThumbnail = false, - this.thumbnailSize = 250, super.key, }); @@ -29,32 +23,6 @@ class ImmichImage extends StatelessWidget { final double? width; final double? height; final BoxFit fit; - final bool isThumbnail; - final int thumbnailSize; - - /// Factory constructor to use the thumbnail variant - factory ImmichImage.thumbnail( - Asset? asset, { - BoxFit fit = BoxFit.cover, - double? width, - double? height, - }) { - // Use the width and height to derive thumbnail size - final thumbnailSize = max(width ?? 250, height ?? 250).toInt(); - - return ImmichImage( - asset, - isThumbnail: true, - fit: fit, - width: width, - height: height, - placeholder: ThumbnailPlaceholder( - height: thumbnailSize.toDouble(), - width: thumbnailSize.toDouble(), - ), - thumbnailSize: thumbnailSize, - ); - } // Helper function to return the image provider for the asset // either by using the asset ID or the asset itself @@ -66,8 +34,6 @@ class ImmichImage extends StatelessWidget { static ImageProvider imageProvider({ Asset? asset, String? assetId, - bool isThumbnail = false, - int thumbnailSize = 250, }) { if (asset == null && assetId == null) { throw Exception('Must supply either asset or assetId'); @@ -76,24 +42,18 @@ class ImmichImage extends StatelessWidget { if (asset == null) { return ImmichRemoteImageProvider( assetId: assetId!, - isThumbnail: isThumbnail, + isThumbnail: false, ); } - if (useLocal(asset) && isThumbnail) { - return AssetEntityImageProvider( - asset.local!, - isOriginal: false, - thumbnailSize: ThumbnailSize.square(thumbnailSize), - ); - } else if (useLocal(asset) && !isThumbnail) { + if (useLocal(asset)) { return ImmichLocalImageProvider( asset: asset, ); } else { return ImmichRemoteImageProvider( assetId: asset.remoteId!, - isThumbnail: isThumbnail, + isThumbnail: false, ); } } @@ -105,15 +65,11 @@ class ImmichImage extends StatelessWidget { Widget build(BuildContext context) { if (asset == null) { return Container( - decoration: const BoxDecoration( - color: Colors.grey, - ), - child: SizedBox( - width: width, - height: height, - child: const Center( - child: Icon(Icons.no_photography), - ), + color: Colors.grey, + width: width, + height: height, + child: const Center( + child: Icon(Icons.no_photography), ), ); } @@ -131,7 +87,6 @@ class ImmichImage extends StatelessWidget { }, image: ImmichImage.imageProvider( asset: asset, - isThumbnail: isThumbnail, ), width: width, height: height, diff --git a/mobile/lib/shared/ui/immich_thumbnail.dart b/mobile/lib/shared/ui/immich_thumbnail.dart new file mode 100644 index 000000000..fe35bdaac --- /dev/null +++ b/mobile/lib/shared/ui/immich_thumbnail.dart @@ -0,0 +1,89 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_thumbnail_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart'; +import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart'; +import 'package:octo_image/octo_image.dart'; + +class ImmichThumbnail extends HookWidget { + const ImmichThumbnail({ + this.asset, + this.width = 250, + this.height = 250, + this.fit = BoxFit.cover, + super.key, + }); + + final Asset? asset; + final double width; + final double height; + final BoxFit fit; + + /// Helper function to return the image provider for the asset thumbnail + /// either by using the asset ID or the asset itself + /// [asset] is the Asset to request, or else use [assetId] to get a remote + /// image provider + static ImageProvider imageProvider({ + Asset? asset, + String? assetId, + int thumbnailSize = 256, + }) { + if (asset == null && assetId == null) { + throw Exception('Must supply either asset or assetId'); + } + + if (asset == null) { + return ImmichRemoteImageProvider( + assetId: assetId!, + isThumbnail: true, + ); + } + + if (useLocal(asset)) { + return ImmichLocalThumbnailProvider( + asset: asset, + height: thumbnailSize, + width: thumbnailSize, + ); + } else { + return ImmichRemoteImageProvider( + assetId: asset.remoteId!, + isThumbnail: true, + ); + } + } + + static bool useLocal(Asset asset) => !asset.isRemote || asset.isLocal; + + @override + Widget build(BuildContext context) { + Uint8List? blurhash = useBlurHashRef(asset).value; + if (asset == null) { + return Container( + color: Colors.grey, + width: width, + height: height, + child: const Center( + child: Icon(Icons.no_photography), + ), + ); + } + + return OctoImage.fromSet( + placeholderFadeInDuration: Duration.zero, + fadeInDuration: Duration.zero, + fadeOutDuration: const Duration(milliseconds: 100), + octoSet: blurHashOrPlaceholder(blurhash), + image: ImmichThumbnail.imageProvider( + asset: asset, + ), + width: width, + height: height, + fit: fit, + ); + } +} diff --git a/mobile/lib/shared/ui/thumbhash_placeholder.dart b/mobile/lib/shared/ui/thumbhash_placeholder.dart new file mode 100644 index 000000000..0ec64d376 --- /dev/null +++ b/mobile/lib/shared/ui/thumbhash_placeholder.dart @@ -0,0 +1,48 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart'; +import 'package:immich_mobile/shared/ui/fade_in_placeholder_image.dart'; +import 'package:octo_image/octo_image.dart'; + +/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as +/// placeholder and [OctoError.icon] as error. +OctoSet blurHashOrPlaceholder( + Uint8List? blurhash, { + BoxFit? fit, + Text? errorMessage, +}) { + return OctoSet( + placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), + errorBuilder: blurHashErrorBuilder(blurhash, fit: fit), + ); +} + +OctoPlaceholderBuilder blurHashPlaceholderBuilder( + Uint8List? blurhash, { + BoxFit? fit, +}) { + return (context) => blurhash == null + ? const ThumbnailPlaceholder() + : FadeInPlaceholderImage( + placeholder: const ThumbnailPlaceholder(), + image: MemoryImage(blurhash), + fit: fit ?? BoxFit.cover, + ); +} + +OctoErrorBuilder blurHashErrorBuilder( + Uint8List? blurhash, { + BoxFit? fit, + Text? message, + IconData? icon, + Color? iconColor, + double? iconSize, +}) { + return OctoError.placeholderWithErrorIcon( + blurHashPlaceholderBuilder(blurhash, fit: fit), + message: message, + icon: icon, + iconColor: iconColor, + iconSize: iconSize, + ); +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 6bc37c922..9e379d465 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1491,6 +1491,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + thumbhash: + dependency: "direct main" + description: + name: thumbhash + sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37" + url: "https://pub.dev" + source: hosted + version: "0.1.0+1" time: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 0869d3973..50d170904 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -57,6 +57,7 @@ dependencies: flutter_local_notifications: ^16.3.2 timezone: ^0.9.2 octo_image: ^2.0.0 + thumbhash: 0.1.0+1 openapi: path: openapi diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index a918e2d2c..3359297e2 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -23,15 +23,15 @@ } }, "node_modules/@oazapfts/runtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.0.tgz", - "integrity": "sha512-1ovqeaeEvShbYge5/7ctJokpvqB0anBdfDNfU5jWstjV2/Gbe+vvcBM274Z0abM3IM0b9MmSNWYBXnJXYO8KCw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.1.tgz", + "integrity": "sha512-CMl7f1gXYpjIyEtDhg4YfXwr2MXfbadbvqwKbMsaHkVtSglmuz5A8jSyefTqaJlmh0MOA2ZNS9jnbfIdtcoDiw==", "dev": true }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/server/Dockerfile b/server/Dockerfile index 7ea2795ea..0ebd5c44c 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240222@sha256:2ff467d6ae5c00a2317eb7b13cb40ba5be0fd33c160175dba621b1bf72bc1cd1 as dev +FROM ghcr.io/immich-app/base-server-dev:20240227@sha256:b1e212c106ce2318a587e0b2ef377215c958e877f61993ed9310534e4589cce4 as dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -40,7 +40,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240222@sha256:9ae5eebf95cf7759eec9dcfbd9e48a722701075ac855209f2e0b01c631b76f5c +FROM ghcr.io/immich-app/base-server-prod:20240227@sha256:d47f5f7f2b6c53957c6353352b2fa24f2845da50e6491a7c74eb779ace10628c WORKDIR /usr/src/app ENV NODE_ENV=production \ diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index 0e09a68be..d869775c9 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -538,90 +538,6 @@ describe(`${AssetController.name} (e2e)`, () => { } }); - describe('GET /asset/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).get(`/asset/${uuidStub.notFound}`); - expect(body).toEqual(errorStub.unauthorized); - expect(status).toBe(401); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(server) - .get(`/asset/${uuidStub.invalid}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest(['id must be a UUID'])); - }); - - it('should require access', async () => { - const { status, body } = await request(server) - .get(`/asset/${asset4.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorStub.noPermission); - }); - - it('should get the asset info', async () => { - const { status, body } = await request(server) - .get(`/asset/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(200); - expect(body).toMatchObject({ id: asset1.id }); - }); - - it('should work with a shared link', async () => { - const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.INDIVIDUAL, - assetIds: [asset1.id], - }); - - const { status, body } = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`); - expect(status).toBe(200); - expect(body).toMatchObject({ id: asset1.id }); - }); - - it('should not send people data for shared links for un-authenticated users', async () => { - const personRepository = app.get(IPersonRepository); - const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' }); - - await personRepository.createFaces([ - { - assetId: asset1.id, - personId: person.id, - embedding: Array.from({ length: 512 }, Math.random), - }, - ]); - - const { status, body } = await request(server) - .put(`/asset/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ isFavorite: true }); - expect(status).toEqual(200); - expect(body).toMatchObject({ - id: asset1.id, - isFavorite: true, - people: [ - { - birthDate: null, - id: expect.any(String), - isHidden: false, - name: 'Test Person', - thumbnailPath: '', - }, - ], - }); - - const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.INDIVIDUAL, - assetIds: [asset1.id], - }); - - const data = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`); - expect(data.status).toBe(200); - expect(data.body).toMatchObject({ people: [] }); - }); - }); - describe('POST /asset/upload', () => { it('should require authentication', async () => { const { status, body } = await request(server) @@ -759,286 +675,6 @@ describe(`${AssetController.name} (e2e)`, () => { }); }); - describe('PUT /asset/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).put(`/asset/:${uuidStub.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(server) - .put(`/asset/${uuidStub.invalid}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest(['id must be a UUID'])); - }); - - it('should require access', async () => { - const { status, body } = await request(server) - .put(`/asset/${asset4.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorStub.noPermission); - }); - - it('should favorite an asset', async () => { - expect(asset1).toMatchObject({ isFavorite: false }); - - const { status, body } = await request(server) - .put(`/asset/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ isFavorite: true }); - expect(body).toMatchObject({ id: asset1.id, isFavorite: true }); - expect(status).toEqual(200); - }); - - it('should archive an asset', async () => { - expect(asset1).toMatchObject({ isArchived: false }); - - const { status, body } = await request(server) - .put(`/asset/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ isArchived: true }); - expect(body).toMatchObject({ id: asset1.id, isArchived: true }); - expect(status).toEqual(200); - }); - - it('should update date time original', async () => { - const { status, body } = await request(server) - .put(`/asset/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); - - expect(body).toMatchObject({ - id: asset1.id, - exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00.000Z' }), - }); - expect(status).toEqual(200); - }); - - it('should reject invalid gps coordinates', async () => { - for (const test of [ - { latitude: 12 }, - { longitude: 12 }, - { latitude: 12, longitude: 'abc' }, - { latitude: 'abc', longitude: 12 }, - { latitude: null, longitude: 12 }, - { latitude: 12, longitude: null }, - { latitude: 91, longitude: 12 }, - { latitude: -91, longitude: 12 }, - { latitude: 12, longitude: -181 }, - { latitude: 12, longitude: 181 }, - ]) { - const { status, body } = await request(server) - .put(`/asset/${asset1.id}`) - .send(test) - .set('Authorization', `Bearer ${user1.accessToken}`); - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest()); - } - }); - - it('should update gps data', async () => { - const { status, body } = await request(server) - .put(`/asset/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ latitude: 12, longitude: 12 }); - - expect(body).toMatchObject({ - id: asset1.id, - exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }), - }); - expect(status).toEqual(200); - }); - - it('should set the description', async () => { - const { status, body } = await request(server) - .put(`/asset/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ description: 'Test asset description' }); - expect(body).toMatchObject({ - id: asset1.id, - exifInfo: expect.objectContaining({ description: 'Test asset description' }), - }); - expect(status).toEqual(200); - }); - - it('should return tagged people', async () => { - const personRepository = app.get(IPersonRepository); - const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' }); - - await personRepository.createFaces([ - { - assetId: asset1.id, - personId: person.id, - embedding: Array.from({ length: 512 }, Math.random), - }, - ]); - - const { status, body } = await request(server) - .put(`/asset/${asset1.id}`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ isFavorite: true }); - expect(status).toEqual(200); - expect(body).toMatchObject({ - id: asset1.id, - isFavorite: true, - people: [ - { - birthDate: null, - id: expect.any(String), - isHidden: false, - name: 'Test Person', - thumbnailPath: '', - }, - ], - }); - }); - }); - - describe('GET /asset/statistics', () => { - beforeEach(async () => { - await api.assetApi.upload(server, user1.accessToken, 'favored_asset', { isFavorite: true }); - await api.assetApi.upload(server, user1.accessToken, 'archived_asset', { isArchived: true }); - await api.assetApi.upload(server, user1.accessToken, 'favored_archived_asset', { - isFavorite: true, - isArchived: true, - }); - }); - - it('should require authentication', async () => { - const { status, body } = await request(server).get('/asset/statistics'); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should return stats of all assets', async () => { - const { status, body } = await request(server) - .get('/asset/statistics') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(body).toEqual({ images: 6, videos: 1, total: 7 }); - expect(status).toBe(200); - }); - - it('should return stats of all favored assets', async () => { - const { status, body } = await request(server) - .get('/asset/statistics') - .set('Authorization', `Bearer ${user1.accessToken}`) - .query({ isFavorite: true }); - - expect(status).toBe(200); - expect(body).toEqual({ images: 2, videos: 1, total: 3 }); - }); - - it('should return stats of all archived assets', async () => { - const { status, body } = await request(server) - .get('/asset/statistics') - .set('Authorization', `Bearer ${user1.accessToken}`) - .query({ isArchived: true }); - - expect(status).toBe(200); - expect(body).toEqual({ images: 3, videos: 0, total: 3 }); - }); - - it('should return stats of all favored and archived assets', async () => { - const { status, body } = await request(server) - .get('/asset/statistics') - .set('Authorization', `Bearer ${user1.accessToken}`) - .query({ isFavorite: true, isArchived: true }); - - expect(status).toBe(200); - expect(body).toEqual({ images: 1, videos: 0, total: 1 }); - }); - - it('should return stats of all assets neither favored nor archived', async () => { - const { status, body } = await request(server) - .get('/asset/statistics') - .set('Authorization', `Bearer ${user1.accessToken}`) - .query({ isFavorite: false, isArchived: false }); - - expect(status).toBe(200); - expect(body).toEqual({ images: 2, videos: 0, total: 2 }); - }); - }); - - describe('GET /asset/random', () => { - beforeAll(async () => { - await Promise.all([ - createAsset(user1, new Date('1970-02-01')), - createAsset(user1, new Date('1970-02-01')), - createAsset(user1, new Date('1970-02-01')), - createAsset(user1, new Date('1970-02-01')), - createAsset(user1, new Date('1970-02-01')), - createAsset(user1, new Date('1970-02-01')), - ]); - }); - it('should require authentication', async () => { - const { status, body } = await request(server).get('/asset/random'); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it.each(Array(10))('should return 1 random assets', async () => { - const { status, body } = await request(server) - .get('/asset/random') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - - const assets: AssetResponseDto[] = body; - expect(assets.length).toBe(1); - expect(assets[0].ownerId).toBe(user1.userId); - // - // assets owned by user2 - expect(assets[0].id).not.toBe(asset4.id); - // assets owned by user1 - expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id); - }); - - it.each(Array(10))('should return 2 random assets', async () => { - const { status, body } = await request(server) - .get('/asset/random?count=2') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - - const assets: AssetResponseDto[] = body; - expect(assets.length).toBe(2); - - for (const asset of assets) { - expect(asset.ownerId).toBe(user1.userId); - // assets owned by user1 - expect([asset1.id, asset2.id, asset3.id]).toContain(asset.id); - // assets owned by user2 - expect(asset.id).not.toBe(asset4.id); - } - }); - - it.each(Array(10))( - 'should return 1 asset if there are 10 assets in the database but user 2 only has 1', - async () => { - const { status, body } = await request(server) - .get('/[]asset/random') - .set('Authorization', `Bearer ${user2.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: asset4.id })]); - }, - ); - - it('should return error', async () => { - const { status } = await request(server) - .get('/asset/random?count=ABC') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(400); - }); - }); - describe('GET /asset/time-buckets', () => { it('should require authentication', async () => { const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH }); diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts index 7dd47e06c..8d2a1b79b 100644 --- a/server/e2e/client/asset-api.ts +++ b/server/e2e/client/asset-api.ts @@ -1,4 +1,4 @@ -import { AssetBulkDeleteDto, AssetResponseDto } from '@app/domain'; +import { AssetResponseDto } from '@app/domain'; import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto'; import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; import { randomBytes } from 'node:crypto'; @@ -74,8 +74,4 @@ export const assetApi = { expect(status).toBe(200); return body; }, - delete: async (server: any, accessToken: string, dto: AssetBulkDeleteDto) => { - const { status } = await request(server).delete('/asset').set('Authorization', `Bearer ${accessToken}`).send(dto); - expect(status).toBe(204); - }, }; diff --git a/server/e2e/jobs/specs/trash.e2e-spec.ts b/server/e2e/jobs/specs/trash.e2e-spec.ts deleted file mode 100644 index 5c4b3e905..000000000 --- a/server/e2e/jobs/specs/trash.e2e-spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { LoginResponseDto } from '@app/domain'; -import { api } from 'e2e/client'; -import { readFile } from 'node:fs/promises'; -import { basename, join } from 'node:path'; -import type { App } from 'supertest/types'; -import { IMMICH_TEST_ASSET_PATH, testApp } from '../../../src/test-utils/utils'; - -const assetFilePath = join(IMMICH_TEST_ASSET_PATH, 'formats/png/density_plot.png'); - -describe(`Trash (e2e)`, () => { - let server: App; - let admin: LoginResponseDto; - - beforeAll(async () => { - const app = await testApp.create(); - server = app.getHttpServer(); - }); - - beforeEach(async () => { - await testApp.reset(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - }); - - afterAll(async () => { - await testApp.teardown(); - }); - - it('should move an asset to trash', async () => { - const content = await readFile(assetFilePath); - const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', { - content, - filename: basename(assetFilePath), - }); - - const uploadedAsset = await api.assetApi.get(server, admin.accessToken, assetId); - expect(uploadedAsset.isTrashed).toBe(false); - - await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] }); - - const deletedAsset = await api.assetApi.get(server, admin.accessToken, assetId); - expect(deletedAsset.isTrashed).toBe(true); - }); - - it('should delete all trashed assets', async () => { - const content = await readFile(assetFilePath); - const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', { - content, - filename: basename(assetFilePath), - }); - - await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] }); - - const assetsBeforeEmpty = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(assetsBeforeEmpty.length).toBe(1); - - await api.trashApi.empty(server, admin.accessToken); - - const assetsAfterEmpty = await api.assetApi.getAllAssets(server, admin.accessToken); - expect(assetsAfterEmpty.length).toBe(0); - }); - - it('should restore all trashed assets', async () => { - const content = await readFile(assetFilePath); - const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', { - content, - filename: basename(assetFilePath), - }); - - await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] }); - - const deletedAsset = await api.assetApi.get(server, admin.accessToken, assetId); - expect(deletedAsset.isTrashed).toBe(true); - - await api.trashApi.restore(server, admin.accessToken); - - const restoredAsset = await api.assetApi.get(server, admin.accessToken, assetId); - expect(restoredAsset.isTrashed).toBe(false); - }); -}); diff --git a/server/package-lock.json b/server/package-lock.json index 97c9dca58..ceac96222 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -3179,9 +3179,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dependencies": { "undici-types": "~5.26.4" } @@ -3281,9 +3281,9 @@ } }, "node_modules/@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/send": { @@ -3398,16 +3398,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz", - "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", + "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/type-utils": "7.0.1", - "@typescript-eslint/utils": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/type-utils": "7.0.2", + "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -3433,15 +3433,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz", - "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", + "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4" }, "engines": { @@ -3461,13 +3461,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz", - "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", + "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1" + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3478,13 +3478,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz", - "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", + "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/utils": "7.0.1", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/utils": "7.0.2", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -3505,9 +3505,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz", - "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", + "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3518,13 +3518,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz", - "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", + "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3570,17 +3570,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz", - "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", + "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", "semver": "^7.5.4" }, "engines": { @@ -3595,12 +3595,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz", - "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", + "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.1", + "@typescript-eslint/types": "7.0.2", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -5504,9 +5504,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.4", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz", - "integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==", + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "engines": { "node": ">=12" }, @@ -8139,9 +8139,9 @@ } }, "node_modules/joi": { - "version": "17.12.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz", - "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==", + "version": "17.12.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", @@ -14730,9 +14730,9 @@ } }, "@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "requires": { "undici-types": "~5.26.4" } @@ -14819,9 +14819,9 @@ } }, "@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "@types/send": { @@ -14936,16 +14936,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz", - "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", + "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/type-utils": "7.0.1", - "@typescript-eslint/utils": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/type-utils": "7.0.2", + "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -14955,54 +14955,54 @@ } }, "@typescript-eslint/parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz", - "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", + "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz", - "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", + "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1" + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2" } }, "@typescript-eslint/type-utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz", - "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", + "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/utils": "7.0.1", + "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/utils": "7.0.2", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz", - "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", + "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz", - "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", + "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/visitor-keys": "7.0.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -15032,27 +15032,27 @@ } }, "@typescript-eslint/utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz", - "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", + "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", + "@typescript-eslint/scope-manager": "7.0.2", + "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/typescript-estree": "7.0.2", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz", - "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", + "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.1", + "@typescript-eslint/types": "7.0.2", "eslint-visitor-keys": "^3.4.1" } }, @@ -16494,9 +16494,9 @@ } }, "dotenv": { - "version": "16.4.4", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz", - "integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==" + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" }, "dotenv-expand": { "version": "10.0.0", @@ -18453,9 +18453,9 @@ } }, "joi": { - "version": "17.12.1", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz", - "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==", + "version": "17.12.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", "requires": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts index f4c9aa53e..dc5934e7b 100644 --- a/server/src/domain/media/media.service.spec.ts +++ b/server/src/domain/media/media.service.spec.ts @@ -1801,7 +1801,7 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - `-c:v hevc_rkmpp_encoder`, + `-c:v hevc_rkmpp`, '-c:a copy', '-movflags faststart', '-fps_mode passthrough', @@ -1810,17 +1810,12 @@ describe(MediaService.name, () => { '-g 256', '-tag:v hvc1', '-v verbose', + '-vf scale=-2:720,format=yuv420p', '-level 153', '-rc_mode 3', - '-quality_min 0', - '-quality_max 100', '-b:v 10000k', - '-width 1280', - '-height 720', ], twoPass: false, - ffmpegPath: 'ffmpeg_mpp', - ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp', }, ); }); @@ -1841,7 +1836,7 @@ describe(MediaService.name, () => { { inputOptions: [], outputOptions: [ - `-c:v h264_rkmpp_encoder`, + `-c:v h264_rkmpp`, '-c:a copy', '-movflags faststart', '-fps_mode passthrough', @@ -1849,16 +1844,12 @@ describe(MediaService.name, () => { '-map 0:1', '-g 256', '-v verbose', + '-vf scale=-2:720,format=yuv420p', '-level 51', '-rc_mode 2', - '-quality_min 51', - '-quality_max 51', - '-width 1280', - '-height 720', + '-qp_init 30', ], twoPass: false, - ffmpegPath: 'ffmpeg_mpp', - ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp', }, ); }); diff --git a/server/src/domain/media/media.util.ts b/server/src/domain/media/media.util.ts index c9483c373..e5890bdd0 100644 --- a/server/src/domain/media/media.util.ts +++ b/server/src/domain/media/media.util.ts @@ -607,16 +607,6 @@ export class VAAPIConfig extends BaseHWConfig { } export class RKMPPConfig extends BaseHWConfig { - getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo): TranscodeOptions { - const options = super.getOptions(target, videoStream, audioStream); - options.ffmpegPath = 'ffmpeg_mpp'; - options.ldLibraryPath = '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp'; - if ([TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target)) { - options.outputOptions.push(...this.getSizeOptions(videoStream)); - } - return options; - } - eligibleForTwoPass(): boolean { return false; } @@ -628,18 +618,6 @@ export class RKMPPConfig extends BaseHWConfig { return []; } - getFilterOptions(videoStream: VideoStreamInfo) { - return this.shouldToneMap(videoStream) ? this.getToneMapping() : []; - } - - getSizeOptions(videoStream: VideoStreamInfo) { - if (this.shouldScale(videoStream)) { - const { width, height } = this.getSize(videoStream); - return [`-width ${width}`, `-height ${height}`]; - } - return []; - } - getPresetOptions() { switch (this.config.targetVideoCodec) { case VideoCodec.H264: { @@ -659,12 +637,11 @@ export class RKMPPConfig extends BaseHWConfig { getBitrateOptions() { const bitrate = this.getMaxBitrateValue(); if (bitrate > 0) { - return ['-rc_mode 3', '-quality_min 0', '-quality_max 100', `-b:v ${bitrate}${this.getBitrateUnit()}`]; - } else { - // convert CQP from 51-10 to 0-100, values below 10 are set to 10 - const quality = Math.floor(125 - Math.max(this.config.crf, 10) * (125 / 51)); - return ['-rc_mode 2', `-quality_min ${quality}`, `-quality_max ${quality}`]; + // -b:v specifies max bitrate, average bitrate is derived automatically... + return ['-rc_mode 3', `-b:v ${bitrate}${this.getBitrateUnit()}`]; } + // use CRF value as QP value + return ['-rc_mode 2', `-qp_init ${this.config.crf}`]; } getSupportedCodecs() { @@ -672,6 +649,6 @@ export class RKMPPConfig extends BaseHWConfig { } getVideoCodec(): string { - return `${this.config.targetVideoCodec}_rkmpp_encoder`; + return `${this.config.targetVideoCodec}_rkmpp`; } } diff --git a/server/src/domain/repositories/media.repository.ts b/server/src/domain/repositories/media.repository.ts index 846b6156d..ed6f88449 100644 --- a/server/src/domain/repositories/media.repository.ts +++ b/server/src/domain/repositories/media.repository.ts @@ -51,8 +51,6 @@ export interface TranscodeOptions { inputOptions: string[]; outputOptions: string[]; twoPass: boolean; - ffmpegPath?: string; - ldLibraryPath?: string; } export interface BitrateDistribution { diff --git a/server/src/domain/repositories/search.repository.ts b/server/src/domain/repositories/search.repository.ts index 8566fcd8e..c9fec3cf7 100644 --- a/server/src/domain/repositories/search.repository.ts +++ b/server/src/domain/repositories/search.repository.ts @@ -187,4 +187,5 @@ export interface ISearchRepository { searchFaces(search: FaceEmbeddingSearch): Promise; upsert(smartInfo: Partial, embedding?: Embedding): Promise; searchPlaces(placeName: string): Promise; + deleteAllSearchEmbeddings(): Promise; } diff --git a/server/src/domain/smart-info/smart-info.service.spec.ts b/server/src/domain/smart-info/smart-info.service.spec.ts index 5da7b7824..9835ea1a5 100644 --- a/server/src/domain/smart-info/smart-info.service.spec.ts +++ b/server/src/domain/smart-info/smart-info.service.spec.ts @@ -71,6 +71,7 @@ describe(SmartInfoService.name, () => { expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]); expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH); + expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled(); }); it('should queue all the assets', async () => { @@ -83,6 +84,7 @@ describe(SmartInfoService.name, () => { expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]); expect(assetMock.getAll).toHaveBeenCalled(); + expect(searchMock.deleteAllSearchEmbeddings).toHaveBeenCalled(); }); }); diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/domain/smart-info/smart-info.service.ts index d193b29b5..19d5668cc 100644 --- a/server/src/domain/smart-info/smart-info.service.ts +++ b/server/src/domain/smart-info/smart-info.service.ts @@ -50,6 +50,10 @@ export class SmartInfoService { return true; } + if (force) { + await this.repository.deleteAllSearchEmbeddings(); + } + const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force ? this.assetRepository.getAll(pagination) diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/infra/repositories/media.repository.ts index a981bbc07..1f9395ff2 100644 --- a/server/src/infra/repositories/media.repository.ts +++ b/server/src/infra/repositories/media.repository.ts @@ -76,18 +76,7 @@ export class MediaRepository implements IMediaRepository { transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise { if (!options.twoPass) { return new Promise((resolve, reject) => { - const oldLdLibraryPath = process.env.LD_LIBRARY_PATH; - if (options.ldLibraryPath) { - // fluent ffmpeg does not allow to set environment variables, so we do it manually - process.env.LD_LIBRARY_PATH = this.chainPath(oldLdLibraryPath || '', options.ldLibraryPath); - } - try { - this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run(); - } finally { - if (options.ldLibraryPath) { - process.env.LD_LIBRARY_PATH = oldLdLibraryPath; - } - } + this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run(); }); } @@ -121,7 +110,6 @@ export class MediaRepository implements IMediaRepository { configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) { return ffmpeg(input, { niceness: 10 }) - .setFfmpegPath(options.ffmpegPath || 'ffmpeg') .inputOptions(options.inputOptions) .outputOptions(options.outputOptions) .output(output) diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index 63b3d570e..3a7ec2946 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/infra/repositories/person.repository.ts @@ -40,11 +40,11 @@ export class PersonRepository implements IPersonRepository { } async deleteAll(): Promise { - await this.personRepository.delete({}); + await this.personRepository.clear(); } async deleteAllFaces(): Promise { - await this.assetFaceRepository.delete({}); + await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE'); } getAllFaces( diff --git a/server/src/infra/repositories/search.repository.ts b/server/src/infra/repositories/search.repository.ts index 089640128..c8dc5070f 100644 --- a/server/src/infra/repositories/search.repository.ts +++ b/server/src/infra/repositories/search.repository.ts @@ -229,25 +229,17 @@ export class SearchRepository implements ISearchRepository { this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); await this.smartSearchRepository.manager.transaction(async (manager) => { - await manager.query(`DROP TABLE smart_search`); - - await manager.query(` - CREATE TABLE smart_search ( - "assetId" uuid PRIMARY KEY REFERENCES assets(id) ON DELETE CASCADE, - embedding vector(${dimSize}) NOT NULL )`); - - await manager.query(` - CREATE INDEX clip_index ON smart_search - USING vectors (embedding vector_cos_ops) WITH (options = $$ - [indexing.hnsw] - m = 16 - ef_construction = 300 - $$)`); + await manager.clear(SmartSearchEntity); + await manager.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); }); this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`); } + deleteAllSearchEmbeddings(): Promise { + return this.smartSearchRepository.clear(); + } + private async getDimSize(): Promise { const res = await this.smartSearchRepository.manager.query(` SELECT atttypmod as dimsize diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts index 06a2cb76d..5912d7745 100644 --- a/server/test/repositories/search.repository.mock.ts +++ b/server/test/repositories/search.repository.mock.ts @@ -8,5 +8,6 @@ export const newSearchRepositoryMock = (): jest.Mocked => { searchFaces: jest.fn(), upsert: jest.fn(), searchPlaces: jest.fn(), + deleteAllSearchEmbeddings: jest.fn(), }; }; diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs index ef17242c8..2b89e5dc7 100644 --- a/web/.eslintrc.cjs +++ b/web/.eslintrc.cjs @@ -13,6 +13,7 @@ module.exports = { sourceType: 'module', ecmaVersion: 2022, extraFileExtensions: ['.svelte'], + project: ['./tsconfig.json'], }, env: { browser: true, @@ -32,13 +33,6 @@ module.exports = { NodeJS: true, }, rules: { - 'unicorn/no-useless-undefined': 'off', - 'unicorn/prefer-spread': 'off', - 'unicorn/no-null': 'off', - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/no-nested-ternary': 'off', - 'unicorn/consistent-function-scoping': 'off', - 'unicorn/prefer-top-level-await': 'off', '@typescript-eslint/no-unused-vars': [ 'warn', { @@ -48,5 +42,17 @@ module.exports = { }, ], curly: 2, + 'unicorn/no-useless-undefined': 'off', + 'unicorn/prefer-spread': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/no-nested-ternary': 'off', + 'unicorn/consistent-function-scoping': 'off', + 'unicorn/prefer-top-level-await': 'off', + // TODO: set recommended-type-checked and remove these rules + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/require-await': 'error', }, }; diff --git a/web/package-lock.json b/web/package-lock.json index 78e5caf7c..77a875e51 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,58 +10,58 @@ "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", - "@mdi/js": "^7.3.67", - "@photo-sphere-viewer/core": "^5.7.0", - "@zoom-image/svelte": "^0.2.0", + "@mdi/js": "^7.4.47", + "@photo-sphere-viewer/core": "^5.7.1", + "@zoom-image/svelte": "^0.2.6", "axios": "^1.6.7", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", "dom-to-image": "^2.6.0", - "handlebars": "^4.7.7", + "handlebars": "^4.7.8", "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", - "luxon": "^3.2.1", - "socket.io-client": "^4.6.1", - "svelte-local-storage-store": "^0.6.0", - "svelte-maplibre": "^0.8.0", + "luxon": "^3.4.4", + "socket.io-client": "^4.7.4", + "svelte-local-storage-store": "^0.6.4", + "svelte-maplibre": "^0.8.1", "thumbhash": "^0.1.1" }, "devDependencies": { - "@faker-js/faker": "^8.0.0", - "@floating-ui/dom": "^1.5.1", + "@faker-js/faker": "^8.4.1", + "@floating-ui/dom": "^1.6.3", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/enhanced-img": "^0.1.8", - "@sveltejs/kit": "^2.5.1", + "@sveltejs/kit": "^2.5.2", "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@testing-library/jest-dom": "^6.1.5", - "@testing-library/svelte": "^4.0.3", - "@types/dom-to-image": "^2.6.4", - "@types/justified-layout": "^4.1.0", - "@types/lodash-es": "^4.17.6", - "@types/luxon": "^3.2.0", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.0.4", - "autoprefixer": "^10.4.13", - "eslint": "^8.34.0", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/svelte": "^4.1.0", + "@types/dom-to-image": "^2.6.7", + "@types/justified-layout": "^4.1.4", + "@types/lodash-es": "^4.17.12", + "@types/luxon": "^3.4.2", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "@vitest/coverage-v8": "^1.3.1", + "autoprefixer": "^10.4.17", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.30.0", - "eslint-plugin-unicorn": "^51.0.0", - "factory.ts": "^1.3.0", + "eslint-plugin-svelte": "^2.35.1", + "eslint-plugin-unicorn": "^51.0.1", + "factory.ts": "^1.4.1", "identity-obj-proxy": "^3.0.0", - "postcss": "^8.4.21", - "prettier": "^3.1.0", + "postcss": "^8.4.35", + "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", - "prettier-plugin-svelte": "^3.1.2", + "prettier-plugin-svelte": "^3.2.1", "rollup-plugin-visualizer": "^5.12.0", - "svelte": "^4.2.11", - "svelte-check": "^3.6.4", - "tailwindcss": "^3.2.7", - "tslib": "^2.5.0", + "svelte": "^4.2.12", + "svelte-check": "^3.6.5", + "tailwindcss": "^3.4.1", + "tslib": "^2.6.2", "typescript": "^5.3.3", - "vite": "^5.1.1", - "vitest": "^1.0.4" + "vite": "^5.1.4", + "vitest": "^1.3.1" } }, "../open-api/typescript-sdk": { @@ -898,9 +898,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -948,13 +948,13 @@ "dev": true }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -975,9 +975,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@img/sharp-darwin-arm64": { @@ -1859,9 +1859,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.1.tgz", - "integrity": "sha512-TKj08o3mJCoQNLTdRdGkHPePTCPUGTgkew65RDqjVU3MtPVxljsofXQYfXndHfq0P7KoPRO/0/reF6HesU0Djw==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.2.tgz", + "integrity": "sha512-1Pm2lsBYURQsjnLyZa+jw75eVD4gYHxGRwPyFe4DAmB3FjTVR8vRNWGeuDLGFcKMh/B1ij6FTUrc9GrerogCng==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2284,9 +2284,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@types/supercluster": { @@ -2298,16 +2298,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz", - "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", + "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/type-utils": "7.0.1", - "@typescript-eslint/utils": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/type-utils": "7.1.0", + "@typescript-eslint/utils": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2366,15 +2366,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz", - "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz", + "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4" }, "engines": { @@ -2394,13 +2394,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz", - "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz", + "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1" + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2411,13 +2411,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz", - "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz", + "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.0.1", - "@typescript-eslint/utils": "7.0.1", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/utils": "7.1.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -2438,9 +2438,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz", - "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz", + "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -2451,13 +2451,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz", - "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz", + "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/visitor-keys": "7.0.1", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2536,17 +2536,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz", - "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.1", - "@typescript-eslint/types": "7.0.1", - "@typescript-eslint/typescript-estree": "7.0.1", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", "semver": "^7.5.4" }, "engines": { @@ -2594,12 +2594,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz", - "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz", + "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.1", + "@typescript-eslint/types": "7.1.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2617,24 +2617,23 @@ "dev": true }, "node_modules/@vitest/browser": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.0.4.tgz", - "integrity": "sha512-qMT1NhClex73eA2sOwnlwLcSIVCW8B7NFVzIKuXLKxSJD3LsNq8PCKhwOkBxklbSAcZdkOgL/d3/gzQT7k9eng==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.3.1.tgz", + "integrity": "sha512-pRof8G8nqRWwg3ouyIctyhfIVk5jXgF056uF//sqdi37+pVtDz9kBI/RMu0xlc8tgCyJ2aEMfbgJZPUydlEVaQ==", "dev": true, "optional": true, "peer": true, "dependencies": { - "estree-walker": "^3.0.3", + "@vitest/utils": "1.3.1", "magic-string": "^0.30.5", - "sirv": "^2.0.3" + "sirv": "^2.0.4" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "playwright": "*", - "safaridriver": "*", - "vitest": "^1.0.0", + "vitest": "1.3.1", "webdriverio": "*" }, "peerDependenciesMeta": { @@ -2650,9 +2649,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.2.tgz", - "integrity": "sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", + "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.1", @@ -2673,17 +2672,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "^1.0.0" + "vitest": "1.3.1" } }, "node_modules/@vitest/expect": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz", - "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", "dev": true, "dependencies": { - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "chai": "^4.3.10" }, "funding": { @@ -2691,12 +2690,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz", - "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", "dev": true, "dependencies": { - "@vitest/utils": "1.2.2", + "@vitest/utils": "1.3.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -2732,9 +2731,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz", - "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", "dev": true, "dependencies": { "magic-string": "^0.30.5", @@ -2778,9 +2777,9 @@ "dev": true }, "node_modules/@vitest/spy": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz", - "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", "dev": true, "dependencies": { "tinyspy": "^2.2.0" @@ -2790,9 +2789,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz", - "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", "dev": true, "dependencies": { "diff-sequences": "^29.6.3", @@ -2837,9 +2836,9 @@ "dev": true }, "node_modules/@zoom-image/core": { - "version": "0.32.1", - "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.32.1.tgz", - "integrity": "sha512-R56D749Ck+/1yLWlEJ2FctxjdpTQEje3jPhOAbeEZGzLndIumskO42UqRNixcER6sAzCi01oYopmqnCpDElF0g==", + "version": "0.33.0", + "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.33.0.tgz", + "integrity": "sha512-wkMV8+aE7PeknLFhpIb/6vwRl09Z2gWM4UqKdnXO6Mb0pP9BiuDLcLvGGGB4o++uAPINgDwmNn+Loo641XSjDA==", "dependencies": { "@namnode/store": "^0.1.0" }, @@ -2849,11 +2848,11 @@ } }, "node_modules/@zoom-image/svelte": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.4.tgz", - "integrity": "sha512-rgfgn7Q60VrwmE4MPBzDWaFplc+411Lxg1nMdAnq/UTv4HTWSpiwm1IOg8gQZjRp92a8RXcRmUYXU+wFKEMjSg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.6.tgz", + "integrity": "sha512-dEpA/egmTjVcptwhtcKHvkhVMTzQCpH17erfcXuJByt+nn5Oo4LnZOxE8gwSVEdPp65Ns6Y/byYD0GSQ/vv+DQ==", "dependencies": { - "@zoom-image/core": "0.32.1" + "@zoom-image/core": "0.33.0" }, "funding": { "type": "github", @@ -4055,16 +4054,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -7991,17 +7990,23 @@ } }, "node_modules/strip-literal": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz", - "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", + "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", "dev": true, "dependencies": { - "acorn": "^8.10.0" + "js-tokens": "^8.0.2" }, "funding": { "url": "https://github.com/sponsors/antfu" } }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", + "dev": true + }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", @@ -8077,9 +8082,9 @@ } }, "node_modules/svelte": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.11.tgz", - "integrity": "sha512-YIQk3J4X89wOLhjsqIW8tqY3JHPuBdtdOIkASP2PZeAMcSW9RsIjQzMesCrxOF3gdWYC0mKknlKF7OqmLM+Zqg==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz", + "integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -8101,9 +8106,9 @@ } }, "node_modules/svelte-check": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz", - "integrity": "sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==", + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.5.tgz", + "integrity": "sha512-5aLgoQEdadvp8ypvKQ2avhnQ+V9YPQQaWrTFlXFw5g/v8xIQBvo+X/WqxTyD+V/ItDqXg3+abUA53rdDHgUjCA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -8173,9 +8178,9 @@ } }, "node_modules/svelte-maplibre": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.0.tgz", - "integrity": "sha512-sRSj/zQa7LTfHNIzKcYe+sa9qHClt/OAXcdPQ0w3ksLbCMmVHGk4B2yIXHCVk0g4sc18M85N8KGsHVtZoNC+Mw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.1.tgz", + "integrity": "sha512-CTm/s0+mJzBHSoO5zPKBo3ORmUyiWS3Ex4xvVdNgVg+sDesHasEAJ0N1/NUrd56S33zgRdFZGzRnRguCnKFAzw==", "dependencies": { "d3-geo": "^3.1.0", "just-compare": "^2.3.0", @@ -8456,9 +8461,9 @@ "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" }, "node_modules/tinyspy": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz", - "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true, "engines": { "node": ">=14.0.0" @@ -8733,9 +8738,9 @@ } }, "node_modules/vite": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", - "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", + "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", "dev": true, "dependencies": { "esbuild": "^0.19.3", @@ -8801,9 +8806,9 @@ } }, "node_modules/vite-node": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz", - "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", "dev": true, "dependencies": { "cac": "^6.7.14", @@ -8837,18 +8842,17 @@ } }, "node_modules/vitest": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz", - "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", "dev": true, "dependencies": { - "@vitest/expect": "1.2.2", - "@vitest/runner": "1.2.2", - "@vitest/snapshot": "1.2.2", - "@vitest/spy": "1.2.2", - "@vitest/utils": "1.2.2", + "@vitest/expect": "1.3.1", + "@vitest/runner": "1.3.1", + "@vitest/snapshot": "1.3.1", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "acorn-walk": "^8.3.2", - "cac": "^6.7.14", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", @@ -8857,11 +8861,11 @@ "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", - "strip-literal": "^1.3.0", + "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.2.2", + "vite-node": "1.3.1", "why-is-node-running": "^2.2.2" }, "bin": { @@ -8876,8 +8880,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "^1.0.0", - "@vitest/ui": "^1.0.0", + "@vitest/browser": "1.3.1", + "@vitest/ui": "1.3.1", "happy-dom": "*", "jsdom": "*" }, diff --git a/web/package.json b/web/package.json index 2b53d0645..51f07dded 100644 --- a/web/package.json +++ b/web/package.json @@ -22,59 +22,59 @@ "prepare": "svelte-kit sync" }, "devDependencies": { - "@faker-js/faker": "^8.0.0", - "@floating-ui/dom": "^1.5.1", + "@faker-js/faker": "^8.4.1", + "@floating-ui/dom": "^1.6.3", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.1", "@sveltejs/enhanced-img": "^0.1.8", - "@sveltejs/kit": "^2.5.1", + "@sveltejs/kit": "^2.5.2", "@sveltejs/vite-plugin-svelte": "^3.0.2", - "@testing-library/jest-dom": "^6.1.5", - "@testing-library/svelte": "^4.0.3", - "@types/dom-to-image": "^2.6.4", - "@types/justified-layout": "^4.1.0", - "@types/lodash-es": "^4.17.6", - "@types/luxon": "^3.2.0", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.0.4", - "autoprefixer": "^10.4.13", - "eslint": "^8.34.0", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/svelte": "^4.1.0", + "@types/dom-to-image": "^2.6.7", + "@types/justified-layout": "^4.1.4", + "@types/lodash-es": "^4.17.12", + "@types/luxon": "^3.4.2", + "@typescript-eslint/eslint-plugin": "^7.1.0", + "@typescript-eslint/parser": "^7.1.0", + "@vitest/coverage-v8": "^1.3.1", + "autoprefixer": "^10.4.17", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.30.0", - "eslint-plugin-unicorn": "^51.0.0", - "factory.ts": "^1.3.0", + "eslint-plugin-svelte": "^2.35.1", + "eslint-plugin-unicorn": "^51.0.1", + "factory.ts": "^1.4.1", "identity-obj-proxy": "^3.0.0", - "postcss": "^8.4.21", - "prettier": "^3.1.0", + "postcss": "^8.4.35", + "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", - "prettier-plugin-svelte": "^3.1.2", + "prettier-plugin-svelte": "^3.2.1", "rollup-plugin-visualizer": "^5.12.0", - "svelte": "^4.2.11", - "svelte-check": "^3.6.4", - "tailwindcss": "^3.2.7", - "tslib": "^2.5.0", + "svelte": "^4.2.12", + "svelte-check": "^3.6.5", + "tailwindcss": "^3.4.1", + "tslib": "^2.6.2", "typescript": "^5.3.3", - "vite": "^5.1.1", - "vitest": "^1.0.4" + "vite": "^5.1.4", + "vitest": "^1.3.1" }, "type": "module", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", - "@mdi/js": "^7.3.67", - "@photo-sphere-viewer/core": "^5.7.0", - "@zoom-image/svelte": "^0.2.0", + "@mdi/js": "^7.4.47", + "@photo-sphere-viewer/core": "^5.7.1", + "@zoom-image/svelte": "^0.2.6", "axios": "^1.6.7", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", "dom-to-image": "^2.6.0", - "handlebars": "^4.7.7", + "handlebars": "^4.7.8", "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", - "luxon": "^3.2.1", - "socket.io-client": "^4.6.1", - "svelte-local-storage-store": "^0.6.0", - "svelte-maplibre": "^0.8.0", + "luxon": "^3.4.4", + "socket.io-client": "^4.7.4", + "svelte-local-storage-store": "^0.6.4", + "svelte-maplibre": "^0.8.1", "thumbhash": "^0.1.1" } } diff --git a/web/src/hooks.client.ts b/web/src/hooks.client.ts index 802e9a712..f30e3ee8c 100644 --- a/web/src/hooks.client.ts +++ b/web/src/hooks.client.ts @@ -1,7 +1,6 @@ import { isHttpError } from '@immich/sdk'; import type { HandleClientError } from '@sveltejs/kit'; -const LOG_PREFIX = '[hooks.client.ts]'; const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?'; const parseError = (error: unknown) => { @@ -23,6 +22,6 @@ const parseError = (error: unknown) => { export const handleError: HandleClientError = ({ error }) => { const result = parseError(error); - console.error(`${LOG_PREFIX}:handleError ${result.message}`); + console.error(`[hooks.client.ts]:handleError ${result.message}`); return result; }; diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index 60756166f..edf5c4830 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -48,11 +48,11 @@ await handleCommand(jobId, dto); }; - const onConfirm = () => { + const onConfirm = async () => { if (!confirmJob) { return; } - handleCommand(confirmJob, { command: JobCommand.Start, force: true }); + await handleCommand(confirmJob, { command: JobCommand.Start, force: true }); confirmJob = null; }; diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte index 1ad962b0d..16b2afc7f 100644 --- a/web/src/lib/components/admin-page/settings/admin-settings.svelte +++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte @@ -54,7 +54,7 @@ }); }; - const resetToDefault = async (configKeys: Array) => { + const resetToDefault = (configKeys: Array) => { for (const key of configKeys) { config = { ...config, [key]: defaultConfig[key] }; } diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index e9983cc7e..d4f60ab60 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -21,6 +21,7 @@ import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js'; import UpdatePanel from '../shared-components/update-panel.svelte'; + import { handlePromiseError } from '$lib/utils'; export let sharedLink: SharedLinkResponseDto; export let user: UserResponseDto | undefined = undefined; @@ -35,7 +36,7 @@ dragAndDropFilesStore.subscribe((value) => { if (value.isDragging && value.files.length > 0) { - fileUploadHandler(value.files, album.id); + handlePromiseError(fileUploadHandler(value.files, album.id)); dragAndDropFilesStore.set({ isDragging: false, files: [] }); } }); @@ -67,7 +68,7 @@ const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); - onMount(async () => { + onMount(() => { document.addEventListener('keydown', onKeyboardPress); }); diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index dff2d7f31..4dd1b75e7 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -1,7 +1,7 @@ diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 38f65e3df..7f94857af 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -7,7 +7,7 @@ import { featureFlags } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { websocketEvents } from '$lib/stores/websocket'; - import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink } from '$lib/utils'; + import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink, handlePromiseError } from '$lib/utils'; import { delay, getAssetFilename } from '$lib/utils/asset-utils'; import { autoGrowHeight } from '$lib/utils/autogrow'; import { clickOutside } from '$lib/utils/click-outside'; @@ -78,7 +78,7 @@ originalDescription = description; }; - $: handleNewAsset(asset); + $: handlePromiseError(handleNewAsset(asset)); $: latlng = (() => { const lat = asset.exifInfo?.latitude; @@ -113,7 +113,7 @@ switch (event.key) { case 'Enter': { if (ctrl && event.target === textArea) { - handleFocusOut(); + await handleFocusOut(); } } } diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index e10d5573c..79e827615 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -4,7 +4,7 @@ import { boundingBoxesArray } from '$lib/stores/people.store'; import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store'; import { photoZoomState } from '$lib/stores/zoom-image.store'; - import { getKey } from '$lib/utils'; + import { getKey, handlePromiseError } from '$lib/utils'; import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { getBoundingBox } from '$lib/utils/people-utils'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; @@ -102,7 +102,7 @@ } }; - const doZoomImage = async () => { + const doZoomImage = () => { setZoomImageWheelState({ currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1, }); @@ -120,7 +120,7 @@ if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed && !$alwaysLoadOriginalFile) { hasZoomed = true; - loadAssetData({ loadOriginal: true }); + handlePromiseError(loadAssetData({ loadOriginal: true })); } }); diff --git a/web/src/lib/components/asset-viewer/video-viewer.svelte b/web/src/lib/components/asset-viewer/video-viewer.svelte index 96713d621..f5ff5f0fc 100644 --- a/web/src/lib/components/asset-viewer/video-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-viewer.svelte @@ -20,7 +20,7 @@ video.muted = false; dispatch('onVideoStarted'); } catch (error) { - await handleError(error, 'Unable to play video'); + handleError(error, 'Unable to play video'); } finally { isVideoLoading = false; } diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index b89f90668..7f48a9eae 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -49,7 +49,7 @@ if (assetType === AssetTypeEnum.Image) { image = $photoViewer; } else if (assetType === AssetTypeEnum.Video) { - const data = await getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp); + const data = getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp); const img: HTMLImageElement = new Image(); img.src = data; diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index 434bfb88a..7b075405c 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -43,10 +43,10 @@ dispatch('back'); }; - const handleSwapPeople = () => { + const handleSwapPeople = async () => { [person, selectedPeople[0]] = [selectedPeople[0], person]; $page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE); - goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`); + await goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`); }; const onSelect = (selected: PersonResponseDto) => { diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index 75c87a8eb..b1bd8129e 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -3,7 +3,7 @@ import { timeBeforeShowLoadingSpinner } from '$lib/constants'; import { boundingBoxesArray } from '$lib/stores/people.store'; import { websocketEvents } from '$lib/stores/websocket'; - import { getPeopleThumbnailUrl } from '$lib/utils'; + import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { getPersonNameWithHiddenValue } from '$lib/utils/person'; import { @@ -46,8 +46,8 @@ let allPeople: PersonResponseDto[] = []; // timers - let loaderLoadingDoneTimeout: NodeJS.Timeout; - let automaticRefreshTimeout: NodeJS.Timeout; + let loaderLoadingDoneTimeout: ReturnType; + let automaticRefreshTimeout: ReturnType; const thumbnailWidth = '90px'; @@ -85,7 +85,7 @@ }; onMount(() => { - loadPeople(); + handlePromiseError(loadPeople()); return websocketEvents.on('on_person_thumbnail', onPersonThumbnail); }); @@ -170,7 +170,7 @@ } }; - const handlePersonPicker = async (person: PersonResponseDto | null) => { + const handlePersonPicker = (person: PersonResponseDto | null) => { if (person) { editedPerson = person; showSeletecFaces = true; diff --git a/web/src/lib/components/faces-page/unmerge-face-selector.svelte b/web/src/lib/components/faces-page/unmerge-face-selector.svelte index 3c09b208d..254594988 100644 --- a/web/src/lib/components/faces-page/unmerge-face-selector.svelte +++ b/web/src/lib/components/faces-page/unmerge-face-selector.svelte @@ -132,9 +132,7 @@ title={'Assign selected assets to a new person'} size={'sm'} disabled={disableButtons || hasSelection} - on:click={() => { - handleCreate(); - }} + on:click={handleCreate} > {#if !showLoadingSpinnerCreate} @@ -147,9 +145,7 @@ size={'sm'} title={'Assign selected assets to an existing person'} disabled={disableButtons || !hasSelection} - on:click={() => { - handleReassign(); - }} + on:click={handleReassign} > {#if !showLoadingSpinnerReassign}

diff --git a/web/src/lib/components/forms/library-scan-settings-form.svelte b/web/src/lib/components/forms/library-scan-settings-form.svelte index f110a5a09..4d6c9c0dd 100644 --- a/web/src/lib/components/forms/library-scan-settings-form.svelte +++ b/web/src/lib/components/forms/library-scan-settings-form.svelte @@ -37,7 +37,7 @@ dispatch('submit', { library, type: LibraryType.External }); }; - const handleAddExclusionPattern = async () => { + const handleAddExclusionPattern = () => { if (!addExclusionPattern) { return; } @@ -60,7 +60,7 @@ } }; - const handleEditExclusionPattern = async () => { + const handleEditExclusionPattern = () => { if (editExclusionPattern === null) { return; } @@ -79,7 +79,7 @@ } }; - const handleDeleteExclusionPattern = async () => { + const handleDeleteExclusionPattern = () => { if (editExclusionPattern === null) { return; } diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte index 9ea654a80..202c6b60c 100644 --- a/web/src/lib/components/forms/login-form.svelte +++ b/web/src/lib/components/forms/login-form.svelte @@ -47,7 +47,7 @@ return; } } catch (error) { - await handleError(error, 'Unable to connect!'); + handleError(error, 'Unable to connect!'); } oauthLoading = false; diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index c07672fd1..30617f0aa 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -8,7 +8,7 @@ import { AppRoute, QueryParameter } from '$lib/constants'; import type { Viewport } from '$lib/stores/assets.store'; import { memoryStore } from '$lib/stores/memory.store'; - import { getAssetThumbnailUrl } from '$lib/utils'; + import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; import { fromLocalDateTime } from '$lib/utils/timeline-util'; import { ThumbnailFormat, getMemoryLane } from '@immich/sdk'; import { mdiChevronDown, mdiChevronLeft, mdiChevronRight, mdiChevronUp, mdiPause, mdiPlay } from '@mdi/js'; @@ -59,30 +59,30 @@ let paused = false; // Play or pause progress when the paused state changes. - $: paused ? pause() : play(); + $: paused ? handlePromiseError(pause()) : handlePromiseError(play()); // Progress should be paused when it's no longer possible to advance. $: paused ||= !canGoForward || galleryInView; // Advance to the next asset or memory when progress is complete. - $: $progress === 1 && toNext(); + $: $progress === 1 && handlePromiseError(toNext()); // Progress should be resumed when reset and not paused. - $: !$progress && !paused && play(); + $: !$progress && !paused && handlePromiseError(play()); // Progress should be reset when the current memory or asset changes. - $: memoryIndex, assetIndex, reset(); + $: memoryIndex, assetIndex, handlePromiseError(reset()); - const handleKeyDown = (e: KeyboardEvent) => { + const handleKeyDown = async (e: KeyboardEvent) => { if (e.key === 'ArrowRight' && canGoForward) { e.preventDefault(); - toNext(); + await toNext(); } else if (e.key === 'ArrowLeft' && canGoBack) { e.preventDefault(); - toPrevious(); + await toPrevious(); } else if (e.key === 'Escape') { e.preventDefault(); - goto(AppRoute.PHOTOS); + await goto(AppRoute.PHOTOS); } }; diff --git a/web/src/lib/components/photos-page/actions/add-to-album.svelte b/web/src/lib/components/photos-page/actions/add-to-album.svelte index c7101922f..cb059947b 100644 --- a/web/src/lib/components/photos-page/actions/add-to-album.svelte +++ b/web/src/lib/components/photos-page/actions/add-to-album.svelte @@ -27,18 +27,22 @@ showAlbumPicker = false; const assetIds = [...getAssets()].map((asset) => asset.id); - createAlbum({ createAlbumDto: { albumName, assetIds } }).then((response) => { - const { id, albumName } = response; + createAlbum({ createAlbumDto: { albumName, assetIds } }) + .then(async (response) => { + const { id, albumName } = response; - notificationController.show({ - message: `Added ${assetIds.length} to ${albumName}`, - type: NotificationType.Info, + notificationController.show({ + message: `Added ${assetIds.length} to ${albumName}`, + type: NotificationType.Info, + }); + + clearSelect(); + + await goto(`${AppRoute.ALBUMS}/${id}`); + }) + .catch((error) => { + console.error(`[add-to-album.svelte]:handleAddToNewAlbum ${error}`, error); }); - - clearSelect(); - - goto(`${AppRoute.ALBUMS}/${id}`); - }); }; const handleAddToAlbum = async (album: AlbumResponseDto) => { diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index d8372525b..0c40c8710 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -80,13 +80,17 @@ }); } - const assetClickHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => { + const assetClickHandler = async ( + asset: AssetResponseDto, + assetsInDateGroup: AssetResponseDto[], + groupTitle: string, + ) => { if (isSelectionMode || $isMultiSelectState) { assetSelectHandler(asset, assetsInDateGroup, groupTitle); return; } - assetViewingStore.setAssetId(asset.id); + await assetViewingStore.setAssetId(asset.id); }; const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets }); diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index bfb0edd7e..a512adaad 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -21,6 +21,7 @@ import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; import AssetDateGroup from './asset-date-group.svelte'; import DeleteAssetDialog from './delete-asset-dialog.svelte'; + import { handlePromiseError } from '$lib/utils'; export let isSelectionMode = false; export let singleSelect = false; @@ -47,19 +48,19 @@ $: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0; $: idsSelectedAssets = [...$selectedAssets].filter((a) => !a.isExternal).map((a) => a.id); - const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>(); + const onKeydown = (event: KeyboardEvent) => handlePromiseError(handleKeyboardPress(event)); onMount(async () => { showSkeleton = false; - document.addEventListener('keydown', onKeyboardPress); + document.addEventListener('keydown', onKeydown); assetStore.connect(); await assetStore.init(viewport); }); onDestroy(() => { if (browser) { - document.removeEventListener('keydown', onKeyboardPress); + document.removeEventListener('keydown', onKeydown); } if ($showAssetViewer) { @@ -69,13 +70,13 @@ assetStore.disconnect(); }); - const trashOrDelete = (force: boolean = false) => { + const trashOrDelete = async (force: boolean = false) => { isShowDeleteConfirmation = false; - deleteAssets(!(isTrashEnabled && !force), (assetId) => assetStore.removeAsset(assetId), idsSelectedAssets); + await deleteAssets(!(isTrashEnabled && !force), (assetId) => assetStore.removeAsset(assetId), idsSelectedAssets); assetInteractionStore.clearMultiselect(); }; - const handleKeyboardPress = (event: KeyboardEvent) => { + const handleKeyboardPress = async (event: KeyboardEvent) => { if ($isSearchEnabled || shouldIgnoreShortcut(event)) { return; } @@ -98,7 +99,7 @@ } case '/': { event.preventDefault(); - goto(AppRoute.EXPLORE); + await goto(AppRoute.EXPLORE); return; } case 'Delete': { @@ -112,7 +113,7 @@ force = true; } - trashOrDelete(force); + await trashOrDelete(force); } return; } @@ -126,12 +127,12 @@ } }; - function intersectedHandler(event: CustomEvent) { + async function intersectedHandler(event: CustomEvent) { const element_ = event.detail.container as HTMLElement; const target = element_.firstChild as HTMLElement; if (target) { const bucketDate = target.id.split('_')[1]; - assetStore.loadBucket(bucketDate, event.detail.position); + await assetStore.loadBucket(bucketDate, event.detail.position); } } @@ -142,7 +143,7 @@ const handlePrevious = async () => { const previousAsset = await assetStore.getPreviousAssetId($viewingAsset.id); if (previousAsset) { - assetViewingStore.setAssetId(previousAsset); + await assetViewingStore.setAssetId(previousAsset); } return !!previousAsset; @@ -151,7 +152,7 @@ const handleNext = async () => { const nextAsset = await assetStore.getNextAssetId($viewingAsset.id); if (nextAsset) { - assetViewingStore.setAssetId(nextAsset); + await assetViewingStore.setAssetId(nextAsset); } return !!nextAsset; @@ -369,7 +370,7 @@ (isShowDeleteConfirmation = false)} - on:confirm={() => trashOrDelete(true)} + on:confirm={() => handlePromiseError(trashOrDelete(true))} /> {/if} diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index 003633137..b587f1e13 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -2,7 +2,7 @@ import { goto } from '$app/navigation'; import { AppRoute } from '$lib/constants'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; - import { getKey } from '$lib/utils'; + import { getKey, handlePromiseError } from '$lib/utils'; import { downloadArchive } from '$lib/utils/asset-utils'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import { handleError } from '$lib/utils/handle-error'; @@ -29,7 +29,7 @@ dragAndDropFilesStore.subscribe((value) => { if (value.isDragging && value.files.length > 0) { - handleUploadAssets(value.files); + handlePromiseError(handleUploadAssets(value.files)); dragAndDropFilesStore.set({ isDragging: false, files: [] }); } }); @@ -59,7 +59,7 @@ type: NotificationType.Info, }); } catch (error) { - await handleError(error, 'Unable to add assets to shared link'); + handleError(error, 'Unable to add assets to shared link'); } }; diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index 682042604..063d5971c 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -2,7 +2,7 @@ import Icon from '$lib/components/elements/icon.svelte'; import { Theme } from '$lib/constants'; import { colorTheme, mapSettings } from '$lib/stores/preferences.store'; - import { getAssetThumbnailUrl } from '$lib/utils'; + import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; import { getMapStyle, MapTheme, type MapMarkerResponseDto } from '@immich/sdk'; import { mdiCog, mdiMapMarker } from '@mdi/js'; import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson'; @@ -152,9 +152,7 @@ applyToClusters asButton let:feature - on:click={(event) => { - handleClusterClick(event.detail.feature.properties.cluster_id, map); - }} + on:click={(event) => handlePromiseError(handleClusterClick(event.detail.feature.properties.cluster_id, map))} >
{ - progress.set(90); + onMount(async () => { + await progress.set(90); }); diff --git a/web/src/lib/components/shared-components/notification/notification-card.svelte b/web/src/lib/components/shared-components/notification/notification-card.svelte index a031b7681..ef855c90a 100644 --- a/web/src/lib/components/shared-components/notification/notification-card.svelte +++ b/web/src/lib/components/shared-components/notification/notification-card.svelte @@ -59,7 +59,7 @@ } }; - let removeNotificationTimeout: NodeJS.Timeout | undefined; + let removeNotificationTimeout: ReturnType | undefined; onMount(() => { removeNotificationTimeout = setTimeout(discard, notificationInfo.timeout); diff --git a/web/src/lib/components/shared-components/portal/portal.svelte b/web/src/lib/components/shared-components/portal/portal.svelte index c3dee4212..924e5f0c6 100644 --- a/web/src/lib/components/shared-components/portal/portal.svelte +++ b/web/src/lib/components/shared-components/portal/portal.svelte @@ -1,4 +1,5 @@ @@ -141,7 +142,7 @@ clearSearchTerm(searchTerm)} - on:selectSearchTerm={({ detail: searchTerm }) => onHistoryTermClick(searchTerm)} + on:selectSearchTerm={({ detail: searchTerm }) => handlePromiseError(onHistoryTermClick(searchTerm))} /> {/if} diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index adc824aec..6f377faff 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -8,6 +8,7 @@ diff --git a/web/src/lib/components/shared-components/upload-asset-preview.svelte b/web/src/lib/components/shared-components/upload-asset-preview.svelte index 7d5a634fb..4aa6f18ec 100644 --- a/web/src/lib/components/shared-components/upload-asset-preview.svelte +++ b/web/src/lib/components/shared-components/upload-asset-preview.svelte @@ -13,9 +13,9 @@ export let uploadAsset: UploadAsset; - const handleRetry = (uploadAsset: UploadAsset) => { + const handleRetry = async (uploadAsset: UploadAsset) => { uploadAssetsStore.removeUploadAsset(uploadAsset.id); - fileUploadHandler([uploadAsset.file], uploadAsset.albumId); + await fileUploadHandler([uploadAsset.file], uploadAsset.albumId); }; diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte index 51f1d440d..f2d90b6c6 100644 --- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte +++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte @@ -1,6 +1,5 @@ diff --git a/web/src/routes/(user)/memory/photos/[assetId]/+page.ts b/web/src/routes/(user)/memory/photos/[assetId]/+page.ts index 428a3caae..f90b10991 100644 --- a/web/src/routes/(user)/memory/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/memory/photos/[assetId]/+page.ts @@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load = (async () => { +export const load = (() => { redirect(302, AppRoute.PHOTOS); }) satisfies PageLoad; diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 1372d254b..6373b81fd 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -81,12 +81,12 @@ const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); - onMount(() => { + onMount(async () => { document.addEventListener('keydown', onKeyboardPress); const getSearchedPeople = $page.url.searchParams.get(QueryParameter.SEARCHED_PEOPLE); if (getSearchedPeople) { searchName = getSearchedPeople; - handleSearchPeople(true); + await handleSearchPeople(true); } }); @@ -108,10 +108,10 @@ } }; - const handleSearch = (force: boolean) => { + const handleSearch = async (force: boolean) => { $page.url.searchParams.set(QueryParameter.SEARCHED_PEOPLE, searchName); - goto($page.url); - handleSearchPeople(force); + await goto($page.url); + await handleSearchPeople(force); }; const handleCloseClick = () => { @@ -293,8 +293,8 @@ } }; - const handleMergePeople = (detail: PersonResponseDto) => { - goto( + const handleMergePeople = async (detail: PersonResponseDto) => { + await goto( `${AppRoute.PEOPLE}/${detail.id}?${QueryParameter.ACTION}=${ActionQueryParameterValue.MERGE}&${QueryParameter.PREVIOUS_ROUTE}=${AppRoute.PEOPLE}`, ); }; @@ -303,7 +303,7 @@ if (searchName === '') { if ($page.url.searchParams.has(QueryParameter.SEARCHED_PEOPLE)) { $page.url.searchParams.delete(QueryParameter.SEARCHED_PEOPLE); - goto($page.url); + await goto($page.url); } return; } @@ -331,7 +331,7 @@ return; } if (personName === '') { - changeName(); + await changeName(); return; } const data = await searchPerson({ name: personName, withHidden: true }); @@ -359,7 +359,7 @@ .slice(0, 3); return; } - changeName(); + await changeName(); }; const submitBirthDateChange = async (value: string) => { diff --git a/web/src/routes/(user)/people/[personId]/+page.svelte b/web/src/routes/(user)/people/[personId]/+page.svelte index f572ff2cb..dd4f2d7bd 100644 --- a/web/src/routes/(user)/people/[personId]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/+page.svelte @@ -185,7 +185,7 @@ } }; - const handleEscape = () => { + const handleEscape = async () => { if ($showAssetViewer || viewMode === ViewMode.SUGGEST_MERGE) { return; } @@ -193,7 +193,7 @@ assetInteractionStore.clearMultiselect(); return; } else { - goto(previousRoute); + await goto(previousRoute); return; } }; @@ -235,7 +235,7 @@ type: NotificationType.Info, }); - goto(previousRoute, { replaceState: true }); + await goto(previousRoute, { replaceState: true }); } catch (error) { handleError(error, 'Unable to hide person'); } @@ -244,7 +244,7 @@ const handleMerge = async (person: PersonResponseDto) => { const { assets } = await getPersonStatistics({ id: person.id }); numberOfAssets = assets; - handleGoBack(); + await handleGoBack(); data.person = person; @@ -292,7 +292,7 @@ refreshAssetGrid = !refreshAssetGrid; return; } - goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true }); + await goto(`${AppRoute.PEOPLE}/${personToBeMergedIn.id}`, { replaceState: true }); } catch (error) { handleError(error, 'Unable to save name'); } @@ -341,7 +341,7 @@ return; } if (name === '') { - changeName(); + await changeName(); return; } @@ -366,7 +366,7 @@ viewMode = ViewMode.SUGGEST_MERGE; return; } - changeName(); + await changeName(); }; const handleSetBirthDate = async (birthDate: string) => { @@ -392,11 +392,11 @@ } }; - const handleGoBack = () => { + const handleGoBack = async () => { viewMode = ViewMode.VIEW_ASSETS; if ($page.url.searchParams.has(QueryParameter.ACTION)) { $page.url.searchParams.delete(QueryParameter.ACTION); - goto($page.url); + await goto($page.url); } }; diff --git a/web/src/routes/(user)/people/[personId]/photos/[assetId]/+page.ts b/web/src/routes/(user)/people/[personId]/photos/[assetId]/+page.ts index 5ac7adf5c..bb2e587da 100644 --- a/web/src/routes/(user)/people/[personId]/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/people/[personId]/photos/[assetId]/+page.ts @@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load = (async ({ params }) => { +export const load = (({ params }) => { redirect(302, `${AppRoute.PEOPLE}/${params.personId}`); }) satisfies PageLoad; diff --git a/web/src/routes/(user)/photos/[assetId]/+page.ts b/web/src/routes/(user)/photos/[assetId]/+page.ts index 428a3caae..f90b10991 100644 --- a/web/src/routes/(user)/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/photos/[assetId]/+page.ts @@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load = (async () => { +export const load = (() => { redirect(302, AppRoute.PHOTOS); }) satisfies PageLoad; diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index bda74db78..28ad6bcbd 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -35,6 +35,7 @@ import type { Viewport } from '$lib/stores/assets.store'; import { locale } from '$lib/stores/preferences.store'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; + import { handlePromiseError } from '$lib/utils'; import { parseUtcDate } from '$lib/utils/date-time'; const MAX_ASSET_COUNT = 5000; @@ -53,7 +54,7 @@ const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); - const handleKeyboardPress = (event: KeyboardEvent) => { + const handleKeyboardPress = async (event: KeyboardEvent) => { if (shouldIgnoreShortcut(event)) { return; } @@ -65,7 +66,7 @@ return; } if (!$preventRaceConditionSearchBar) { - goto(previousRoute); + await goto(previousRoute); } $preventRaceConditionSearchBar = false; return; @@ -108,13 +109,13 @@ return searchQuery ? JSON.parse(searchQuery) : {}; })(); - $: terms, onSearchQueryUpdate(); + $: terms, handlePromiseError(onSearchQueryUpdate()); async function onSearchQueryUpdate() { nextPage = 1; searchResultAssets = []; searchResultAlbums = []; - loadNextPage(); + await loadNextPage(); } export const loadNextPage = async () => { diff --git a/web/src/routes/(user)/search/photos/[assetId]/+page.ts b/web/src/routes/(user)/search/photos/[assetId]/+page.ts index 3c4bafa3e..f1e512693 100644 --- a/web/src/routes/(user)/search/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/search/photos/[assetId]/+page.ts @@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load = (async () => { +export const load = (() => { redirect(302, AppRoute.SEARCH); }) satisfies PageLoad; diff --git a/web/src/routes/(user)/share/[key]/+error.svelte b/web/src/routes/(user)/share/[key]/+error.svelte index fb1f0d766..c68b534a9 100644 --- a/web/src/routes/(user)/share/[key]/+error.svelte +++ b/web/src/routes/(user)/share/[key]/+error.svelte @@ -1,7 +1,14 @@ + + Opps! Error - Immich -
-
Page not found :/
+
+

Page not found :/

+ {#if $page.error?.message} +

{$page.error.message}

+ {/if}
diff --git a/web/src/routes/(user)/share/[key]/+page.ts b/web/src/routes/(user)/share/[key]/+page.ts index dd34b47e4..380c9d002 100644 --- a/web/src/routes/(user)/share/[key]/+page.ts +++ b/web/src/routes/(user)/share/[key]/+page.ts @@ -1,7 +1,6 @@ import { getAssetThumbnailUrl } from '$lib/utils'; import { authenticate } from '$lib/utils/auth'; -import { ThumbnailFormat, getMySharedLink } from '@immich/sdk'; -import { error as throwError, type HttpError } from '@sveltejs/kit'; +import { ThumbnailFormat, getMySharedLink, isHttpError } from '@immich/sdk'; import type { PageLoad } from './$types'; export const load = (async ({ params }) => { @@ -22,9 +21,7 @@ export const load = (async ({ params }) => { }, }; } catch (error) { - // handle unauthorized error - // TODO this doesn't allow for 404 shared links anymore - if ((error as HttpError).status === 401) { + if (isHttpError(error) && error.data.message === 'Invalid password') { return { passwordRequired: true, sharedLinkKey: key, @@ -34,8 +31,6 @@ export const load = (async ({ params }) => { }; } - throwError(404, { - message: 'Invalid shared link', - }); + throw error; } }) satisfies PageLoad; diff --git a/web/src/routes/(user)/sharing/+page.svelte b/web/src/routes/(user)/sharing/+page.svelte index 602a3a2e0..b50cb5089 100644 --- a/web/src/routes/(user)/sharing/+page.svelte +++ b/web/src/routes/(user)/sharing/+page.svelte @@ -19,7 +19,7 @@ const createSharedAlbum = async () => { try { const newAlbum = await createAlbum({ createAlbumDto: { albumName: '' } }); - goto(`${AppRoute.ALBUMS}/${newAlbum.id}`); + await goto(`${AppRoute.ALBUMS}/${newAlbum.id}`); } catch (error) { handleError(error, 'Unable to create album'); } diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte index 394e74faf..3a6ff6ac0 100644 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte +++ b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte @@ -40,7 +40,7 @@ deleteLinkId = null; await refresh(); } catch (error) { - await handleError(error, 'Unable to delete shared link'); + handleError(error, 'Unable to delete shared link'); } }; diff --git a/web/src/routes/(user)/trash/+page.svelte b/web/src/routes/(user)/trash/+page.svelte index 3dc126d7b..c2398563e 100644 --- a/web/src/routes/(user)/trash/+page.svelte +++ b/web/src/routes/(user)/trash/+page.svelte @@ -24,10 +24,11 @@ import { emptyTrash, restoreTrash } from '@immich/sdk'; import { mdiDeleteOutline, mdiHistory } from '@mdi/js'; import type { PageData } from './$types'; + import { handlePromiseError } from '$lib/utils'; export let data: PageData; - $: $featureFlags.trash || goto(AppRoute.PHOTOS); + $featureFlags.trash || handlePromiseError(goto(AppRoute.PHOTOS)); const assetStore = new AssetStore({ isTrashed: true }); const assetInteractionStore = createAssetInteractionStore(); diff --git a/web/src/routes/(user)/trash/photos/[assetId]/+page.ts b/web/src/routes/(user)/trash/photos/[assetId]/+page.ts index 0474207e3..eb3a453d2 100644 --- a/web/src/routes/(user)/trash/photos/[assetId]/+page.ts +++ b/web/src/routes/(user)/trash/photos/[assetId]/+page.ts @@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load = (async () => { +export const load = (() => { redirect(302, AppRoute.TRASH); }) satisfies PageLoad; diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts index ab0a19f1c..ffb36cf35 100644 --- a/web/src/routes/+layout.ts +++ b/web/src/routes/+layout.ts @@ -3,7 +3,7 @@ import type { LayoutLoad } from './$types'; export const ssr = false; export const csr = true; -export const load = (async () => { +export const load = (() => { return { meta: { title: 'Immich', diff --git a/web/src/routes/admin/+page.ts b/web/src/routes/admin/+page.ts index e4f090a06..0d53c4ef2 100644 --- a/web/src/routes/admin/+page.ts +++ b/web/src/routes/admin/+page.ts @@ -2,6 +2,6 @@ import { AppRoute } from '$lib/constants'; import { redirect } from '@sveltejs/kit'; import type { PageLoad } from './$types'; -export const load = (async () => { +export const load = (() => { redirect(302, AppRoute.ADMIN_USER_MANAGEMENT); }) satisfies PageLoad; diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index 38ecaa62f..2f6c98af1 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -4,6 +4,7 @@ import Icon from '$lib/components/elements/icon.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import { AppRoute } from '$lib/constants'; + import { asyncTimeout } from '$lib/utils'; import { getAllJobsStatus, type AllJobStatusResponseDto } from '@immich/sdk'; import { mdiCog } from '@mdi/js'; import { onDestroy, onMount } from 'svelte'; @@ -11,21 +12,19 @@ export let data: PageData; - let timer: ReturnType; - let jobs: AllJobStatusResponseDto; - const load = async () => { - jobs = await getAllJobsStatus(); - }; + let running = true; onMount(async () => { - await load(); - timer = setInterval(load, 5000); + while (running) { + jobs = await getAllJobsStatus(); + await asyncTimeout(5000); + } }); onDestroy(() => { - clearInterval(timer); + running = false; }); diff --git a/web/src/routes/admin/server-status/+page.svelte b/web/src/routes/admin/server-status/+page.svelte index bf2830f15..54f62b3ad 100644 --- a/web/src/routes/admin/server-status/+page.svelte +++ b/web/src/routes/admin/server-status/+page.svelte @@ -4,19 +4,21 @@ import { getServerStatistics } from '@immich/sdk'; import { onDestroy, onMount } from 'svelte'; import type { PageData } from './$types'; + import { asyncTimeout } from '$lib/utils'; export let data: PageData; - let setIntervalHandler: ReturnType; + let running = true; onMount(async () => { - setIntervalHandler = setInterval(async () => { + while (running) { data.stats = await getServerStatistics(); - }, 5000); + await asyncTimeout(5000); + } }); onDestroy(() => { - clearInterval(setIntervalHandler); + running = false; }); diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index 73237915b..a3a93e0f3 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -51,7 +51,7 @@ shouldShowCreateUserForm = false; }; - const editUserHandler = async (user: UserResponseDto) => { + const editUserHandler = (user: UserResponseDto) => { selectedUser = user; shouldShowEditUserForm = true; }; @@ -67,7 +67,7 @@ shouldShowInfoPanel = true; }; - const deleteUserHandler = async (user: UserResponseDto) => { + const deleteUserHandler = (user: UserResponseDto) => { selectedUser = user; shouldShowDeleteConfirmDialog = true; }; @@ -82,7 +82,7 @@ shouldShowDeleteConfirmDialog = false; }; - const restoreUserHandler = async (user: UserResponseDto) => { + const restoreUserHandler = (user: UserResponseDto) => { selectedUser = user; shouldShowRestoreDialog = true; }; diff --git a/web/src/routes/auth/change-password/+page.svelte b/web/src/routes/auth/change-password/+page.svelte index f56169ddb..277b3c004 100644 --- a/web/src/routes/auth/change-password/+page.svelte +++ b/web/src/routes/auth/change-password/+page.svelte @@ -3,10 +3,17 @@ import ChangePasswordForm from '$lib/components/forms/change-password-form.svelte'; import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte'; import { AppRoute } from '$lib/constants'; - import { user } from '$lib/stores/user.store'; + import { resetSavedUser, user } from '$lib/stores/user.store'; + import { logout } from '@immich/sdk'; import type { PageData } from './$types'; export let data: PageData; + + const onSuccess = async () => { + await goto(AppRoute.AUTH_LOGIN); + resetSavedUser(); + await logout(); + }; @@ -18,5 +25,5 @@ enter the new password below.

- goto(AppRoute.AUTH_LOGIN)} /> +
diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 199cd06e3..9c22439c5 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -1,21 +1,12 @@ {#if $featureFlags.loaded} diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index 4cf42d4d3..09139a7f7 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -29,17 +29,17 @@ const handleDoneClicked = async () => { if (index >= onboardingSteps.length - 1) { await setAdminOnboarding(); - goto(AppRoute.PHOTOS); + await goto(AppRoute.PHOTOS); } else { index++; - goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`); + await goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`); } }; - const handlePrevious = () => { + const handlePrevious = async () => { if (index >= 1) { index--; - goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`); + await goto(`${AppRoute.AUTH_ONBOARDING}?${QueryParameter.ONBOARDING_STEP}=${onboardingSteps[index].name}`); } }; diff --git a/web/svelte.config.js b/web/svelte.config.js index 0081e8e76..3cb982c6b 100644 --- a/web/svelte.config.js +++ b/web/svelte.config.js @@ -4,12 +4,6 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), - onwarn: (warning, handler) => { - if (warning.code.includes('a11y')) { - return; - } - handler(warning); - }, kit: { adapter: adapter({ // default options are shown. On some platforms