diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 87170e5d59..07e07f422a 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 a6a6835882..dd1c53468a 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 c3ede44661..121cd1d940 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 22d8585704..f48f74d51c 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 f5585d815a..2f6ae3ebde 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/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md index 66f4d7b9f2..24919347f0 100644 --- a/docs/docs/administration/reverse-proxy.md +++ b/docs/docs/administration/reverse-proxy.md @@ -44,22 +44,13 @@ Below is an example config for Apache2 site configuration. ``` - ServerName + ServerName + ProxyRequests Off + ProxyPass / http://127.0.0.1:2283/ timeout=600 upgrade=websocket + ProxyPassReverse / http://127.0.0.1:2283/ + ProxyPreserveHost On - ProxyRequests off - ProxyVia on - - RewriteEngine On - RewriteCond %{REQUEST_URI} ^/api/socket.io [NC] - RewriteCond %{QUERY_STRING} transport=websocket [NC] - RewriteRule /(.*) ws://localhost:2283/$1 [P,L] - - ProxyPass /api/socket.io ws://localhost:2283/api/socket.io - ProxyPassReverse /api/socket.io ws://localhost:2283/api/socket.io - - - ProxyPass http://localhost:2283/ - ProxyPassReverse http://localhost:2283/ - ``` + +**timeout:** is measured in seconds, and it is particularly useful when long operations are triggered (i.e. Repair), so the server doesn't return an error. diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index fb02aff2ff..8228ff2893 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 9fe651a52a..954d1cc3fc 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", @@ -904,9 +919,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.0.tgz", - "integrity": "sha512-e5Y5uK5NNoQMQaNitGQQjo9FoA5ZNcu7Bn6pH+dxUf48u6po1cX38kFBYUHZ9GNVkF4JLbncE0WeWwTw+nLrxg==", + "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", @@ -927,17 +942,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.3.0" + "vitest": "1.3.1" } }, "node_modules/@vitest/expect": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.0.tgz", - "integrity": "sha512-7bWt0vBTZj08B+Ikv70AnLRicohYwFgzNjFqo9SxxqHHxSlUJGSXmCRORhOnRMisiUryKMdvsi1n27Bc6jL9DQ==", + "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.3.0", - "@vitest/utils": "1.3.0", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", "chai": "^4.3.10" }, "funding": { @@ -945,12 +960,12 @@ } }, "node_modules/@vitest/runner": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.0.tgz", - "integrity": "sha512-1Jb15Vo/Oy7mwZ5bXi7zbgszsdIBNjc4IqP8Jpr/8RdBC4nF1CTzIAn2dxYvpF1nGSseeL39lfLQ2uvs5u1Y9A==", + "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.3.0", + "@vitest/utils": "1.3.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" }, @@ -959,9 +974,9 @@ } }, "node_modules/@vitest/snapshot": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.0.tgz", - "integrity": "sha512-swmktcviVVPYx9U4SEQXLV6AEY51Y6bZ14jA2yo6TgMxQ3h+ZYiO0YhAHGJNp0ohCFbPAis1R9kK0cvN6lDPQA==", + "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", @@ -973,9 +988,9 @@ } }, "node_modules/@vitest/spy": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.0.tgz", - "integrity": "sha512-AkCU0ThZunMvblDpPKgjIi025UxR8V7MZ/g/EwmAGpjIujLVV2X6rGYGmxE2D4FJbAy0/ijdROHMWa2M/6JVMw==", + "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" @@ -985,9 +1000,9 @@ } }, "node_modules/@vitest/utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.0.tgz", - "integrity": "sha512-/LibEY/fkaXQufi4GDlQZhikQsPO2entBKtfuyIpr1jV4DpaeasqkeHjhdOhU24vSHshcSuEyVlWdzvv2XmYCw==", + "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", @@ -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", @@ -2551,9 +2625,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", @@ -2606,9 +2680,9 @@ } }, "node_modules/vite-node": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.0.tgz", - "integrity": "sha512-D/oiDVBw75XMnjAXne/4feCkCEwcbr2SU1bjAhCcfI5Bq3VoOHji8/wCPAfUkDIeohJ5nSZ39fNxM3dNZ6OBOA==", + "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", @@ -2642,16 +2716,16 @@ } }, "node_modules/vitest": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.0.tgz", - "integrity": "sha512-V9qb276J1jjSx9xb75T2VoYXdO1UKi+qfflY7V7w93jzX7oA/+RtYE6TcifxksxsZvygSSMwu2Uw6di7yqDMwg==", + "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.3.0", - "@vitest/runner": "1.3.0", - "@vitest/snapshot": "1.3.0", - "@vitest/spy": "1.3.0", - "@vitest/utils": "1.3.0", + "@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", "chai": "^4.3.10", "debug": "^4.3.4", @@ -2665,7 +2739,7 @@ "tinybench": "^2.5.1", "tinypool": "^0.8.2", "vite": "^5.0.0", - "vite-node": "1.3.0", + "vite-node": "1.3.1", "why-is-node-running": "^2.2.2" }, "bin": { @@ -2680,8 +2754,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.3.0", - "@vitest/ui": "1.3.0", + "@vitest/browser": "1.3.1", + "@vitest/ui": "1.3.1", "happy-dom": "*", "jsdom": "*" }, @@ -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 ebd5b9aeae..7bbdfd1d9d 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 738411338f..39c075dba2 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 c131edc49c..3385e50f4d 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 0000000000..db1821260b --- /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 f1d7bd1123..22d66baf05 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 e791c447ac..0bb760fbc5 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 0000000000..2de838f981 --- /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 e9e89befd1..038a2c2ca0 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 6dd664e1e2..908118d77c 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 fbc0b43b31..428c88b454 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 faa7b34255..ce79ed5454 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/ann/__init__.py b/machine-learning/ann/__init__.py index 0793d1011b..e69de29bb2 100644 --- a/machine-learning/ann/__init__.py +++ b/machine-learning/ann/__init__.py @@ -1 +0,0 @@ -from .ann import Ann, is_available diff --git a/machine-learning/ann/ann.py b/machine-learning/ann/ann.py index 94f665bfc7..148d5ba101 100644 --- a/machine-learning/ann/ann.py +++ b/machine-learning/ann/ann.py @@ -32,8 +32,7 @@ T = TypeVar("T", covariant=True) class Newable(Protocol[T]): - def new(self) -> None: - ... + def new(self) -> None: ... class _Singleton(type, Newable[T]): diff --git a/machine-learning/app/models/base.py b/machine-learning/app/models/base.py index 6097c7c987..ad48624b4e 100644 --- a/machine-learning/app/models/base.py +++ b/machine-learning/app/models/base.py @@ -1,18 +1,16 @@ from __future__ import annotations +import os from abc import ABC, abstractmethod from pathlib import Path from shutil import rmtree from typing import Any -import onnx import onnxruntime as ort from huggingface_hub import snapshot_download -from onnx.shape_inference import infer_shapes -from onnx.tools.update_model_dims import update_inputs_outputs_dims import ann.ann -from app.models.constants import STATIC_INPUT_PROVIDERS, SUPPORTED_PROVIDERS +from app.models.constants import SUPPORTED_PROVIDERS from ..config import get_cache_dir, get_hf_model_name, log, settings from ..schemas import ModelRuntime, ModelType @@ -113,63 +111,25 @@ class InferenceModel(ABC): ) model_path = onnx_path - if any(provider in STATIC_INPUT_PROVIDERS for provider in self.providers): - static_path = model_path.parent / "static_1" / "model.onnx" - static_path.parent.mkdir(parents=True, exist_ok=True) - if not static_path.is_file(): - self._convert_to_static(model_path, static_path) - model_path = static_path - match model_path.suffix: case ".armnn": session = AnnSession(model_path) case ".onnx": - session = ort.InferenceSession( - model_path.as_posix(), - sess_options=self.sess_options, - providers=self.providers, - provider_options=self.provider_options, - ) + cwd = os.getcwd() + try: + os.chdir(model_path.parent) + session = ort.InferenceSession( + model_path.as_posix(), + sess_options=self.sess_options, + providers=self.providers, + provider_options=self.provider_options, + ) + finally: + os.chdir(cwd) case _: raise ValueError(f"Unsupported model file type: {model_path.suffix}") return session - def _convert_to_static(self, source_path: Path, target_path: Path) -> None: - inferred = infer_shapes(onnx.load(source_path)) - inputs = self._get_static_dims(inferred.graph.input) - outputs = self._get_static_dims(inferred.graph.output) - - # check_model gets called in update_inputs_outputs_dims and doesn't work for large models - check_model = onnx.checker.check_model - try: - - def check_model_stub(*args: Any, **kwargs: Any) -> None: - pass - - onnx.checker.check_model = check_model_stub - updated_model = update_inputs_outputs_dims(inferred, inputs, outputs) - finally: - onnx.checker.check_model = check_model - - onnx.save( - updated_model, - target_path, - save_as_external_data=True, - all_tensors_to_one_file=False, - size_threshold=1048576, - ) - - def _get_static_dims(self, graph_io: Any, dim_size: int = 1) -> dict[str, list[int]]: - return { - field.name: [ - d.dim_value if d.HasField("dim_value") else dim_size - for shape in field.type.ListFields() - if (dim := shape[1].shape.dim) - for d in dim - ] - for field in graph_io - } - @property def model_type(self) -> ModelType: return self._model_type @@ -205,6 +165,14 @@ class InferenceModel(ABC): def providers_default(self) -> list[str]: available_providers = set(ort.get_available_providers()) log.debug(f"Available ORT providers: {available_providers}") + if (openvino := "OpenVINOExecutionProvider") in available_providers: + device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids() + log.debug(f"Available OpenVINO devices: {device_ids}") + + gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")] + if not gpu_devices: + log.warning("No GPU device found in OpenVINO. Falling back to CPU.") + available_providers.remove(openvino) return [provider for provider in SUPPORTED_PROVIDERS if provider in available_providers] @property @@ -224,15 +192,7 @@ class InferenceModel(ABC): case "CPUExecutionProvider" | "CUDAExecutionProvider": option = {"arena_extend_strategy": "kSameAsRequested"} case "OpenVINOExecutionProvider": - try: - device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids() - log.debug(f"Available OpenVINO devices: {device_ids}") - gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")] - option = {"device_id": gpu_devices[0]} if gpu_devices else {} - except AttributeError as e: - log.warning("Failed to get OpenVINO device IDs. Using default options.") - log.error(e) - option = {} + option = {"device_type": "GPU_FP32"} case _: option = {} options.append(option) diff --git a/machine-learning/app/models/constants.py b/machine-learning/app/models/constants.py index 18965d2b1d..b112e9279d 100644 --- a/machine-learning/app/models/constants.py +++ b/machine-learning/app/models/constants.py @@ -54,9 +54,6 @@ _INSIGHTFACE_MODELS = { SUPPORTED_PROVIDERS = ["CUDAExecutionProvider", "OpenVINOExecutionProvider", "CPUExecutionProvider"] -STATIC_INPUT_PROVIDERS = ["OpenVINOExecutionProvider"] - - def is_openclip(model_name: str) -> bool: return clean_name(model_name) in _OPENCLIP_MODELS diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index cf941c1bbf..0f802997fd 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -1,4 +1,5 @@ import json +import os from io import BytesIO from pathlib import Path from random import randint @@ -44,11 +45,23 @@ class TestBase: assert encoder.providers == self.CUDA_EP @pytest.mark.providers(OV_EP) - def test_sets_openvino_provider_if_available(self, providers: list[str]) -> None: + def test_sets_openvino_provider_if_available(self, providers: list[str], mocker: MockerFixture) -> None: + mocked = mocker.patch("app.models.base.ort.capi._pybind_state") + mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"] + encoder = OpenCLIPEncoder("ViT-B-32__openai") assert encoder.providers == self.OV_EP + @pytest.mark.providers(OV_EP) + def test_avoids_openvino_if_gpu_not_available(self, providers: list[str], mocker: MockerFixture) -> None: + mocked = mocker.patch("app.models.base.ort.capi._pybind_state") + mocked.get_available_openvino_device_ids.return_value = ["CPU"] + + encoder = OpenCLIPEncoder("ViT-B-32__openai") + + assert encoder.providers == self.CPU_EP + @pytest.mark.providers(CUDA_EP_OUT_OF_ORDER) def test_sets_providers_in_correct_order(self, providers: list[str]) -> None: encoder = OpenCLIPEncoder("ViT-B-32__openai") @@ -67,22 +80,14 @@ class TestBase: assert encoder.providers == providers - def test_sets_default_provider_options(self) -> None: - encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"]) - - assert encoder.provider_options == [ - {}, - {"arena_extend_strategy": "kSameAsRequested"}, - ] - - def test_sets_openvino_device_id_if_possible(self, mocker: MockerFixture) -> None: + def test_sets_default_provider_options(self, mocker: MockerFixture) -> None: mocked = mocker.patch("app.models.base.ort.capi._pybind_state") mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"] encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"]) assert encoder.provider_options == [ - {"device_id": "GPU.0"}, + {"device_type": "GPU_FP32"}, {"arena_extend_strategy": "kSameAsRequested"}, ] @@ -237,12 +242,12 @@ class TestBase: mock_model_path.is_file.return_value = True mock_model_path.suffix = ".armnn" mock_model_path.with_suffix.return_value = mock_model_path - mock_session = mocker.patch("app.models.base.AnnSession") + mock_ann = mocker.patch("app.models.base.AnnSession") encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder._make_session(mock_model_path) - mock_session.assert_called_once() + mock_ann.assert_called_once() def test_make_session_return_ort_if_available_and_ann_is_not(self, mocker: MockerFixture) -> None: mock_armnn_path = mocker.Mock() @@ -256,6 +261,7 @@ class TestBase: mock_ann = mocker.patch("app.models.base.AnnSession") mock_ort = mocker.patch("app.models.base.ort.InferenceSession") + mocker.patch("app.models.base.os.chdir") encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder._make_session(mock_armnn_path) @@ -278,6 +284,26 @@ class TestBase: mock_ann.assert_not_called() mock_ort.assert_not_called() + def test_make_session_changes_cwd(self, mocker: MockerFixture) -> None: + mock_model_path = mocker.Mock() + mock_model_path.is_file.return_value = True + mock_model_path.suffix = ".onnx" + mock_model_path.parent = "model_parent" + mock_model_path.with_suffix.return_value = mock_model_path + mock_ort = mocker.patch("app.models.base.ort.InferenceSession") + mock_chdir = mocker.patch("app.models.base.os.chdir") + + encoder = OpenCLIPEncoder("ViT-B-32__openai") + encoder._make_session(mock_model_path) + + mock_chdir.assert_has_calls( + [ + mock.call(mock_model_path.parent), + mock.call(os.getcwd()), + ] + ) + mock_ort.assert_called_once() + def test_download(self, mocker: MockerFixture) -> None: mock_snapshot_download = mocker.patch("app.models.base.snapshot_download") diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 40402886ff..c22668380e 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:926cac38640709f90f3fef2a3f730733b5c350be612f0d14706be8833b79ad8c as builder +FROM mambaorg/micromamba:bookworm-slim@sha256:6038b89363c9181215f3d9e8ce2720c880e224537f4028a854482e43a9b4998a as builder ENV NODE_ENV=production \ TRANSFORMERS_CACHE=/cache \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index c8ae6c7410..d7cff9c10b 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -1250,13 +1250,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.26.0" +version = "0.27.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, - {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, ] [package.dependencies] @@ -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]] @@ -2465,13 +2465,13 @@ files = [ [[package]] name = "pytest" -version = "8.0.0" +version = "8.0.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, - {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, ] [package.dependencies] @@ -2836,28 +2836,28 @@ files = [ [[package]] name = "ruff" -version = "0.2.1" +version = "0.2.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"}, - {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"}, - {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"}, - {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"}, - {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"}, - {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"}, - {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"}, - {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, + {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, + {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, + {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, + {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, ] [[package]] diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 750ca65f26..fec5c72130 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -82,10 +82,10 @@ warn_untyped_fields = true [tool.ruff] line-length = 120 target-version = "py311" -select = ["E", "F", "I"] -[tool.ruff.per-file-ignores] -"test_main.py" = ["F403"] +[tool.ruff.lint] +select = ["E", "F", "I"] +per-file-ignores = { "test_main.py" = ["F403"] } [tool.black] line-length = 120 diff --git a/mobile/lib/extensions/asyncvalue_extensions.dart b/mobile/lib/extensions/asyncvalue_extensions.dart index c616835a81..af02ff13c2 100644 --- a/mobile/lib/extensions/asyncvalue_extensions.dart +++ b/mobile/lib/extensions/asyncvalue_extensions.dart @@ -30,7 +30,7 @@ extension LogOnError on AsyncValue { } if (hasError && !hasValue) { - _asyncErrorLogger.severe("$error", error, stackTrace); + _asyncErrorLogger.severe('Could not load value', error, stackTrace); return onError?.call(error, stackTrace) ?? ScaffoldErrorBody(errorMsg: error?.toString()); } diff --git a/mobile/lib/extensions/response_extensions.dart b/mobile/lib/extensions/response_extensions.dart new file mode 100644 index 0000000000..7fec41d07c --- /dev/null +++ b/mobile/lib/extensions/response_extensions.dart @@ -0,0 +1,5 @@ +import 'package:http/http.dart'; + +extension LoggerExtension on Response { + String toLoggerString() => "Status: $statusCode $reasonPhrase\n\n$body"; +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 293867fb32..f20cf7ecc6 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -73,15 +73,14 @@ Future initApp() async { FlutterError.onError = (details) { FlutterError.presentError(details); log.severe( - 'FlutterError - Catch all error: ${details.toString()} - ${details.exception} - ${details.library} - ${details.context} - ${details.stack}', - details, + 'FlutterError - Catch all', + "${details.toString()}\nException: ${details.exception}\nLibrary: ${details.library}\nContext: ${details.context}", details.stack, ); }; PlatformDispatcher.instance.onError = (error, stack) { - log.severe('PlatformDispatcher - Catch all error: $error', error, stack); - debugPrint("PlatformDispatcher - Catch all error: $error $stack"); + log.severe('PlatformDispatcher - Catch all', error, stack); return true; }; diff --git a/mobile/lib/mixins/error_logger.mixin.dart b/mobile/lib/mixins/error_logger.mixin.dart index 38837a716f..9b2bc6f98e 100644 --- a/mobile/lib/mixins/error_logger.mixin.dart +++ b/mobile/lib/mixins/error_logger.mixin.dart @@ -10,13 +10,14 @@ mixin ErrorLoggerMixin { /// Else, logs the error to the overrided logger and returns an AsyncError<> AsyncFuture guardError( Future Function() fn, { + required String errorMessage, Level logLevel = Level.SEVERE, }) async { try { final result = await fn(); return AsyncData(result); } catch (error, stackTrace) { - logger.log(logLevel, "$error", error, stackTrace); + logger.log(logLevel, errorMessage, error, stackTrace); return AsyncError(error, stackTrace); } } @@ -26,12 +27,13 @@ mixin ErrorLoggerMixin { Future logError( Future Function() fn, { required T defaultValue, + required String errorMessage, Level logLevel = Level.SEVERE, }) async { try { return await fn(); } catch (error, stackTrace) { - logger.log(logLevel, "$error", error, stackTrace); + logger.log(logLevel, errorMessage, error, stackTrace); } return defaultValue; } diff --git a/mobile/lib/modules/activities/services/activity.service.dart b/mobile/lib/modules/activities/services/activity.service.dart index db35c17aee..cde98f73ae 100644 --- a/mobile/lib/modules/activities/services/activity.service.dart +++ b/mobile/lib/modules/activities/services/activity.service.dart @@ -24,6 +24,7 @@ class ActivityService with ErrorLoggerMixin { return list != null ? list.map(Activity.fromDto).toList() : []; }, defaultValue: [], + errorMessage: "Failed to get all activities for album $albumId", ); } @@ -35,6 +36,7 @@ class ActivityService with ErrorLoggerMixin { return dto?.comments ?? 0; }, defaultValue: 0, + errorMessage: "Failed to statistics for album $albumId", ); } @@ -45,6 +47,7 @@ class ActivityService with ErrorLoggerMixin { return true; }, defaultValue: false, + errorMessage: "Failed to delete activity", ); } @@ -54,21 +57,24 @@ class ActivityService with ErrorLoggerMixin { String? assetId, String? comment, }) async { - return guardError(() async { - final dto = await _apiService.activityApi.createActivity( - ActivityCreateDto( - albumId: albumId, - type: type == ActivityType.comment - ? ReactionType.comment - : ReactionType.like, - assetId: assetId, - comment: comment, - ), - ); - if (dto != null) { - return Activity.fromDto(dto); - } - throw NoResponseDtoError(); - }); + return guardError( + () async { + final dto = await _apiService.activityApi.createActivity( + ActivityCreateDto( + albumId: albumId, + type: type == ActivityType.comment + ? ReactionType.comment + : ReactionType.like, + assetId: assetId, + comment: comment, + ), + ); + if (dto != null) { + return Activity.fromDto(dto); + } + throw NoResponseDtoError(); + }, + errorMessage: "Failed to create $type for album $albumId", + ); } } diff --git a/mobile/lib/modules/album/ui/album_thumbnail_card.dart b/mobile/lib/modules/album/ui/album_thumbnail_card.dart index 880312322c..1da284572b 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 f70c706f35..5a27def4c9 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 2e826e86da..e6b2ade6bc 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/hooks/chewiew_controller_hook.dart b/mobile/lib/modules/asset_viewer/hooks/chewiew_controller_hook.dart new file mode 100644 index 0000000000..224eb838e7 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/hooks/chewiew_controller_hook.dart @@ -0,0 +1,179 @@ +import 'dart:async'; + +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/store.dart'; +import 'package:video_player/video_player.dart'; +import 'package:immich_mobile/shared/models/store.dart' as store; +import 'package:wakelock_plus/wakelock_plus.dart'; + +/// Provides the initialized video player controller +/// If the asset is local, use the local file +/// Otherwise, use a video player with a URL +ChewieController? useChewieController( + Asset asset, { + EdgeInsets controlsSafeAreaMinimum = const EdgeInsets.only( + bottom: 100, + ), + bool showOptions = true, + bool showControlsOnInitialize = false, + bool autoPlay = true, + bool autoInitialize = true, + bool allowFullScreen = false, + bool allowedScreenSleep = false, + bool showControls = true, + Widget? customControls, + Widget? placeholder, + Duration hideControlsTimer = const Duration(seconds: 1), + VoidCallback? onPlaying, + VoidCallback? onPaused, + VoidCallback? onVideoEnded, +}) { + return use( + _ChewieControllerHook( + asset: asset, + placeholder: placeholder, + showOptions: showOptions, + controlsSafeAreaMinimum: controlsSafeAreaMinimum, + autoPlay: autoPlay, + allowFullScreen: allowFullScreen, + customControls: customControls, + hideControlsTimer: hideControlsTimer, + showControlsOnInitialize: showControlsOnInitialize, + showControls: showControls, + autoInitialize: autoInitialize, + allowedScreenSleep: allowedScreenSleep, + onPlaying: onPlaying, + onPaused: onPaused, + onVideoEnded: onVideoEnded, + ), + ); +} + +class _ChewieControllerHook extends Hook { + final Asset asset; + final EdgeInsets controlsSafeAreaMinimum; + final bool showOptions; + final bool showControlsOnInitialize; + final bool autoPlay; + final bool autoInitialize; + final bool allowFullScreen; + final bool allowedScreenSleep; + final bool showControls; + final Widget? customControls; + final Widget? placeholder; + final Duration hideControlsTimer; + final VoidCallback? onPlaying; + final VoidCallback? onPaused; + final VoidCallback? onVideoEnded; + + const _ChewieControllerHook({ + required this.asset, + this.controlsSafeAreaMinimum = const EdgeInsets.only( + bottom: 100, + ), + this.showOptions = true, + this.showControlsOnInitialize = false, + this.autoPlay = true, + this.autoInitialize = true, + this.allowFullScreen = false, + this.allowedScreenSleep = false, + this.showControls = true, + this.customControls, + this.placeholder, + this.hideControlsTimer = const Duration(seconds: 3), + this.onPlaying, + this.onPaused, + this.onVideoEnded, + }); + + @override + createState() => _ChewieControllerHookState(); +} + +class _ChewieControllerHookState + extends HookState { + ChewieController? chewieController; + VideoPlayerController? videoPlayerController; + + @override + void initHook() async { + super.initHook(); + unawaited(_initialize()); + } + + @override + void dispose() { + chewieController?.dispose(); + videoPlayerController?.dispose(); + super.dispose(); + } + + @override + ChewieController? build(BuildContext context) { + return chewieController; + } + + /// Initializes the chewie controller and video player controller + Future _initialize() async { + if (hook.asset.isLocal && hook.asset.livePhotoVideoId == null) { + // Use a local file for the video player controller + final file = await hook.asset.local!.file; + if (file == null) { + throw Exception('No file found for the video'); + } + videoPlayerController = VideoPlayerController.file(file); + } else { + // Use a network URL for the video player controller + final serverEndpoint = store.Store.get(store.StoreKey.serverEndpoint); + final String videoUrl = hook.asset.livePhotoVideoId != null + ? '$serverEndpoint/asset/file/${hook.asset.livePhotoVideoId}' + : '$serverEndpoint/asset/file/${hook.asset.remoteId}'; + + final url = Uri.parse(videoUrl); + final accessToken = store.Store.get(StoreKey.accessToken); + + videoPlayerController = VideoPlayerController.networkUrl( + url, + httpHeaders: {"x-immich-user-token": accessToken}, + ); + } + + videoPlayerController!.addListener(() { + final value = videoPlayerController!.value; + if (value.isPlaying) { + WakelockPlus.enable(); + hook.onPlaying?.call(); + } else if (!value.isPlaying) { + WakelockPlus.disable(); + hook.onPaused?.call(); + } + + if (value.position == value.duration) { + WakelockPlus.disable(); + hook.onVideoEnded?.call(); + } + }); + + await videoPlayerController!.initialize(); + + setState(() { + chewieController = ChewieController( + videoPlayerController: videoPlayerController!, + controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum, + showOptions: hook.showOptions, + showControlsOnInitialize: hook.showControlsOnInitialize, + autoPlay: hook.autoPlay, + autoInitialize: hook.autoInitialize, + allowFullScreen: hook.allowFullScreen, + allowedScreenSleep: hook.allowedScreenSleep, + showControls: hook.showControls, + customControls: hook.customControls, + placeholder: hook.placeholder, + hideControlsTimer: hook.hideControlsTimer, + ); + }); + } +} 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 4c1e9fc5c8..3094c69076 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 0000000000..bb86cfafda --- /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 9f9af7aded..d9fbd80485 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 8332d8d3d7..92b85b3472 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/services/image_viewer.service.dart b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart index db527c6e23..54682fdeeb 100644 --- a/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart +++ b/mobile/lib/modules/asset_viewer/services/image_viewer.service.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; @@ -39,7 +40,8 @@ class ImageViewerService { final failedResponse = imageResponse.statusCode != 200 ? imageResponse : motionReponse; _log.severe( - "Motion asset download failed with status - ${failedResponse.statusCode} and response - ${failedResponse.body}", + "Motion asset download failed", + failedResponse.toLoggerString(), ); return false; } @@ -75,9 +77,7 @@ class ImageViewerService { .downloadFileWithHttpInfo(asset.remoteId!); if (res.statusCode != 200) { - _log.severe( - "Asset download failed with status - ${res.statusCode} and response - ${res.body}", - ); + _log.severe("Asset download failed", res.toLoggerString()); return false; } @@ -98,7 +98,7 @@ class ImageViewerService { return entity != null; } } catch (error, stack) { - _log.severe("Error saving file ${error.toString()}", error, stack); + _log.severe("Error saving downloaded asset", error, stack); return false; } finally { // Clear temp files diff --git a/mobile/lib/modules/asset_viewer/ui/description_input.dart b/mobile/lib/modules/asset_viewer/ui/description_input.dart index c5972a822d..c5bae07cde 100644 --- a/mobile/lib/modules/asset_viewer/ui/description_input.dart +++ b/mobile/lib/modules/asset_viewer/ui/description_input.dart @@ -48,7 +48,7 @@ class DescriptionInput extends HookConsumerWidget { ); } catch (error, stack) { hasError.value = true; - _log.severe("Error updating description $error", error, stack); + _log.severe("Error updating description", error, stack); ImmichToast.show( context: context, msg: "description_input_submit_error".tr(), diff --git a/mobile/lib/modules/asset_viewer/ui/video_player_controls.dart b/mobile/lib/modules/asset_viewer/ui/video_player_controls.dart index 781e84e458..bfc45b8a35 100644 --- a/mobile/lib/modules/asset_viewer/ui/video_player_controls.dart +++ b/mobile/lib/modules/asset_viewer/ui/video_player_controls.dart @@ -7,7 +7,7 @@ import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provi import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart'; import 'package:video_player/video_player.dart'; class VideoPlayerControls extends ConsumerStatefulWidget { @@ -66,7 +66,9 @@ class VideoPlayerControlsState extends ConsumerState children: [ if (_displayBufferingIndicator) const Center( - child: ImmichLoadingIndicator(), + child: DelayedLoadingIndicator( + fadeInDuration: Duration(milliseconds: 400), + ), ) else _buildHitArea(), @@ -79,6 +81,7 @@ class VideoPlayerControlsState extends ConsumerState @override void dispose() { _dispose(); + super.dispose(); } @@ -92,6 +95,7 @@ class VideoPlayerControlsState extends ConsumerState final oldController = _chewieController; _chewieController = ChewieController.of(context); controller = chewieController.videoPlayerController; + _latestValue = controller.value; if (oldController != chewieController) { _dispose(); @@ -106,12 +110,10 @@ class VideoPlayerControlsState extends ConsumerState return GestureDetector( onTap: () { - if (_latestValue.isPlaying) { - ref.read(showControlsProvider.notifier).show = false; - } else { + if (!_latestValue.isPlaying) { _playPause(); - ref.read(showControlsProvider.notifier).show = false; } + ref.read(showControlsProvider.notifier).show = false; }, child: CenterPlayButton( backgroundColor: Colors.black54, @@ -131,10 +133,11 @@ class VideoPlayerControlsState extends ConsumerState } Future _initialize() async { + ref.read(showControlsProvider.notifier).show = false; _mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute))); - controller.addListener(_updateState); _latestValue = controller.value; + controller.addListener(_updateState); if (controller.value.isPlaying || chewieController.autoPlay) { _startHideTimer(); @@ -167,9 +170,8 @@ class VideoPlayerControlsState extends ConsumerState } void _startHideTimer() { - final hideControlsTimer = chewieController.hideControlsTimer.isNegative - ? ChewieController.defaultHideControlsTimer - : chewieController.hideControlsTimer; + final hideControlsTimer = chewieController.hideControlsTimer; + _hideTimer?.cancel(); _hideTimer = Timer(hideControlsTimer, () { ref.read(showControlsProvider.notifier).show = false; }); diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 59dfef8164..48eb778c10 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!), ), ), ), @@ -704,6 +699,18 @@ class GalleryViewerPage extends HookConsumerWidget { ); } + useEffect( + () { + if (ref.read(showControlsProvider)) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + } + return null; + }, + [], + ); + ref.listen(showControlsProvider, (_, show) { if (show) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); @@ -728,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 @@ -794,7 +807,9 @@ class GalleryViewerPage extends HookConsumerWidget { minScale: 1.0, basePosition: Alignment.center, child: VideoViewerPage( - onPlaying: () => isPlayingVideo.value = true, + onPlaying: () { + isPlayingVideo.value = true; + }, onPaused: () => WidgetsBinding.instance.addPostFrameCallback( (_) => isPlayingVideo.value = false, 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 72aa397f67..eb125f27fb 100644 --- a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart @@ -1,23 +1,15 @@ -import 'dart:io'; - import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:chewie/chewie.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; -import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/modules/asset_viewer/hooks/chewiew_controller_hook.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/models/store.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; -import 'package:photo_manager/photo_manager.dart'; -import 'package:video_player/video_player.dart'; -import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart'; @RoutePage() // ignore: must_be_immutable -class VideoViewerPage extends HookConsumerWidget { +class VideoViewerPage extends HookWidget { final Asset asset; final bool isMotionVideo; final Widget? placeholder; @@ -42,211 +34,49 @@ class VideoViewerPage extends HookConsumerWidget { }); @override - Widget build(BuildContext context, WidgetRef ref) { - if (asset.isLocal && asset.livePhotoVideoId == null) { - final AsyncValue videoFile = ref.watch(_fileFamily(asset.local!)); - return AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: videoFile.when( - data: (data) => VideoPlayer( - file: data, - isMotionVideo: false, - onVideoEnded: () {}, - ), - error: (error, stackTrace) => Icon( - Icons.image_not_supported_outlined, - color: context.primaryColor, - ), - loading: () => showDownloadingIndicator - ? const Center(child: ImmichLoadingIndicator()) - : Container(), - ), - ); - } - final downloadAssetStatus = - ref.watch(imageViewerStateProvider).downloadAssetStatus; - final String videoUrl = isMotionVideo - ? '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.livePhotoVideoId}' - : '${Store.get(StoreKey.serverEndpoint)}/asset/file/${asset.remoteId}'; - - return Stack( - children: [ - VideoPlayer( - url: videoUrl, - accessToken: Store.get(StoreKey.accessToken), - isMotionVideo: isMotionVideo, - onVideoEnded: onVideoEnded, - onPaused: onPaused, - onPlaying: onPlaying, - placeholder: placeholder, - hideControlsTimer: hideControlsTimer, - showControls: showControls, - showDownloadingIndicator: showDownloadingIndicator, - ), - AnimatedOpacity( - duration: const Duration(milliseconds: 400), - opacity: (downloadAssetStatus == DownloadAssetStatus.loading && - showDownloadingIndicator) - ? 1.0 - : 0.0, - child: SizedBox( - height: context.height, - width: context.width, - child: const Center( - child: ImmichLoadingIndicator(), - ), - ), - ), - ], - ); - } -} - -final _fileFamily = - FutureProvider.family((ref, entity) async { - final file = await entity.file; - if (file == null) { - throw Exception(); - } - return file; -}); - -class VideoPlayer extends StatefulWidget { - final String? url; - final String? accessToken; - final File? file; - final bool isMotionVideo; - final VoidCallback? onVideoEnded; - final Duration hideControlsTimer; - final bool showControls; - - final Function()? onPlaying; - final Function()? onPaused; - - /// The placeholder to show while the video is loading - /// usually, a thumbnail of the video - final Widget? placeholder; - - final bool showDownloadingIndicator; - - const VideoPlayer({ - super.key, - this.url, - this.accessToken, - this.file, - this.onVideoEnded, - required this.isMotionVideo, - this.onPlaying, - this.onPaused, - this.placeholder, - this.hideControlsTimer = const Duration( - seconds: 5, - ), - this.showControls = true, - this.showDownloadingIndicator = true, - }); - - @override - State createState() => _VideoPlayerState(); -} - -class _VideoPlayerState extends State { - late VideoPlayerController videoPlayerController; - ChewieController? chewieController; - - @override - void initState() { - super.initState(); - initializePlayer(); - - videoPlayerController.addListener(() { - if (videoPlayerController.value.isInitialized) { - if (videoPlayerController.value.isPlaying) { - WakelockPlus.enable(); - widget.onPlaying?.call(); - } else if (!videoPlayerController.value.isPlaying) { - WakelockPlus.disable(); - widget.onPaused?.call(); - } - - if (videoPlayerController.value.position == - videoPlayerController.value.duration) { - WakelockPlus.disable(); - widget.onVideoEnded?.call(); - } - } - }); - } - - Future initializePlayer() async { - try { - videoPlayerController = widget.file == null - ? VideoPlayerController.networkUrl( - Uri.parse(widget.url!), - httpHeaders: {"x-immich-user-token": widget.accessToken ?? ""}, - ) - : VideoPlayerController.file(widget.file!); - - await videoPlayerController.initialize(); - _createChewieController(); - setState(() {}); - } catch (e) { - debugPrint("ERROR initialize video player $e"); - } - } - - _createChewieController() { - chewieController = ChewieController( + Widget build(BuildContext context) { + final controller = useChewieController( + asset, controlsSafeAreaMinimum: const EdgeInsets.only( bottom: 100, ), - showOptions: true, - showControlsOnInitialize: false, - videoPlayerController: videoPlayerController, - autoPlay: true, - autoInitialize: true, - allowFullScreen: false, - allowedScreenSleep: false, - showControls: widget.showControls && !widget.isMotionVideo, + placeholder: SizedBox.expand(child: placeholder), + showControls: showControls && !isMotionVideo, + hideControlsTimer: hideControlsTimer, customControls: const VideoPlayerControls(), - hideControlsTimer: widget.hideControlsTimer, + onPlaying: onPlaying, + onPaused: onPaused, + onVideoEnded: onVideoEnded, + ); + + // Loading + return PopScope( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + child: Builder( + builder: (context) { + if (controller == null) { + return Stack( + children: [ + if (placeholder != null) SizedBox.expand(child: placeholder!), + const DelayedLoadingIndicator( + fadeInDuration: Duration(milliseconds: 500), + ), + ], + ); + } + + final size = MediaQuery.of(context).size; + return SizedBox( + height: size.height, + width: size.width, + child: Chewie( + controller: controller, + ), + ); + }, + ), + ), ); } - - @override - void dispose() { - super.dispose(); - videoPlayerController.pause(); - videoPlayerController.dispose(); - chewieController?.dispose(); - } - - @override - Widget build(BuildContext context) { - if (chewieController?.videoPlayerController.value.isInitialized == true) { - return SizedBox( - height: context.height, - width: context.width, - child: Chewie( - controller: chewieController!, - ), - ); - } else { - return SizedBox( - height: context.height, - width: context.width, - child: Center( - child: Stack( - children: [ - if (widget.placeholder != null) widget.placeholder!, - if (widget.showDownloadingIndicator) - const Center( - child: ImmichLoadingIndicator(), - ), - ], - ), - ), - ); - } - } } diff --git a/mobile/lib/modules/backup/providers/backup.provider.dart b/mobile/lib/modules/backup/providers/backup.provider.dart index a175a17de1..68c6bf9e66 100644 --- a/mobile/lib/modules/backup/providers/backup.provider.dart +++ b/mobile/lib/modules/backup/providers/backup.provider.dart @@ -245,7 +245,7 @@ class BackupNotifier extends StateNotifier { } catch (e, stack) { log.severe( "Failed to get thumbnail for album ${album.name}", - e.toString(), + e, stack, ); } 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 73b31617f1..a194bc2ade 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/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart index 23dcd50533..e010024332 100644 --- a/mobile/lib/modules/login/providers/authentication.provider.dart +++ b/mobile/lib/modules/login/providers/authentication.provider.dart @@ -108,7 +108,7 @@ class AuthenticationNotifier extends StateNotifier { .then((_) => log.info("Logout was successful for $userEmail")) .onError( (error, stackTrace) => - log.severe("Error logging out $userEmail", error, stackTrace), + log.severe("Logout failed for $userEmail", error, stackTrace), ); await Future.wait([ @@ -129,8 +129,8 @@ class AuthenticationNotifier extends StateNotifier { shouldChangePassword: false, isAuthenticated: false, ); - } catch (e) { - log.severe("Error logging out $e"); + } catch (e, stack) { + log.severe('Logout failed', e, stack); } } diff --git a/mobile/lib/modules/login/services/oauth.service.dart b/mobile/lib/modules/login/services/oauth.service.dart index 8f34c968eb..952c6fa8d6 100644 --- a/mobile/lib/modules/login/services/oauth.service.dart +++ b/mobile/lib/modules/login/services/oauth.service.dart @@ -36,7 +36,7 @@ class OAuthService { ), ); } catch (e, stack) { - log.severe("Error performing oAuthLogin: ${e.toString()}", e, stack); + log.severe("OAuth login failed", e, stack); return null; } } diff --git a/mobile/lib/modules/map/providers/map_state.provider.dart b/mobile/lib/modules/map/providers/map_state.provider.dart index de6265c233..f1d1a4dde4 100644 --- a/mobile/lib/modules/map/providers/map_state.provider.dart +++ b/mobile/lib/modules/map/providers/map_state.provider.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/modules/map/models/map_state.model.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; @@ -51,7 +52,8 @@ class MapStateNotifier extends _$MapStateNotifier { lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current), ); _log.severe( - "Cannot fetch map light style with status - ${lightResponse.statusCode} and response - ${lightResponse.body}", + "Cannot fetch map light style", + lightResponse.toLoggerString(), ); return; } @@ -77,9 +79,7 @@ class MapStateNotifier extends _$MapStateNotifier { state = state.copyWith( darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current), ); - _log.severe( - "Cannot fetch map dark style with status - ${darkResponse.statusCode} and response - ${darkResponse.body}", - ); + _log.severe("Cannot fetch map dark style", darkResponse.toLoggerString()); return; } diff --git a/mobile/lib/modules/map/services/map.service.dart b/mobile/lib/modules/map/services/map.service.dart index b3a904cbf1..0a5036056a 100644 --- a/mobile/lib/modules/map/services/map.service.dart +++ b/mobile/lib/modules/map/services/map.service.dart @@ -28,6 +28,7 @@ class MapSerivce with ErrorLoggerMixin { return markers?.map(MapMarker.fromDto) ?? []; }, defaultValue: [], + errorMessage: "Failed to get map markers", ); } } diff --git a/mobile/lib/modules/map/utils/map_utils.dart b/mobile/lib/modules/map/utils/map_utils.dart index 46af81ce1d..f6e8349f51 100644 --- a/mobile/lib/modules/map/utils/map_utils.dart +++ b/mobile/lib/modules/map/utils/map_utils.dart @@ -105,10 +105,8 @@ class MapUtils { timeLimit: const Duration(seconds: 5), ); return (currentUserLocation, null); - } catch (error) { - _log.severe( - "Cannot get user's current location due to ${error.toString()}", - ); + } catch (error, stack) { + _log.severe("Cannot get user's current location", error, stack); return (null, LocationPermission.unableToDetermine); } } diff --git a/mobile/lib/modules/map/widgets/map_asset_grid.dart b/mobile/lib/modules/map/widgets/map_asset_grid.dart index d1f187e258..ad90d36ed1 100644 --- a/mobile/lib/modules/map/widgets/map_asset_grid.dart +++ b/mobile/lib/modules/map/widgets/map_asset_grid.dart @@ -147,7 +147,7 @@ class MapAssetGrid extends HookConsumerWidget { }, error: (error, stackTrace) { log.warning( - "Cannot get assets in the current map bounds $error", + "Cannot get assets in the current map bounds", error, stackTrace, ); diff --git a/mobile/lib/modules/memories/services/memory.service.dart b/mobile/lib/modules/memories/services/memory.service.dart index 8d2cd226a4..8ee203e6c9 100644 --- a/mobile/lib/modules/memories/services/memory.service.dart +++ b/mobile/lib/modules/memories/services/memory.service.dart @@ -47,7 +47,7 @@ class MemoryService { return memories.isNotEmpty ? memories : null; } catch (error, stack) { - log.severe("Cannot get memories ${error.toString()}", error, stack); + log.severe("Cannot get memories", error, stack); return null; } } diff --git a/mobile/lib/modules/memories/ui/memory_card.dart b/mobile/lib/modules/memories/ui/memory_card.dart index 7c998e8f52..af57c272ae 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,27 +36,15 @@ 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) { // Determine the fit using the aspect ratio - BoxFit fit = BoxFit.fitWidth; + BoxFit fit = BoxFit.contain; if (asset.width != null && asset.height != null) { - final aspectRatio = asset.height! / asset.width!; + final aspectRatio = asset.width! / asset.height!; final phoneAspectRatio = constraints.maxWidth / constraints.maxHeight; // Look for a 25% difference in either direction @@ -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 199af835c9..9308e812dc 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/modules/partner/services/partner.service.dart b/mobile/lib/modules/partner/services/partner.service.dart index 32e500353b..d1e40076c7 100644 --- a/mobile/lib/modules/partner/services/partner.service.dart +++ b/mobile/lib/modules/partner/services/partner.service.dart @@ -40,7 +40,7 @@ class PartnerService { return userDtos.map((u) => User.fromPartnerDto(u)).toList(); } } catch (e) { - _log.warning("failed to get partners for direction $direction:\n$e"); + _log.warning("Failed to get partners for direction $direction", e); } return null; } @@ -51,7 +51,7 @@ class PartnerService { partner.isPartnerSharedBy = false; await _db.writeTxn(() => _db.users.put(partner)); } catch (e) { - _log.warning("failed to remove partner ${partner.id}:\n$e"); + _log.warning("Failed to remove partner ${partner.id}", e); return false; } return true; @@ -66,7 +66,7 @@ class PartnerService { return true; } } catch (e) { - _log.warning("failed to add partner ${partner.id}:\n$e"); + _log.warning("Failed to add partner ${partner.id}", e); } return false; } @@ -81,7 +81,7 @@ class PartnerService { return true; } } catch (e) { - _log.warning("failed to update partner ${partner.id}:\n$e"); + _log.warning("Failed to update partner ${partner.id}", e); } return false; } diff --git a/mobile/lib/modules/shared_link/services/shared_link.service.dart b/mobile/lib/modules/shared_link/services/shared_link.service.dart index 3ea1d411b2..62f431580c 100644 --- a/mobile/lib/modules/shared_link/services/shared_link.service.dart +++ b/mobile/lib/modules/shared_link/services/shared_link.service.dart @@ -22,7 +22,7 @@ class SharedLinkService { ? AsyncData(list.map(SharedLink.fromDto).toList()) : const AsyncData([]); } catch (e, stack) { - _log.severe("failed to fetch shared links - $e"); + _log.severe("Failed to fetch shared links", e, stack); return AsyncError(e, stack); } } @@ -31,7 +31,7 @@ class SharedLinkService { try { return await _apiService.sharedLinkApi.removeSharedLink(id); } catch (e) { - _log.severe("failed to delete shared link id - $id with error - $e"); + _log.severe("Failed to delete shared link id - $id", e); } } @@ -81,7 +81,7 @@ class SharedLinkService { } } } catch (e) { - _log.severe("failed to create shared link with error - $e"); + _log.severe("Failed to create shared link", e); } return null; } @@ -113,7 +113,7 @@ class SharedLinkService { return SharedLink.fromDto(responseDto); } } catch (e) { - _log.severe("failed to update shared link id - $id with error - $e"); + _log.severe("Failed to update shared link id - $id", e); } return null; } diff --git a/mobile/lib/modules/trash/providers/trashed_asset.provider.dart b/mobile/lib/modules/trash/providers/trashed_asset.provider.dart index 177e7d2d4c..165d1c0f74 100644 --- a/mobile/lib/modules/trash/providers/trashed_asset.provider.dart +++ b/mobile/lib/modules/trash/providers/trashed_asset.provider.dart @@ -44,7 +44,7 @@ class TrashNotifier extends StateNotifier { .read(syncServiceProvider) .handleRemoteAssetRemoval(idsToRemove.cast().toList()); } catch (error, stack) { - _log.severe("Cannot empty trash ${error.toString()}", error, stack); + _log.severe("Cannot empty trash", error, stack); } } @@ -70,7 +70,7 @@ class TrashNotifier extends StateNotifier { return isRemoved; } catch (error, stack) { - _log.severe("Cannot empty trash ${error.toString()}", error, stack); + _log.severe("Cannot remove assets", error, stack); } return false; } @@ -93,7 +93,7 @@ class TrashNotifier extends StateNotifier { return true; } } catch (error, stack) { - _log.severe("Cannot restore trash ${error.toString()}", error, stack); + _log.severe("Cannot restore assets", error, stack); } return false; } @@ -123,7 +123,7 @@ class TrashNotifier extends StateNotifier { await _db.assets.putAll(updatedAssets); }); } catch (error, stack) { - _log.severe("Cannot restore trash ${error.toString()}", error, stack); + _log.severe("Cannot restore trash", error, stack); } } } diff --git a/mobile/lib/modules/trash/services/trash.service.dart b/mobile/lib/modules/trash/services/trash.service.dart index 9a9ff5d0b6..96b07ca20f 100644 --- a/mobile/lib/modules/trash/services/trash.service.dart +++ b/mobile/lib/modules/trash/services/trash.service.dart @@ -25,7 +25,7 @@ class TrashService { await _apiService.trashApi.restoreAssets(BulkIdsDto(ids: remoteIds)); return true; } catch (error, stack) { - _log.severe("Cannot restore assets ${error.toString()}", error, stack); + _log.severe("Cannot restore assets", error, stack); return false; } } @@ -34,7 +34,7 @@ class TrashService { try { await _apiService.trashApi.emptyTrash(); } catch (error, stack) { - _log.severe("Cannot empty trash ${error.toString()}", error, stack); + _log.severe("Cannot empty trash", error, stack); } } @@ -42,7 +42,7 @@ class TrashService { try { await _apiService.trashApi.restoreTrash(); } catch (error, stack) { - _log.severe("Cannot restore trash ${error.toString()}", error, stack); + _log.severe("Cannot restore trash", error, stack); } } } diff --git a/mobile/lib/routing/auth_guard.dart b/mobile/lib/routing/auth_guard.dart index 6aee9271f4..fe212c4ca9 100644 --- a/mobile/lib/routing/auth_guard.dart +++ b/mobile/lib/routing/auth_guard.dart @@ -1,8 +1,8 @@ import 'dart:io'; import 'package:auto_route/auto_route.dart'; -import 'package:flutter/foundation.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; @@ -16,28 +16,31 @@ class AuthGuard extends AutoRouteGuard { resolver.next(true); try { - var res = await _apiService.authenticationApi.validateAccessToken(); + // Look in the store for an access token + Store.get(StoreKey.accessToken); + + // Validate the access token with the server + final res = await _apiService.authenticationApi.validateAccessToken(); if (res == null || res.authStatus != true) { // If the access token is invalid, take user back to login - _log.fine("User token is invalid. Redirecting to login"); + _log.fine('User token is invalid. Redirecting to login'); router.replaceAll([const LoginRoute()]); } + } on StoreKeyNotFoundException catch (_) { + // If there is no access token, take us to the login page + _log.warning('No access token in the store.'); + router.replaceAll([const LoginRoute()]); + return; } on ApiException catch (e) { - if (e.code == HttpStatus.badRequest && - e.innerException is SocketException) { - // offline? - _log.fine( - "Unable to validate user token. User may be offline and offline browsing is allowed.", - ); - } else { - debugPrint("Error [onNavigation] ${e.toString()}"); + // On an unauthorized request, take us to the login page + if (e.code == HttpStatus.unauthorized) { + _log.warning("Unauthorized access token."); router.replaceAll([const LoginRoute()]); return; } } catch (e) { - debugPrint("Error [onNavigation] ${e.toString()}"); - router.replaceAll([const LoginRoute()]); - return; + // Otherwise, this is not fatal, but we still log the warning + _log.warning('Error validating access token from server: $e'); } } } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index f6968dafe5..16ac5efb0e 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1393,7 +1393,7 @@ class VideoViewerRoute extends PageRouteInfo { void Function()? onPaused, Widget? placeholder, bool showControls = true, - Duration hideControlsTimer = const Duration(seconds: 5), + Duration hideControlsTimer = const Duration(milliseconds: 1500), bool showDownloadingIndicator = true, List? children, }) : super( @@ -1429,7 +1429,7 @@ class VideoViewerRouteArgs { this.onPaused, this.placeholder, this.showControls = true, - this.hideControlsTimer = const Duration(seconds: 5), + this.hideControlsTimer = const Duration(milliseconds: 1500), this.showDownloadingIndicator = true, }); diff --git a/mobile/lib/shared/models/asset.dart b/mobile/lib/shared/models/asset.dart index afd49adc6a..3c3c4df82f 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; @@ -171,6 +175,11 @@ class Asset { int? stackCount; + /// Aspect ratio of the asset + @ignore + double? get aspectRatio => + width == null || height == null ? 0 : width! / height!; + /// `true` if this [Asset] is present on the device @ignore bool get isLocal => localId != null; @@ -274,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 && @@ -338,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) @@ -374,6 +385,7 @@ class Asset { ExifInfo? exifInfo, String? stackParentId, int? stackCount, + String? thumbhash, }) => Asset( id: id ?? this.id, @@ -398,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 d845b5353a..5912f291b5 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/models/logger_message.model.dart b/mobile/lib/shared/models/logger_message.model.dart index cb1d45a580..f657257eab 100644 --- a/mobile/lib/shared/models/logger_message.model.dart +++ b/mobile/lib/shared/models/logger_message.model.dart @@ -9,6 +9,7 @@ part 'logger_message.model.g.dart'; class LoggerMessage { Id id = Isar.autoIncrement; String message; + String? details; @Enumerated(EnumType.ordinal) LogLevel level = LogLevel.INFO; DateTime createdAt; @@ -17,6 +18,7 @@ class LoggerMessage { LoggerMessage({ required this.message, + required this.details, required this.level, required this.createdAt, required this.context1, diff --git a/mobile/lib/shared/models/logger_message.model.g.dart b/mobile/lib/shared/models/logger_message.model.g.dart index a6b960eece..76c823704c 100644 --- a/mobile/lib/shared/models/logger_message.model.g.dart +++ b/mobile/lib/shared/models/logger_message.model.g.dart @@ -32,14 +32,19 @@ const LoggerMessageSchema = CollectionSchema( name: r'createdAt', type: IsarType.dateTime, ), - r'level': PropertySchema( + r'details': PropertySchema( id: 3, + name: r'details', + type: IsarType.string, + ), + r'level': PropertySchema( + id: 4, name: r'level', type: IsarType.byte, enumMap: _LoggerMessagelevelEnumValueMap, ), r'message': PropertySchema( - id: 4, + id: 5, name: r'message', type: IsarType.string, ) @@ -76,6 +81,12 @@ int _loggerMessageEstimateSize( bytesCount += 3 + value.length * 3; } } + { + final value = object.details; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.message.length * 3; return bytesCount; } @@ -89,8 +100,9 @@ void _loggerMessageSerialize( writer.writeString(offsets[0], object.context1); writer.writeString(offsets[1], object.context2); writer.writeDateTime(offsets[2], object.createdAt); - writer.writeByte(offsets[3], object.level.index); - writer.writeString(offsets[4], object.message); + writer.writeString(offsets[3], object.details); + writer.writeByte(offsets[4], object.level.index); + writer.writeString(offsets[5], object.message); } LoggerMessage _loggerMessageDeserialize( @@ -103,9 +115,10 @@ LoggerMessage _loggerMessageDeserialize( context1: reader.readStringOrNull(offsets[0]), context2: reader.readStringOrNull(offsets[1]), createdAt: reader.readDateTime(offsets[2]), - level: _LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offsets[3])] ?? + details: reader.readStringOrNull(offsets[3]), + level: _LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offsets[4])] ?? LogLevel.ALL, - message: reader.readString(offsets[4]), + message: reader.readString(offsets[5]), ); object.id = id; return object; @@ -125,9 +138,11 @@ P _loggerMessageDeserializeProp

( case 2: return (reader.readDateTime(offset)) as P; case 3: + return (reader.readStringOrNull(offset)) as P; + case 4: return (_LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offset)] ?? LogLevel.ALL) as P; - case 4: + case 5: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -619,6 +634,160 @@ extension LoggerMessageQueryFilter }); } + QueryBuilder + detailsIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'details', + )); + }); + } + + QueryBuilder + detailsIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'details', + )); + }); + } + + QueryBuilder + detailsEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'details', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + detailsGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'details', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + detailsLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'details', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + detailsBetween( + 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'details', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + detailsStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'details', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + detailsEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'details', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + detailsContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'details', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + detailsMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'details', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + detailsIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'details', + value: '', + )); + }); + } + + QueryBuilder + detailsIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'details', + value: '', + )); + }); + } + QueryBuilder idEqualTo( Id value) { return QueryBuilder.apply(this, (query) { @@ -913,6 +1082,18 @@ extension LoggerMessageQuerySortBy }); } + QueryBuilder sortByDetails() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'details', Sort.asc); + }); + } + + QueryBuilder sortByDetailsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'details', Sort.desc); + }); + } + QueryBuilder sortByLevel() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'level', Sort.asc); @@ -979,6 +1160,18 @@ extension LoggerMessageQuerySortThenBy }); } + QueryBuilder thenByDetails() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'details', Sort.asc); + }); + } + + QueryBuilder thenByDetailsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'details', Sort.desc); + }); + } + QueryBuilder thenById() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'id', Sort.asc); @@ -1038,6 +1231,13 @@ extension LoggerMessageQueryWhereDistinct }); } + QueryBuilder distinctByDetails( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'details', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByLevel() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'level'); @@ -1078,6 +1278,12 @@ extension LoggerMessageQueryProperty }); } + QueryBuilder detailsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'details'); + }); + } + QueryBuilder levelProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'level'); diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart index 64a0f28ab7..3086ab9246 100644 --- a/mobile/lib/shared/services/asset.service.dart +++ b/mobile/lib/shared/services/asset.service.dart @@ -90,7 +90,7 @@ class AssetService { return allAssets; } catch (error, stack) { log.severe( - 'Error while getting remote assets: ${error.toString()}', + 'Error while getting remote assets', error, stack, ); @@ -117,7 +117,7 @@ class AssetService { ); return true; } catch (error, stack) { - log.severe("Error deleteAssets ${error.toString()}", error, stack); + log.severe("Error while deleting assets", error, stack); } return false; } diff --git a/mobile/lib/shared/services/immich_logger.service.dart b/mobile/lib/shared/services/immich_logger.service.dart index b66177e570..967ab2d5f2 100644 --- a/mobile/lib/shared/services/immich_logger.service.dart +++ b/mobile/lib/shared/services/immich_logger.service.dart @@ -12,7 +12,7 @@ import 'package:share_plus/share_plus.dart'; /// [ImmichLogger] is a custom logger that is built on top of the [logging] package. /// The logs are written to the database and onto console, using `debugPrint` method. /// -/// The logs are deleted when exceeding the `maxLogEntries` (default 200) property +/// The logs are deleted when exceeding the `maxLogEntries` (default 500) property /// in the class. /// /// Logs can be shared by calling the `shareLogs` method, which will open a share dialog @@ -58,6 +58,7 @@ class ImmichLogger { debugPrint('[${record.level.name}] [${record.time}] ${record.message}'); final lm = LoggerMessage( message: record.message, + details: record.error?.toString(), level: record.level.toLogLevel(), createdAt: record.time, context1: record.loggerName, diff --git a/mobile/lib/shared/services/share.service.dart b/mobile/lib/shared/services/share.service.dart index d7daa51b86..be7c0c168d 100644 --- a/mobile/lib/shared/services/share.service.dart +++ b/mobile/lib/shared/services/share.service.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/response_extensions.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/api.provider.dart'; import 'package:logging/logging.dart'; @@ -41,7 +42,8 @@ class ShareService { if (res.statusCode != 200) { _log.severe( - "Asset download failed with status - ${res.statusCode} and response - ${res.body}", + "Asset download for ${asset.fileName} failed", + res.toLoggerString(), ); continue; } @@ -68,7 +70,7 @@ class ShareService { ); return true; } catch (error) { - _log.severe("Share failed with error $error"); + _log.severe("Share failed", error); } return false; } diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart index d039b34094..a441091d37 100644 --- a/mobile/lib/shared/services/sync.service.dart +++ b/mobile/lib/shared/services/sync.service.dart @@ -140,7 +140,7 @@ class SyncService { try { await _db.writeTxn(() => a.put(_db)); } on IsarError catch (e) { - _log.severe("Failed to put new asset into db: $e"); + _log.severe("Failed to put new asset into db", e); return false; } return true; @@ -173,7 +173,7 @@ class SyncService { } return false; } on IsarError catch (e) { - _log.severe("Failed to sync remote assets to db: $e"); + _log.severe("Failed to sync remote assets to db", e); } return null; } @@ -232,7 +232,7 @@ class SyncService { await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete)); await upsertAssetsWithExif(toAdd + toUpdate); } on IsarError catch (e) { - _log.severe("Failed to sync remote assets to db: $e"); + _log.severe("Failed to sync remote assets to db", e); } await _updateUserAssetsETag(user, now); return true; @@ -364,7 +364,7 @@ class SyncService { }); _log.info("Synced changes of remote album ${album.name} to DB"); } on IsarError catch (e) { - _log.severe("Failed to sync remote album to database $e"); + _log.severe("Failed to sync remote album to database", e); } if (album.shared || dto.shared) { @@ -441,7 +441,7 @@ class SyncService { assert(ok); _log.info("Removed local album $album from DB"); } catch (e) { - _log.severe("Failed to remove local album $album from DB"); + _log.severe("Failed to remove local album $album from DB", e); } } @@ -577,7 +577,7 @@ class SyncService { }); _log.info("Synced changes of local album ${ape.name} to DB"); } on IsarError catch (e) { - _log.severe("Failed to update synced album ${ape.name} in DB: $e"); + _log.severe("Failed to update synced album ${ape.name} in DB", e); } return true; @@ -623,7 +623,7 @@ class SyncService { }); _log.info("Fast synced local album ${ape.name} to DB"); } on IsarError catch (e) { - _log.severe("Failed to fast sync local album ${ape.name} to DB: $e"); + _log.severe("Failed to fast sync local album ${ape.name} to DB", e); return false; } @@ -656,7 +656,7 @@ class SyncService { await _db.writeTxn(() => _db.albums.store(a)); _log.info("Added a new local album to DB: ${ape.name}"); } on IsarError catch (e) { - _log.severe("Failed to add new local album ${ape.name} to DB: $e"); + _log.severe("Failed to add new local album ${ape.name} to DB", e); } } @@ -706,9 +706,7 @@ class SyncService { }); _log.info("Upserted ${assets.length} assets into the DB"); } on IsarError catch (e) { - _log.severe( - "Failed to upsert ${assets.length} assets into the DB: ${e.toString()}", - ); + _log.severe("Failed to upsert ${assets.length} assets into the DB", e); // give details on the errors assets.sort(Asset.compareByOwnerChecksum); final inDb = await _db.assets.getAllByOwnerIdChecksum( @@ -776,7 +774,7 @@ class SyncService { }); return true; } catch (e) { - _log.severe("Failed to remove all local albums and assets: $e"); + _log.severe("Failed to remove all local albums and assets", e); return false; } } diff --git a/mobile/lib/shared/services/user.service.dart b/mobile/lib/shared/services/user.service.dart index 4d398c3a88..ae65ed31db 100644 --- a/mobile/lib/shared/services/user.service.dart +++ b/mobile/lib/shared/services/user.service.dart @@ -42,7 +42,7 @@ class UserService { final dto = await _apiService.userApi.getAllUsers(isAll); return dto?.map(User.fromUserDto).toList(); } catch (e) { - _log.warning("Failed get all users:\n$e"); + _log.warning("Failed get all users", e); return null; } } @@ -65,7 +65,7 @@ class UserService { ), ); } catch (e) { - _log.warning("Failed to upload profile image:\n$e"); + _log.warning("Failed to upload profile image", e); return null; } } diff --git a/mobile/lib/shared/ui/delayed_loading_indicator.dart b/mobile/lib/shared/ui/delayed_loading_indicator.dart new file mode 100644 index 0000000000..b4d9f4c806 --- /dev/null +++ b/mobile/lib/shared/ui/delayed_loading_indicator.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; + +class DelayedLoadingIndicator extends StatelessWidget { + /// The delay to avoid showing the loading indicator + final Duration delay; + + /// Defaults to using the [ImmichLoadingIndicator] + final Widget? child; + + /// An optional fade in duration to animate the loading + final Duration? fadeInDuration; + + const DelayedLoadingIndicator({ + super.key, + this.delay = const Duration(seconds: 3), + this.child, + this.fadeInDuration, + }); + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: fadeInDuration ?? Duration.zero, + child: FutureBuilder( + future: Future.delayed(delay), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return child ?? + const ImmichLoadingIndicator( + key: ValueKey('loading'), + ); + } + + return Container(key: const ValueKey('hiding')); + }, + ), + ); + } +} 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 0000000000..e0620ea4f0 --- /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 0000000000..24b3c25e13 --- /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 280f7de170..3137f63014 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 0000000000..fe35bdaac2 --- /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 0000000000..0ec64d3760 --- /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/lib/shared/views/app_log_detail_page.dart b/mobile/lib/shared/views/app_log_detail_page.dart index 126f46c8ff..6b99d7f0af 100644 --- a/mobile/lib/shared/views/app_log_detail_page.dart +++ b/mobile/lib/shared/views/app_log_detail_page.dart @@ -15,7 +15,7 @@ class AppLogDetailPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { var isDarkTheme = context.isDarkTheme; - buildStackMessage(String stackTrace) { + buildTextWithCopyButton(String header, String text) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( @@ -28,7 +28,7 @@ class AppLogDetailPage extends HookConsumerWidget { Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text( - "STACK TRACES", + header, style: TextStyle( fontSize: 12.0, color: context.primaryColor, @@ -38,8 +38,7 @@ class AppLogDetailPage extends HookConsumerWidget { ), IconButton( onPressed: () { - Clipboard.setData(ClipboardData(text: stackTrace)) - .then((_) { + Clipboard.setData(ClipboardData(text: text)).then((_) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( @@ -68,73 +67,7 @@ class AppLogDetailPage extends HookConsumerWidget { child: Padding( padding: const EdgeInsets.all(8.0), child: SelectableText( - stackTrace, - style: const TextStyle( - fontSize: 12.0, - fontWeight: FontWeight.bold, - fontFamily: "Inconsolata", - ), - ), - ), - ), - ], - ), - ); - } - - buildLogMessage(String message) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "MESSAGE", - style: TextStyle( - fontSize: 12.0, - color: context.primaryColor, - fontWeight: FontWeight.bold, - ), - ), - ), - IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: message)).then((_) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - "Copied to clipboard", - style: context.textTheme.bodyLarge?.copyWith( - color: context.primaryColor, - ), - ), - ), - ); - }); - }, - icon: Icon( - Icons.copy, - size: 16.0, - color: context.primaryColor, - ), - ), - ], - ), - Container( - decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], - borderRadius: BorderRadius.circular(15.0), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SelectableText( - message, + text, style: const TextStyle( fontSize: 12.0, fontWeight: FontWeight.bold, @@ -194,11 +127,16 @@ class AppLogDetailPage extends HookConsumerWidget { body: SafeArea( child: ListView( children: [ - buildLogMessage(logMessage.message), + buildTextWithCopyButton("MESSAGE", logMessage.message), + if (logMessage.details != null) + buildTextWithCopyButton("DETAILS", logMessage.details.toString()), if (logMessage.context1 != null) buildLogContext1(logMessage.context1.toString()), if (logMessage.context2 != null) - buildStackMessage(logMessage.context2.toString()), + buildTextWithCopyButton( + "STACK TRACE", + logMessage.context2.toString(), + ), ], ), ), diff --git a/mobile/lib/shared/views/app_log_page.dart b/mobile/lib/shared/views/app_log_page.dart index a0c4553f98..993b25c7cf 100644 --- a/mobile/lib/shared/views/app_log_page.dart +++ b/mobile/lib/shared/views/app_log_page.dart @@ -69,9 +69,9 @@ class AppLogPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: Text( - "Logs - ${logMessages.value.length}", - style: const TextStyle( + title: const Text( + "Logs", + style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16.0, ), @@ -135,29 +135,15 @@ class AppLogPage extends HookConsumerWidget { dense: true, tileColor: getTileColor(logMessage.level), minLeadingWidth: 10, - title: Text.rich( - TextSpan( - children: [ - TextSpan( - text: "#$index ", - style: TextStyle( - color: isDarkTheme ? Colors.white70 : Colors.grey[600], - fontSize: 14.0, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: truncateLogMessage(logMessage.message, 4), - style: const TextStyle( - fontSize: 14.0, - ), - ), - ], + title: Text( + truncateLogMessage(logMessage.message, 4), + style: const TextStyle( + fontSize: 14.0, + fontFamily: "Inconsolata", ), - style: const TextStyle(fontSize: 14.0, fontFamily: "Inconsolata"), ), subtitle: Text( - "[${logMessage.context1}] Logged on ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}", + "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}", style: TextStyle( fontSize: 12.0, color: Colors.grey[600], diff --git a/mobile/lib/shared/views/immich_loading_overlay.dart b/mobile/lib/shared/views/immich_loading_overlay.dart index 85f0123ed9..c600d2a724 100644 --- a/mobile/lib/shared/views/immich_loading_overlay.dart +++ b/mobile/lib/shared/views/immich_loading_overlay.dart @@ -1,7 +1,7 @@ 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/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart'; final _loadingEntry = OverlayEntry( builder: (context) => SizedBox.square( @@ -9,7 +9,12 @@ final _loadingEntry = OverlayEntry( child: DecoratedBox( decoration: BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), - child: const Center(child: ImmichLoadingIndicator()), + child: const Center( + child: DelayedLoadingIndicator( + delay: Duration(seconds: 1), + fadeInDuration: Duration(milliseconds: 400), + ), + ), ), ), ); @@ -27,19 +32,19 @@ class _LoadingOverlay extends Hook> { class _LoadingOverlayState extends HookState, _LoadingOverlay> { - late final _isProcessing = ValueNotifier(false)..addListener(_listener); - OverlayEntry? overlayEntry; + late final _isLoading = ValueNotifier(false)..addListener(_listener); + OverlayEntry? _loadingOverlay; void _listener() { setState(() { WidgetsBinding.instance.addPostFrameCallback((_) { - if (_isProcessing.value) { - overlayEntry?.remove(); - overlayEntry = _loadingEntry; + if (_isLoading.value) { + _loadingOverlay?.remove(); + _loadingOverlay = _loadingEntry; Overlay.of(context).insert(_loadingEntry); } else { - overlayEntry?.remove(); - overlayEntry = null; + _loadingOverlay?.remove(); + _loadingOverlay = null; } }); }); @@ -47,17 +52,17 @@ class _LoadingOverlayState @override ValueNotifier build(BuildContext context) { - return _isProcessing; + return _isLoading; } @override void dispose() { - _isProcessing.dispose(); + _isLoading.dispose(); super.dispose(); } @override - Object? get debugValue => _isProcessing.value; + Object? get debugValue => _isLoading.value; @override String get debugLabel => 'useProcessingOverlay<>'; diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart index 8dddb60aaa..3c0d65bde9 100644 --- a/mobile/lib/shared/views/splash_screen.dart +++ b/mobile/lib/shared/views/splash_screen.dart @@ -35,10 +35,10 @@ class SplashScreenPage extends HookConsumerWidget { deviceIsOffline = true; log.fine("Device seems to be offline upon launch"); } else { - log.severe(e); + log.severe("Failed to resolve endpoint", e); } } catch (e) { - log.severe(e); + log.severe("Failed to resolve endpoint", e); } try { @@ -53,7 +53,7 @@ class SplashScreenPage extends HookConsumerWidget { ref.read(authenticationProvider.notifier).logout(); log.severe( - 'Cannot set success login info: $error', + 'Cannot set success login info', error, stackTrace, ); diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 0679a1749d..ea413b4870 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -108,6 +108,7 @@ doc/PersonResponseDto.md doc/PersonStatisticsResponseDto.md doc/PersonUpdateDto.md doc/PersonWithFacesResponseDto.md +doc/PlacesResponseDto.md doc/QueueStatusDto.md doc/ReactionLevel.md doc/ReactionType.md @@ -308,6 +309,7 @@ lib/model/person_response_dto.dart lib/model/person_statistics_response_dto.dart lib/model/person_update_dto.dart lib/model/person_with_faces_response_dto.dart +lib/model/places_response_dto.dart lib/model/queue_status_dto.dart lib/model/reaction_level.dart lib/model/reaction_type.dart @@ -485,6 +487,7 @@ test/person_response_dto_test.dart test/person_statistics_response_dto_test.dart test/person_update_dto_test.dart test/person_with_faces_response_dto_test.dart +test/places_response_dto_test.dart test/queue_status_dto_test.dart test/reaction_level_test.dart test/reaction_type_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 7bda432fe5..71b2c6056c 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -166,6 +166,7 @@ Class | Method | HTTP request | Description *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | *SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata | *SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | +*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places | *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config | *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | @@ -306,6 +307,7 @@ Class | Method | HTTP request | Description - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) - [PersonUpdateDto](doc//PersonUpdateDto.md) - [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md) + - [PlacesResponseDto](doc//PlacesResponseDto.md) - [QueueStatusDto](doc//QueueStatusDto.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) diff --git a/mobile/openapi/doc/PlacesResponseDto.md b/mobile/openapi/doc/PlacesResponseDto.md new file mode 100644 index 0000000000..a4bf36493c --- /dev/null +++ b/mobile/openapi/doc/PlacesResponseDto.md @@ -0,0 +1,19 @@ +# openapi.model.PlacesResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**admin1name** | **String** | | [optional] +**admin2name** | **String** | | [optional] +**latitude** | **num** | | +**longitude** | **num** | | +**name** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md index f975e94484..f63488222b 100644 --- a/mobile/openapi/doc/SearchApi.md +++ b/mobile/openapi/doc/SearchApi.md @@ -14,6 +14,7 @@ Method | HTTP request | Description [**search**](SearchApi.md#search) | **GET** /search | [**searchMetadata**](SearchApi.md#searchmetadata) | **POST** /search/metadata | [**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person | +[**searchPlaces**](SearchApi.md#searchplaces) | **GET** /search/places | [**searchSmart**](SearchApi.md#searchsmart) | **POST** /search/smart | @@ -316,6 +317,61 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **searchPlaces** +> List searchPlaces(name) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = SearchApi(); +final name = name_example; // String | + +try { + final result = api_instance.searchPlaces(name); + print(result); +} catch (e) { + print('Exception when calling SearchApi->searchPlaces: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **name** | **String**| | + +### Return type + +[**List**](PlacesResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **searchSmart** > SearchResponseDto searchSmart(smartSearchDto) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 72a6567648..56bd907e0a 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -142,6 +142,7 @@ part 'model/person_response_dto.dart'; part 'model/person_statistics_response_dto.dart'; part 'model/person_update_dto.dart'; part 'model/person_with_faces_response_dto.dart'; +part 'model/places_response_dto.dart'; part 'model/queue_status_dto.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 062ca4a50b..3a0bc56bb6 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -360,6 +360,58 @@ class SearchApi { return null; } + /// Performs an HTTP 'GET /search/places' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] name (required): + Future searchPlacesWithHttpInfo(String name,) async { + // ignore: prefer_const_declarations + final path = r'/search/places'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + queryParams.addAll(_queryParams('', 'name', name)); + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] name (required): + Future?> searchPlaces(String name,) async { + final response = await searchPlacesWithHttpInfo(name,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Performs an HTTP 'POST /search/smart' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 2df5e67119..24cffb7cff 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -366,6 +366,8 @@ class ApiClient { return PersonUpdateDto.fromJson(value); case 'PersonWithFacesResponseDto': return PersonWithFacesResponseDto.fromJson(value); + case 'PlacesResponseDto': + return PlacesResponseDto.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); case 'ReactionLevel': diff --git a/mobile/openapi/lib/model/places_response_dto.dart b/mobile/openapi/lib/model/places_response_dto.dart new file mode 100644 index 0000000000..a2d8378883 --- /dev/null +++ b/mobile/openapi/lib/model/places_response_dto.dart @@ -0,0 +1,148 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PlacesResponseDto { + /// Returns a new [PlacesResponseDto] instance. + PlacesResponseDto({ + this.admin1name, + this.admin2name, + required this.latitude, + required this.longitude, + required this.name, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? admin1name; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? admin2name; + + num latitude; + + num longitude; + + String name; + + @override + bool operator ==(Object other) => identical(this, other) || other is PlacesResponseDto && + other.admin1name == admin1name && + other.admin2name == admin2name && + other.latitude == latitude && + other.longitude == longitude && + other.name == name; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (admin1name == null ? 0 : admin1name!.hashCode) + + (admin2name == null ? 0 : admin2name!.hashCode) + + (latitude.hashCode) + + (longitude.hashCode) + + (name.hashCode); + + @override + String toString() => 'PlacesResponseDto[admin1name=$admin1name, admin2name=$admin2name, latitude=$latitude, longitude=$longitude, name=$name]'; + + Map toJson() { + final json = {}; + if (this.admin1name != null) { + json[r'admin1name'] = this.admin1name; + } else { + // json[r'admin1name'] = null; + } + if (this.admin2name != null) { + json[r'admin2name'] = this.admin2name; + } else { + // json[r'admin2name'] = null; + } + json[r'latitude'] = this.latitude; + json[r'longitude'] = this.longitude; + json[r'name'] = this.name; + return json; + } + + /// Returns a new [PlacesResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PlacesResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PlacesResponseDto( + admin1name: mapValueOfType(json, r'admin1name'), + admin2name: mapValueOfType(json, r'admin2name'), + latitude: num.parse('${json[r'latitude']}'), + longitude: num.parse('${json[r'longitude']}'), + name: mapValueOfType(json, r'name')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PlacesResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PlacesResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PlacesResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PlacesResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'latitude', + 'longitude', + 'name', + }; +} + diff --git a/mobile/openapi/test/places_response_dto_test.dart b/mobile/openapi/test/places_response_dto_test.dart new file mode 100644 index 0000000000..5a320fce64 --- /dev/null +++ b/mobile/openapi/test/places_response_dto_test.dart @@ -0,0 +1,47 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for PlacesResponseDto +void main() { + // final instance = PlacesResponseDto(); + + group('test PlacesResponseDto', () { + // String admin1name + test('to test the property `admin1name`', () async { + // TODO + }); + + // String admin2name + test('to test the property `admin2name`', () async { + // TODO + }); + + // num latitude + test('to test the property `latitude`', () async { + // TODO + }); + + // num longitude + test('to test the property `longitude`', () async { + // TODO + }); + + // String name + test('to test the property `name`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart index 14169e461d..aa4a94847b 100644 --- a/mobile/openapi/test/search_api_test.dart +++ b/mobile/openapi/test/search_api_test.dart @@ -42,6 +42,11 @@ void main() { // TODO }); + //Future> searchPlaces(String name) async + test('test searchPlaces', () async { + // TODO + }); + //Future searchSmart(SmartSearchDto smartSearchDto) async test('test searchSmart', () async { // TODO diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index ffa57f826b..9e379d4653 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -413,10 +413,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_selector_linux: dependency: transitive description: @@ -569,10 +569,10 @@ packages: dependency: "direct main" description: name: flutter_udid - sha256: "666412097b86d9a6f9803073d0f0ba70de9b198fe6493d89d352a1f8cd6c5c84" + sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" flutter_web_auth: dependency: "direct main" description: @@ -619,10 +619,10 @@ packages: dependency: "direct main" description: name: geolocator - sha256: e946395fc608842bb2f6c914807e9183f86f3cb787f6b8f832753e5251036f02 + sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd" url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "11.0.0" geolocator_android: dependency: transitive description: @@ -651,10 +651,10 @@ packages: dependency: transitive description: name: geolocator_web - sha256: "59083f7e0871b78299918d92bf930a14377f711d2d1156c558cd5ebae6c20d58" + sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.0" geolocator_windows: dependency: transitive description: @@ -860,6 +860,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -907,18 +931,18 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: "direct overridden" description: @@ -1002,10 +1026,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_provider: dependency: "direct main" description: @@ -1138,10 +1162,10 @@ packages: dependency: transitive description: name: platform - sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: @@ -1170,10 +1194,10 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.2" provider: dependency: transitive description: @@ -1298,10 +1322,10 @@ packages: dependency: transitive description: name: shared_preferences_linux - sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: @@ -1322,10 +1346,10 @@ packages: dependency: transitive description: name: shared_preferences_windows - sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shelf: dependency: transitive description: @@ -1467,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: @@ -1631,10 +1663,10 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" wakelock_plus: dependency: "direct main" description: @@ -1679,10 +1711,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" win32: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 47a4d3805e..50d170904f 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -32,8 +32,8 @@ dependencies: git: url: https://github.com/maplibre/flutter-maplibre-gl.git ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - geolocator: ^10.1.0 # used to move to current location in map view - flutter_udid: ^2.1.1 + geolocator: ^11.0.0 # used to move to current location in map view + flutter_udid: ^3.0.0 package_info_plus: ^5.0.1 url_launcher: ^6.2.4 http: 0.13.5 @@ -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/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 287daa69ad..c08fa73d3c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4700,6 +4700,50 @@ ] } }, + "/search/places": { + "get": { + "operationId": "searchPlaces", + "parameters": [ + { + "name": "name", + "required": true, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/PlacesResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, "/search/smart": { "post": { "operationId": "searchSmart", @@ -8760,6 +8804,31 @@ ], "type": "object" }, + "PlacesResponseDto": { + "properties": { + "admin1name": { + "type": "string" + }, + "admin2name": { + "type": "string" + }, + "latitude": { + "type": "number" + }, + "longitude": { + "type": "number" + }, + "name": { + "type": "string" + } + }, + "required": [ + "latitude", + "longitude", + "name" + ], + "type": "object" + }, "QueueStatusDto": { "properties": { "isActive": { diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts index dd65064a1b..73b0523f4b 100644 --- a/open-api/typescript-sdk/axios-client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -2988,6 +2988,43 @@ export interface PersonWithFacesResponseDto { */ 'thumbnailPath': string; } +/** + * + * @export + * @interface PlacesResponseDto + */ +export interface PlacesResponseDto { + /** + * + * @type {string} + * @memberof PlacesResponseDto + */ + 'admin1name'?: string; + /** + * + * @type {string} + * @memberof PlacesResponseDto + */ + 'admin2name'?: string; + /** + * + * @type {number} + * @memberof PlacesResponseDto + */ + 'latitude': number; + /** + * + * @type {number} + * @memberof PlacesResponseDto + */ + 'longitude': number; + /** + * + * @type {string} + * @memberof PlacesResponseDto + */ + 'name': string; +} /** * * @export @@ -15451,6 +15488,51 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} name + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchPlaces: async (name: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'name' is not null or undefined + assertParamExists('searchPlaces', 'name', name) + const localVarPath = `/search/places`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + if (name !== undefined) { + localVarQueryParameter['name'] = name; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -15588,6 +15670,18 @@ export const SearchApiFp = function(configuration?: Configuration) { const operationBasePath = operationServerMap['SearchApi.searchPerson']?.[index]?.url; return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); }, + /** + * + * @param {string} name + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async searchPlaces(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.searchPlaces(name, options); + const index = configuration?.serverIndex ?? 0; + const operationBasePath = operationServerMap['SearchApi.searchPlaces']?.[index]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath); + }, /** * * @param {SmartSearchDto} smartSearchDto @@ -15655,6 +15749,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: RawAxiosRequestConfig): AxiosPromise> { return localVarFp.searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {SearchApiSearchPlacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + searchPlaces(requestParameters: SearchApiSearchPlacesRequest, options?: RawAxiosRequestConfig): AxiosPromise> { + return localVarFp.searchPlaces(requestParameters.name, options).then((request) => request(axios, basePath)); + }, /** * * @param {SearchApiSearchSmartRequest} requestParameters Request parameters. @@ -15821,6 +15924,20 @@ export interface SearchApiSearchPersonRequest { readonly withHidden?: boolean } +/** + * Request parameters for searchPlaces operation in SearchApi. + * @export + * @interface SearchApiSearchPlacesRequest + */ +export interface SearchApiSearchPlacesRequest { + /** + * + * @type {string} + * @memberof SearchApiSearchPlaces + */ + readonly name: string +} + /** * Request parameters for searchSmart operation in SearchApi. * @export @@ -15897,6 +16014,17 @@ export class SearchApi extends BaseAPI { return SearchApiFp(this.configuration).searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {SearchApiSearchPlacesRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof SearchApi + */ + public searchPlaces(requestParameters: SearchApiSearchPlacesRequest, options?: RawAxiosRequestConfig) { + return SearchApiFp(this.configuration).searchPlaces(requestParameters.name, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {SearchApiSearchSmartRequest} requestParameters Request parameters. diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts index 50dc680e1a..7712e58990 100644 --- a/open-api/typescript-sdk/fetch-client.ts +++ b/open-api/typescript-sdk/fetch-client.ts @@ -645,6 +645,13 @@ export type MetadataSearchDto = { withPeople?: boolean; withStacked?: boolean; }; +export type PlacesResponseDto = { + admin1name?: string; + admin2name?: string; + latitude: number; + longitude: number; + name: string; +}; export type SmartSearchDto = { city?: string; country?: string; @@ -2199,6 +2206,18 @@ export function searchPerson({ name, withHidden }: { ...opts })); } +export function searchPlaces({ name }: { + name: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: PlacesResponseDto[]; + }>(`/search/places${QS.query(QS.explode({ + name + }))}`, { + ...opts + })); +} export function searchSmart({ smartSearchDto }: { smartSearchDto: SmartSearchDto; }, opts?: Oazapfts.RequestOpts) { diff --git a/open-api/typescript-sdk/fetch-errors.ts b/open-api/typescript-sdk/fetch-errors.ts new file mode 100644 index 0000000000..f21f0ed1c4 --- /dev/null +++ b/open-api/typescript-sdk/fetch-errors.ts @@ -0,0 +1,15 @@ +import { HttpError } from '@oazapfts/runtime'; + +export interface ApiExceptionResponse { + message: string; + error?: string; + statusCode: number; +} + +export interface ApiHttpError extends HttpError { + data: ApiExceptionResponse; +} + +export function isHttpError(error: unknown): error is ApiHttpError { + return error instanceof HttpError; +} diff --git a/open-api/typescript-sdk/fetch.ts b/open-api/typescript-sdk/fetch.ts index 5441cd8268..5759e66ad9 100644 --- a/open-api/typescript-sdk/fetch.ts +++ b/open-api/typescript-sdk/fetch.ts @@ -1 +1,2 @@ export * from './fetch-client'; +export * from './fetch-errors'; diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index a918e2d2c9..3359297e26 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 7ea2795ea7..0ebd5c44cb 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 0e09a68be5..d869775c98 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 7dd47e06c6..8d2a1b79bc 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 5c4b3e9051..0000000000 --- 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 97c9dca58e..ceac96222d 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/domain.constant.ts b/server/src/domain/domain.constant.ts index 4e7c4d5524..0dc9c54140 100644 --- a/server/src/domain/domain.constant.ts +++ b/server/src/domain/domain.constant.ts @@ -91,7 +91,7 @@ export const citiesFile = 'cities500.txt'; export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt'); export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt'); export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt'); -export const geodataCitites500Path = join(GEODATA_ROOT_PATH, citiesFile); +export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile); const image: Record = { '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts index f4c9aa53e7..dc5934e7b1 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 c9483c3736..e5890bdd03 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 846b6156d6..ed6f884493 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 7183e9e3fe..c9fec3cf71 100644 --- a/server/src/domain/repositories/search.repository.ts +++ b/server/src/domain/repositories/search.repository.ts @@ -1,4 +1,4 @@ -import { AssetEntity, AssetFaceEntity, AssetType, SmartInfoEntity } from '@app/infra/entities'; +import { AssetEntity, AssetFaceEntity, AssetType, GeodataPlacesEntity, SmartInfoEntity } from '@app/infra/entities'; import { Paginated } from '../domain.util'; export const ISearchRepository = 'ISearchRepository'; @@ -186,4 +186,6 @@ export interface ISearchRepository { searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated; searchFaces(search: FaceEmbeddingSearch): Promise; upsert(smartInfo: Partial, embedding?: Embedding): Promise; + searchPlaces(placeName: string): Promise; + deleteAllSearchEmbeddings(): Promise; } diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index 4f2aa18199..877a494e4d 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -1,5 +1,5 @@ import { AssetOrder } from '@app/domain/asset/dto/asset.dto'; -import { AssetType } from '@app/infra/entities'; +import { AssetType, GeodataPlacesEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; @@ -241,6 +241,12 @@ export class SearchDto { size?: number; } +export class SearchPlacesDto { + @IsString() + @IsNotEmpty() + name!: string; +} + export class SearchPeopleDto { @IsString() @IsNotEmpty() @@ -251,3 +257,21 @@ export class SearchPeopleDto { @Optional() withHidden?: boolean; } + +export class PlacesResponseDto { + name!: string; + latitude!: number; + longitude!: number; + admin1name?: string; + admin2name?: string; +} + +export function mapPlaces(place: GeodataPlacesEntity): PlacesResponseDto { + return { + name: place.name, + latitude: place.latitude, + longitude: place.longitude, + admin1name: place.admin1Name, + admin2name: place.admin2Name, + }; +} diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 452c556f41..5b56399981 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -16,7 +16,15 @@ import { SearchStrategy, } from '../repositories'; import { FeatureFlag, SystemConfigCore } from '../system-config'; -import { MetadataSearchDto, SearchDto, SearchPeopleDto, SmartSearchDto } from './dto'; +import { + MetadataSearchDto, + PlacesResponseDto, + SearchDto, + SearchPeopleDto, + SearchPlacesDto, + SmartSearchDto, + mapPlaces, +} from './dto'; import { SearchSuggestionRequestDto, SearchSuggestionType } from './dto/search-suggestion.dto'; import { SearchResponseDto } from './response-dto'; @@ -41,6 +49,11 @@ export class SearchService { return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden }); } + async searchPlaces(dto: SearchPlacesDto): Promise { + const places = await this.searchRepository.searchPlaces(dto.name); + return places.map((place) => mapPlaces(place)); + } + async getExploreData(auth: AuthDto): Promise[]> { await this.configCore.requireFeature(FeatureFlag.SEARCH); const options = { maxFields: 12, minAssetsPerField: 5 }; @@ -182,26 +195,22 @@ export class SearchService { } async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise { - if (dto.type === SearchSuggestionType.COUNTRY) { - return this.metadataRepository.getCountries(auth.user.id); + switch (dto.type) { + case SearchSuggestionType.COUNTRY: { + return this.metadataRepository.getCountries(auth.user.id); + } + case SearchSuggestionType.STATE: { + return this.metadataRepository.getStates(auth.user.id, dto.country); + } + case SearchSuggestionType.CITY: { + return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state); + } + case SearchSuggestionType.CAMERA_MAKE: { + return this.metadataRepository.getCameraMakes(auth.user.id, dto.model); + } + case SearchSuggestionType.CAMERA_MODEL: { + return this.metadataRepository.getCameraModels(auth.user.id, dto.make); + } } - - if (dto.type === SearchSuggestionType.STATE) { - return this.metadataRepository.getStates(auth.user.id, dto.country); - } - - if (dto.type === SearchSuggestionType.CITY) { - return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state); - } - - if (dto.type === SearchSuggestionType.CAMERA_MAKE) { - return this.metadataRepository.getCameraMakes(auth.user.id, dto.model); - } - - if (dto.type === SearchSuggestionType.CAMERA_MODEL) { - return this.metadataRepository.getCameraModels(auth.user.id, dto.make); - } - - return []; } } 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 5da7b7824b..9835ea1a53 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 d193b29b51..19d5668cc5 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/domain/storage-template/storage-template.service.ts b/server/src/domain/storage-template/storage-template.service.ts index d696982540..857d1df327 100644 --- a/server/src/domain/storage-template/storage-template.service.ts +++ b/server/src/domain/storage-template/storage-template.service.ts @@ -117,7 +117,7 @@ export class StorageTemplateService { return true; } const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getAll(pagination), + this.assetRepository.getAll(pagination, { withExif: true }), ); const users = await this.userRepository.getList(); diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts index 4e57cfaa62..b807da9665 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/immich/controllers/search.controller.ts @@ -2,9 +2,11 @@ import { AuthDto, MetadataSearchDto, PersonResponseDto, + PlacesResponseDto, SearchDto, SearchExploreResponseDto, SearchPeopleDto, + SearchPlacesDto, SearchResponseDto, SearchService, SmartSearchDto, @@ -48,6 +50,11 @@ export class SearchController { return this.service.searchPerson(auth, dto); } + @Get('places') + searchPlaces(@Query() dto: SearchPlacesDto): Promise { + return this.service.searchPlaces(dto); + } + @Get('suggestions') getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise { return this.service.getSearchSuggestions(auth, dto); diff --git a/server/src/infra/entities/geodata-admin1.entity.ts b/server/src/infra/entities/geodata-admin1.entity.ts deleted file mode 100644 index 36cf0a805e..0000000000 --- a/server/src/infra/entities/geodata-admin1.entity.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; - -@Entity('geodata_admin1') -export class GeodataAdmin1Entity { - @PrimaryColumn({ type: 'varchar' }) - key!: string; - - @Column({ type: 'varchar' }) - name!: string; -} diff --git a/server/src/infra/entities/geodata-admin2.entity.ts b/server/src/infra/entities/geodata-admin2.entity.ts deleted file mode 100644 index bd03e83776..0000000000 --- a/server/src/infra/entities/geodata-admin2.entity.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Column, Entity, PrimaryColumn } from 'typeorm'; - -@Entity('geodata_admin2') -export class GeodataAdmin2Entity { - @PrimaryColumn({ type: 'varchar' }) - key!: string; - - @Column({ type: 'varchar' }) - name!: string; -} diff --git a/server/src/infra/entities/geodata-places.entity.ts b/server/src/infra/entities/geodata-places.entity.ts index 244e4261b0..966a50d5c9 100644 --- a/server/src/infra/entities/geodata-places.entity.ts +++ b/server/src/infra/entities/geodata-places.entity.ts @@ -1,6 +1,4 @@ -import { GeodataAdmin1Entity } from '@app/infra/entities/geodata-admin1.entity'; -import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity'; -import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; +import { Column, Entity, PrimaryColumn } from 'typeorm'; @Entity('geodata_places', { synchronize: false }) export class GeodataPlacesEntity { @@ -21,7 +19,7 @@ export class GeodataPlacesEntity { // asExpression: 'll_to_earth((latitude)::double precision, (longitude)::double precision)', // type: 'earth', // }) - earthCoord!: unknown; + // earthCoord!: unknown; @Column({ type: 'char', length: 2 }) countryCode!: string; @@ -32,27 +30,14 @@ export class GeodataPlacesEntity { @Column({ type: 'varchar', length: 80, nullable: true }) admin2Code!: string; - @Column({ - type: 'varchar', - generatedType: 'STORED', - asExpression: `"countryCode" || '.' || "admin1Code"`, - nullable: true, - }) - admin1Key!: string; + @Column({ type: 'varchar', nullable: true }) + admin1Name!: string; - @ManyToOne(() => GeodataAdmin1Entity, { eager: true, nullable: true, createForeignKeyConstraints: false }) - admin1!: GeodataAdmin1Entity; + @Column({ type: 'varchar', nullable: true }) + admin2Name!: string; - @Column({ - type: 'varchar', - generatedType: 'STORED', - asExpression: `"countryCode" || '.' || "admin1Code" || '.' || "admin2Code"`, - nullable: true, - }) - admin2Key!: string; - - @ManyToOne(() => GeodataAdmin2Entity, { eager: true, nullable: true, createForeignKeyConstraints: false }) - admin2!: GeodataAdmin2Entity; + @Column({ type: 'varchar', nullable: true }) + alternateNames!: string; @Column({ type: 'date' }) modificationDate!: Date; diff --git a/server/src/infra/entities/index.ts b/server/src/infra/entities/index.ts index 957e15a887..af620790ef 100644 --- a/server/src/infra/entities/index.ts +++ b/server/src/infra/entities/index.ts @@ -7,8 +7,6 @@ import { AssetStackEntity } from './asset-stack.entity'; import { AssetEntity } from './asset.entity'; import { AuditEntity } from './audit.entity'; import { ExifEntity } from './exif.entity'; -import { GeodataAdmin1Entity } from './geodata-admin1.entity'; -import { GeodataAdmin2Entity } from './geodata-admin2.entity'; import { GeodataPlacesEntity } from './geodata-places.entity'; import { LibraryEntity } from './library.entity'; import { MoveEntity } from './move.entity'; @@ -32,8 +30,6 @@ export * from './asset-stack.entity'; export * from './asset.entity'; export * from './audit.entity'; export * from './exif.entity'; -export * from './geodata-admin1.entity'; -export * from './geodata-admin2.entity'; export * from './geodata-places.entity'; export * from './library.entity'; export * from './move.entity'; @@ -59,8 +55,6 @@ export const databaseEntities = [ AuditEntity, ExifEntity, GeodataPlacesEntity, - GeodataAdmin1Entity, - GeodataAdmin2Entity, MoveEntity, PartnerEntity, PersonEntity, diff --git a/server/src/infra/migrations/1708059341865-GeodataLocationSearch.ts b/server/src/infra/migrations/1708059341865-GeodataLocationSearch.ts new file mode 100644 index 0000000000..136ca2598d --- /dev/null +++ b/server/src/infra/migrations/1708059341865-GeodataLocationSearch.ts @@ -0,0 +1,152 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class GeodataLocationSearch1708059341865 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm`); + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS unaccent`); + + // https://stackoverflow.com/a/11007216 + await queryRunner.query(` + CREATE OR REPLACE FUNCTION f_unaccent(text) + RETURNS text + LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT + RETURN unaccent('unaccent', $1)`); + + await queryRunner.query(`ALTER TABLE geodata_places ADD COLUMN "admin1Name" varchar`); + await queryRunner.query(`ALTER TABLE geodata_places ADD COLUMN "admin2Name" varchar`); + + await queryRunner.query(` + UPDATE geodata_places + SET "admin1Name" = admin1.name + FROM geodata_admin1 admin1 + WHERE admin1.key = "admin1Key"`); + + await queryRunner.query(` + UPDATE geodata_places + SET "admin2Name" = admin2.name + FROM geodata_admin2 admin2 + WHERE admin2.key = "admin2Key"`); + + await queryRunner.query(`DROP TABLE geodata_admin1 CASCADE`); + await queryRunner.query(`DROP TABLE geodata_admin2 CASCADE`); + + await queryRunner.query(` + ALTER TABLE geodata_places + DROP COLUMN "admin1Key", + DROP COLUMN "admin2Key"`); + + await queryRunner.query(` + CREATE INDEX idx_geodata_places_name + ON geodata_places + USING gin (f_unaccent(name) gin_trgm_ops)`); + + await queryRunner.query(` + CREATE INDEX idx_geodata_places_admin1_name + ON geodata_places + USING gin (f_unaccent("admin1Name") gin_trgm_ops)`); + + await queryRunner.query(` + CREATE INDEX idx_geodata_places_admin2_name + ON geodata_places + USING gin (f_unaccent("admin2Name") gin_trgm_ops)`); + + await queryRunner.query( + ` + DELETE FROM "typeorm_metadata" + WHERE + "type" = $1 AND + "name" = $2 AND + "database" = $3 AND + "schema" = $4 AND + "table" = $5`, + ['GENERATED_COLUMN', 'admin1Key', 'immich', 'public', 'geodata_places'], + ); + + await queryRunner.query( + ` + DELETE FROM "typeorm_metadata" + WHERE + "type" = $1 AND + "name" = $2 AND + "database" = $3 AND + "schema" = $4 AND + "table" = $5`, + ['GENERATED_COLUMN', 'admin2Key', 'immich', 'public', 'geodata_places'], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "geodata_admin1" ( + "key" character varying NOT NULL, + "name" character varying NOT NULL, + CONSTRAINT "PK_3fe3a89c5aac789d365871cb172" PRIMARY KEY ("key") + )`); + + await queryRunner.query(` + CREATE TABLE "geodata_admin2" ( + "key" character varying NOT NULL, + "name" character varying NOT NULL, + CONSTRAINT "PK_1e3886455dbb684d6f6b4756726" PRIMARY KEY ("key") + )`); + + await queryRunner.query(` + ALTER TABLE geodata_places + ADD COLUMN "admin1Key" character varying + GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code") STORED, + ADD COLUMN "admin2Key" character varying + GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code" || '.' || "admin2Code") STORED`); + + await queryRunner.query( + ` + INSERT INTO "geodata_admin1" + SELECT DISTINCT + "admin1Key" AS "key", + "admin1Name" AS "name" + FROM geodata_places + WHERE "admin1Name" IS NOT NULL`, + ); + + await queryRunner.query( + ` + INSERT INTO "geodata_admin2" + SELECT DISTINCT + "admin2Key" AS "key", + "admin2Name" AS "name" + FROM geodata_places + WHERE "admin2Name" IS NOT NULL`, + ); + + await queryRunner.query(` + UPDATE geodata_places + SET "admin1Name" = admin1.name + FROM geodata_admin1 admin1 + WHERE admin1.key = "admin1Key"`); + + await queryRunner.query(` + UPDATE geodata_places + SET "admin2Name" = admin2.name + FROM geodata_admin2 admin2 + WHERE admin2.key = "admin2Key";`); + + await queryRunner.query( + ` + INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") + VALUES ($1, $2, $3, $4, $5, $6)`, + ['immich', 'public', 'geodata_places', 'GENERATED_COLUMN', 'admin1Key', '"countryCode" || \'.\' || "admin1Code"'], + ); + + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") + VALUES ($1, $2, $3, $4, $5, $6)`, + [ + 'immich', + 'public', + 'geodata_places', + 'GENERATED_COLUMN', + 'admin2Key', + '"countryCode" || \'.\' || "admin1Code" || \'.\' || "admin2Code"', + ], + ); + } +} diff --git a/server/src/infra/migrations/1708116312820-GeonamesEnhancement.ts b/server/src/infra/migrations/1708116312820-GeonamesEnhancement.ts new file mode 100644 index 0000000000..0cea9a0411 --- /dev/null +++ b/server/src/infra/migrations/1708116312820-GeonamesEnhancement.ts @@ -0,0 +1,18 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class GeonamesEnhancement1708116312820 implements MigrationInterface { + name = 'GeonamesEnhancement1708116312820' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE geodata_places ADD COLUMN "alternateNames" varchar`); + await queryRunner.query(` + CREATE INDEX idx_geodata_places_admin2_alternate_names + ON geodata_places + USING gin (f_unaccent("alternateNames") gin_trgm_ops)`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE geodata_places DROP COLUMN "alternateNames"`); + } + +} diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/infra/repositories/media.repository.ts index a981bbc072..1f9395ff21 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/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index 6a90ad1081..4abfe0eace 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -2,7 +2,7 @@ import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, - geodataCitites500Path, + geodataCities500Path, geodataDatePath, GeoPoint, IMetadataRepository, @@ -10,13 +10,7 @@ import { ISystemMetadataRepository, ReverseGeocodeResult, } from '@app/domain'; -import { - ExifEntity, - GeodataAdmin1Entity, - GeodataAdmin2Entity, - GeodataPlacesEntity, - SystemMetadataKey, -} from '@app/infra/entities'; +import { ExifEntity, GeodataPlacesEntity, SystemMetadataKey } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { Inject } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; @@ -26,19 +20,16 @@ import { getName } from 'i18n-iso-countries'; import { createReadStream, existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import * as readLine from 'node:readline'; -import { DataSource, DeepPartial, QueryRunner, Repository } from 'typeorm'; +import { DataSource, QueryRunner, Repository } from 'typeorm'; +import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js'; import { DummyValue, GenerateSql } from '../infra.util'; -type GeoEntity = GeodataPlacesEntity | GeodataAdmin1Entity | GeodataAdmin2Entity; -type GeoEntityClass = typeof GeodataPlacesEntity | typeof GeodataAdmin1Entity | typeof GeodataAdmin2Entity; - export class MetadataRepository implements IMetadataRepository { constructor( @InjectRepository(ExifEntity) private exifRepository: Repository, @InjectRepository(GeodataPlacesEntity) private readonly geodataPlacesRepository: Repository, - @InjectRepository(GeodataAdmin1Entity) private readonly geodataAdmin1Repository: Repository, - @InjectRepository(GeodataAdmin2Entity) private readonly geodataAdmin2Repository: Repository, - @Inject(ISystemMetadataRepository) private readonly systemMetadataRepository: ISystemMetadataRepository, + @Inject(ISystemMetadataRepository) + private readonly systemMetadataRepository: ISystemMetadataRepository, @InjectDataSource() private dataSource: DataSource, ) {} @@ -54,7 +45,6 @@ export class MetadataRepository implements IMetadataRepository { return; } - this.logger.log('Importing geodata to database from file'); await this.importGeodata(); await this.systemMetadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, { @@ -69,12 +59,14 @@ export class MetadataRepository implements IMetadataRepository { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); + const admin1 = await this.loadAdmin(geodataAdmin1Path); + const admin2 = await this.loadAdmin(geodataAdmin2Path); + try { await queryRunner.startTransaction(); - await this.loadCities500(queryRunner); - await this.loadAdmin1(queryRunner); - await this.loadAdmin2(queryRunner); + await queryRunner.manager.clear(GeodataPlacesEntity); + await this.loadCities500(queryRunner, admin1, admin2); await queryRunner.commitTransaction(); } catch (error) { @@ -86,76 +78,73 @@ export class MetadataRepository implements IMetadataRepository { } } - private async loadGeodataToTableFromFile( + private async loadGeodataToTableFromFile( queryRunner: QueryRunner, - lineToEntityMapper: (lineSplit: string[]) => T, + lineToEntityMapper: (lineSplit: string[]) => GeodataPlacesEntity, filePath: string, - entity: GeoEntityClass, ) { if (!existsSync(filePath)) { this.logger.error(`Geodata file ${filePath} not found`); throw new Error(`Geodata file ${filePath} not found`); } - await queryRunner.manager.clear(entity); const input = createReadStream(filePath); - let buffer: DeepPartial[] = []; - const lineReader = readLine.createInterface({ input: input }); + let bufferGeodata: QueryDeepPartialEntity[] = []; + const lineReader = readLine.createInterface({ input }); for await (const line of lineReader) { const lineSplit = line.split('\t'); - buffer.push(lineToEntityMapper(lineSplit)); - if (buffer.length > 1000) { - await queryRunner.manager.save(buffer); - buffer = []; + const geoData = lineToEntityMapper(lineSplit); + bufferGeodata.push(geoData); + if (bufferGeodata.length > 1000) { + await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']); + bufferGeodata = []; } } - await queryRunner.manager.save(buffer); + await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']); } - private async loadCities500(queryRunner: QueryRunner) { - await this.loadGeodataToTableFromFile( + private async loadCities500( + queryRunner: QueryRunner, + admin1Map: Map, + admin2Map: Map, + ) { + await this.loadGeodataToTableFromFile( queryRunner, (lineSplit: string[]) => this.geodataPlacesRepository.create({ id: Number.parseInt(lineSplit[0]), name: lineSplit[1], + alternateNames: lineSplit[3], latitude: Number.parseFloat(lineSplit[4]), longitude: Number.parseFloat(lineSplit[5]), countryCode: lineSplit[8], admin1Code: lineSplit[10], admin2Code: lineSplit[11], modificationDate: lineSplit[18], + admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`), + admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`), }), - geodataCitites500Path, - GeodataPlacesEntity, + geodataCities500Path, ); } - private async loadAdmin1(queryRunner: QueryRunner) { - await this.loadGeodataToTableFromFile( - queryRunner, - (lineSplit: string[]) => - this.geodataAdmin1Repository.create({ - key: lineSplit[0], - name: lineSplit[1], - }), - geodataAdmin1Path, - GeodataAdmin1Entity, - ); - } + private async loadAdmin(filePath: string) { + if (!existsSync(filePath)) { + this.logger.error(`Geodata file ${filePath} not found`); + throw new Error(`Geodata file ${filePath} not found`); + } - private async loadAdmin2(queryRunner: QueryRunner) { - await this.loadGeodataToTableFromFile( - queryRunner, - (lineSplit: string[]) => - this.geodataAdmin2Repository.create({ - key: lineSplit[0], - name: lineSplit[1], - }), - geodataAdmin2Path, - GeodataAdmin2Entity, - ); + const input = createReadStream(filePath); + const lineReader = readLine.createInterface({ input: input }); + + const adminMap = new Map(); + for await (const line of lineReader) { + const lineSplit = line.split('\t'); + adminMap.set(lineSplit[0], lineSplit[1]); + } + + return adminMap; } async teardown() { @@ -167,8 +156,6 @@ export class MetadataRepository implements IMetadataRepository { const response = await this.geodataPlacesRepository .createQueryBuilder('geoplaces') - .leftJoinAndSelect('geoplaces.admin1', 'admin1') - .leftJoinAndSelect('geoplaces.admin2', 'admin2') .where('earth_box(ll_to_earth(:latitude, :longitude), 25000) @> "earthCoord"', point) .orderBy('earth_distance(ll_to_earth(:latitude, :longitude), "earthCoord")') .limit(1) @@ -183,9 +170,9 @@ export class MetadataRepository implements IMetadataRepository { this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`); - const { countryCode, name: city, admin1, admin2 } = response; + const { countryCode, name: city, admin1Name, admin2Name } = response; const country = getName(countryCode, 'en') ?? null; - const stateParts = [admin2?.name, admin1?.name].filter((name) => !!name); + const stateParts = [admin2Name, admin1Name].filter((name) => !!name); const state = stateParts.length > 0 ? stateParts.join(', ') : null; return { country, state, city }; diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index 63b3d570ef..3a7ec29466 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 a30c96b10d..c8dc5070f7 100644 --- a/server/src/infra/repositories/search.repository.ts +++ b/server/src/infra/repositories/search.repository.ts @@ -12,7 +12,13 @@ import { SmartSearchOptions, } from '@app/domain'; import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant'; -import { AssetEntity, AssetFaceEntity, SmartInfoEntity, SmartSearchEntity } from '@app/infra/entities'; +import { + AssetEntity, + AssetFaceEntity, + GeodataPlacesEntity, + SmartInfoEntity, + SmartSearchEntity, +} from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -31,6 +37,7 @@ export class SearchRepository implements ISearchRepository { @InjectRepository(AssetEntity) private assetRepository: Repository, @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository, @InjectRepository(SmartSearchEntity) private smartSearchRepository: Repository, + @InjectRepository(GeodataPlacesEntity) private readonly geodataPlacesRepository: Repository, ) { this.faceColumns = this.assetFaceRepository.manager.connection .getMetadata(AssetFaceEntity) @@ -172,6 +179,27 @@ export class SearchRepository implements ISearchRepository { })); } + @GenerateSql({ params: [DummyValue.STRING] }) + async searchPlaces(placeName: string): Promise { + return await this.geodataPlacesRepository + .createQueryBuilder('geoplaces') + .where(`f_unaccent(name) %>> f_unaccent(:placeName)`) + .orWhere(`f_unaccent("admin2Name") %>> f_unaccent(:placeName)`) + .orWhere(`f_unaccent("admin1Name") %>> f_unaccent(:placeName)`) + .orWhere(`f_unaccent("alternateNames") %>> f_unaccent(:placeName)`) + .orderBy( + ` + COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0) + + COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0) + + COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0) + + COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0) + `, + ) + .setParameters({ placeName }) + .limit(20) + .getMany(); + } + async upsert(smartInfo: Partial, embedding?: Embedding): Promise { await this.repository.upsert(smartInfo, { conflictPaths: ['assetId'] }); if (!smartInfo.assetId || !embedding) { @@ -201,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/src/infra/sql/search.repository.sql b/server/src/infra/sql/search.repository.sql index a21697c268..c45d90a7a3 100644 --- a/server/src/infra/sql/search.repository.sql +++ b/server/src/infra/sql/search.repository.sql @@ -238,3 +238,37 @@ FROM WHERE res.distance <= $3 COMMIT + +-- SearchRepository.searchPlaces +SELECT + "geoplaces"."id" AS "geoplaces_id", + "geoplaces"."name" AS "geoplaces_name", + "geoplaces"."longitude" AS "geoplaces_longitude", + "geoplaces"."latitude" AS "geoplaces_latitude", + "geoplaces"."countryCode" AS "geoplaces_countryCode", + "geoplaces"."admin1Code" AS "geoplaces_admin1Code", + "geoplaces"."admin2Code" AS "geoplaces_admin2Code", + "geoplaces"."admin1Name" AS "geoplaces_admin1Name", + "geoplaces"."admin2Name" AS "geoplaces_admin2Name", + "geoplaces"."alternateNames" AS "geoplaces_alternateNames", + "geoplaces"."modificationDate" AS "geoplaces_modificationDate" +FROM + "geodata_places" "geoplaces" +WHERE + f_unaccent (name) %>> f_unaccent ($1) + OR f_unaccent ("admin2Name") %>> f_unaccent ($1) + OR f_unaccent ("admin1Name") %>> f_unaccent ($1) + OR f_unaccent ("alternateNames") %>> f_unaccent ($1) +ORDER BY + COALESCE(f_unaccent (name) <->>> f_unaccent ($1), 0) + COALESCE( + f_unaccent ("admin2Name") <->>> f_unaccent ($1), + 0 + ) + COALESCE( + f_unaccent ("admin1Name") <->>> f_unaccent ($1), + 0 + ) + COALESCE( + f_unaccent ("alternateNames") <->>> f_unaccent ($1), + 0 + ) ASC +LIMIT + 20 diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts index e0bdab269a..5912d77451 100644 --- a/server/test/repositories/search.repository.mock.ts +++ b/server/test/repositories/search.repository.mock.ts @@ -7,5 +7,7 @@ export const newSearchRepositoryMock = (): jest.Mocked => { searchSmart: jest.fn(), searchFaces: jest.fn(), upsert: jest.fn(), + searchPlaces: jest.fn(), + deleteAllSearchEmbeddings: jest.fn(), }; }; diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs index ef17242c8e..2b89e5dc7d 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 78e5caf7c5..77a875e517 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 2b53d06451..51f07dded0 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 1e29371fa9..f30e3ee8ca 100644 --- a/web/src/hooks.client.ts +++ b/web/src/hooks.client.ts @@ -1,40 +1,27 @@ +import { isHttpError } from '@immich/sdk'; import type { HandleClientError } from '@sveltejs/kit'; -import type { AxiosError, AxiosResponse } from 'axios'; -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) => { - const httpError = error as AxiosError; - const request = httpError?.request as Request & { path: string }; - const response = httpError?.response as AxiosResponse<{ - message: string; - statusCode: number; - error: string; - }>; + const httpError = isHttpError(error) ? error : undefined; + const statusCode = httpError?.status || httpError?.data?.statusCode || 500; + const message = httpError?.data?.message || (httpError?.data && String(httpError.data)) || httpError?.message; - let code = response?.data?.statusCode || response?.status || httpError.code || '500'; - if (response) { - code += ` - ${response.data?.error || response.statusText}`; - } - - if (request && response) { - console.log({ - status: response.status, - url: `${request.method} ${request.path}`, - response: response.data || 'No data', - }); - } + console.log({ + status: statusCode, + response: httpError?.data || 'No data', + }); return { - message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE, - code, + message: message || DEFAULT_MESSAGE, + code: statusCode, stack: httpError?.stack, }; }; 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 60756166f2..edf5c48300 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 1ad962b0de..16b2afc7fe 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/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte index 6a542d81d4..ba24f3aabd 100644 --- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte +++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte @@ -112,8 +112,8 @@ desc="Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives." bind:value={config.machineLearning.facialRecognition.minScore} step="0.1" - min="0" - max="1" + min={0} + max={1} disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} isEdited={config.machineLearning.facialRecognition.minScore !== savedConfig.machineLearning.facialRecognition.minScore} @@ -125,8 +125,8 @@ desc="Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible." bind:value={config.machineLearning.facialRecognition.maxDistance} step="0.1" - min="0" - max="2" + min={0} + max={2} disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} isEdited={config.machineLearning.facialRecognition.maxDistance !== savedConfig.machineLearning.facialRecognition.maxDistance} @@ -138,7 +138,7 @@ desc="The minimum number of recognized faces for a person to be created. Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person." bind:value={config.machineLearning.facialRecognition.minFaces} step="1" - min="1" + min={1} disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} isEdited={config.machineLearning.facialRecognition.minFaces !== savedConfig.machineLearning.facialRecognition.minFaces} diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte index 5fc3b3e222..11e07d0029 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte @@ -84,7 +84,26 @@ }; -

+
+
+

+ For more details about this feature, refer to the Storage Template + + and its + implications + +

+
{#await getTemplateOptions() then}
{ 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 dff2d7f319..4dd1b75e79 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 38f65e3df7..7f94857afc 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 e10d5573ca..79e8276153 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/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte index c099ae79c4..164b0a7913 100644 --- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte +++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte @@ -1,23 +1,16 @@
- dispatch('close')} title="Exit Slideshow" /> - {#if $slideshowShuffle} - ($slideshowShuffle = false)} title="Shuffle" /> - {:else} - ($slideshowShuffle = true)} title="No shuffle" /> - {/if} + dispatch('close')} title="Exit Slideshow" /> (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())} title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'} /> - dispatch('prev')} title="Previous" /> - dispatch('next')} title="Next" /> + dispatch('prev')} title="Previous" /> + dispatch('next')} title="Next" /> + (showSettings = !showSettings)} title="Next" />
+{#if showSettings} + (showSettings = false)} /> +{/if} +
{/if}
- {#if isLoading} -
- -
- {:else if searchResultAssets.length > 0} + {#if searchResultAssets.length > 0} - {:else} + {:else if !isLoading}
@@ -307,6 +308,12 @@
{/if} + + {#if isLoading} +
+ +
+ {/if}
diff --git a/web/src/routes/(user)/search/photos/[assetId]/+page.ts b/web/src/routes/(user)/search/photos/[assetId]/+page.ts index 3c4bafa3ef..f1e5126931 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 fb1f0d766c..c68b534a90 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 dd34b47e4b..380c9d0024 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 602a3a2e01..b50cb5089b 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 394e74faf8..3a6ff6ac0a 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 3dc126d7b6..c2398563ec 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 0474207e32..eb3a453d24 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 ab0a19f1c2..ffb36cf352 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 e4f090a069..0d53c4ef2b 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 38ecaa62f5..2f6c98af13 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 bf2830f15b..54f62b3adb 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 ea6cb76539..84917d591e 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 f56169ddb7..277b3c0040 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 199cd06e36..9c22439c56 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 4cf42d4d3d..09139a7f7e 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 0081e8e76b..3cb982c6b8 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