Compare commits

..

11 Commits

Author SHA1 Message Date
Marty Fuhry 45d4984fde blurhash fade to thumb 2024-02-27 09:34:44 -05:00
Marty Fuhry ef2f605635 Fixes an issue where thumbnails fail to load if too many thumbnail requests are made simultaenously 2024-02-27 08:22:20 -05:00
Alex Tran 4532db552e Merge branch 'main' of github.com:immich-app/immich into refactor/immich-thumbnail 2024-02-26 21:26:25 -06:00
Marty Fuhry b9d438006c Fixes fade in duration for fade in placeholder 2024-02-24 20:49:43 -05:00
Marty Fuhry 1eb96fa9e9 Uses blurhash ref instead of state 2024-02-24 20:48:57 -05:00
Marty Fuhry c2694996e5 Uses blurhash hook state 2024-02-21 20:13:47 -05:00
Marty Fuhry 4d60133504 uses hook instead of stateful widget to be more consistent 2024-02-21 20:07:34 -05:00
Marty Fuhry c0ef300040 Fixes image blur 2024-02-21 12:11:38 -05:00
Marty Fuhry b05d4fa7d3 Adds blurhash
format
2024-02-21 10:48:36 -05:00
Marty Fuhry 84cd91bbbe dart format
linter errors

linter
2024-02-21 09:04:33 -05:00
Marty Fuhry 718c258a07 Refactor to use ImmichThumbnail and local thumbnail image provider
format
2024-02-20 22:10:38 -05:00
163 changed files with 2606 additions and 3063 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0 uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.1.0 uses: docker/setup-buildx-action@v3.0.0
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
+1 -1
View File
@@ -66,7 +66,7 @@ jobs:
uses: docker/setup-qemu-action@v3.0.0 uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.1.0 uses: docker/setup-buildx-action@v3.0.0
# Workaround to fix error: # Workaround to fix error:
# failed to push: failed to copy: io: read/write on closed pipe # failed to push: failed to copy: io: read/write on closed pipe
# See https://github.com/docker/build-push-action/issues/761 # See https://github.com/docker/build-push-action/issues/761
+2 -2
View File
@@ -12,7 +12,7 @@ concurrency:
jobs: jobs:
server-e2e-api: server-e2e-api:
name: Server (e2e-api) name: Server (e2e-api)
runs-on: ubuntu-latest runs-on: mich
defaults: defaults:
run: run:
working-directory: ./server working-directory: ./server
@@ -29,7 +29,7 @@ jobs:
server-e2e-jobs: server-e2e-jobs:
name: Server (e2e-jobs) name: Server (e2e-jobs)
runs-on: ubuntu-latest runs-on: mich
steps: steps:
- name: Checkout code - name: Checkout code
+404 -414
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -60,12 +60,12 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
environment: environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
+6
View File
@@ -38,6 +38,12 @@ services:
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
- /dev/dma_heap:/dev/dma_heap - /dev/dma_heap:/dev/dma_heap
- /dev/mpp_service:/dev/mpp_service - /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: vaapi:
devices: devices:
+4 -1
View File
@@ -88,7 +88,10 @@ Some basic examples:
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button. This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes. If your photos are on a network drive you will likely have to enable filesystem polling. The performance hit for polling large libraries is currently unknown, feel free to test this feature and report back. In addition to the boolean feature flag, the configuration file allows customization of the following parameters, please see the [chokidar documentation](https://github.com/paulmillr/chokidar?tab=readme-ov-file#performance) for reference.
- `usePolling` (default: `false`).
- `interval`. (default: 10000). When using polling, this is how often (in milliseconds) the filesystem is polled.
### Nightly job ### Nightly job
-10
View File
@@ -50,22 +50,12 @@ import {
mdiVectorCombine, mdiVectorCombine,
mdiVideo, mdiVideo,
mdiWeb, mdiWeb,
mdiScaleBalance,
} from '@mdi/js'; } from '@mdi/js';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
import React from 'react'; import React from 'react';
import Timeline, { DateType, Item } from '../components/timeline'; import Timeline, { DateType, Item } from '../components/timeline';
const items: Item[] = [ const items: Item[] = [
{
icon: mdiScaleBalance,
description: 'Immich switches to AGPLv3 license',
title: 'AGPL License',
release: 'v1.95.0',
tag: 'v1.95.0',
date: new Date(2024, 1, 20),
dateType: DateType.RELEASE,
},
{ {
icon: mdiEyeRefreshOutline, icon: mdiEyeRefreshOutline,
description: 'Automatically import files in external libraries when the operating system detects changes.', description: 'Automatically import files in external libraries when the operating system detects changes.',
+4 -5
View File
@@ -4,6 +4,7 @@ name: immich-e2e
x-server-build: &server-common x-server-build: &server-common
image: immich-server:latest image: immich-server:latest
container_name: immich-e2e-server
build: build:
context: ../ context: ../
dockerfile: server/Dockerfile dockerfile: server/Dockerfile
@@ -22,16 +23,14 @@ x-server-build: &server-common
services: services:
immich-server: immich-server:
container_name: immich-e2e-server
command: [ "./start.sh", "immich" ] command: [ "./start.sh", "immich" ]
<<: *server-common <<: *server-common
ports: ports:
- 2283:3001 - 2283:3001
immich-microservices: # immich-microservices:
container_name: immich-e2e-microservices # command: [ "./start.sh", "microservices" ]
command: [ "./start.sh", "microservices" ] # <<: *server-common
<<: *server-common
redis: redis:
image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5
+3 -169
View File
@@ -12,15 +12,11 @@
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.41.2", "@playwright/test": "^1.41.2",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^1.3.0", "@vitest/coverage-v8": "^1.3.0",
"exiftool-vendored": "^24.5.0",
"luxon": "^3.4.4",
"pg": "^8.11.3", "pg": "^8.11.3",
"socket.io-client": "^4.7.4",
"supertest": "^6.3.4", "supertest": "^6.3.4",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vitest": "^1.3.0" "vitest": "^1.3.0"
@@ -595,12 +591,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@photostructure/tz-lookup": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.2.tgz",
"integrity": "sha512-H8+tTt7ilJNkFyb+QgPnLEGUjQzGwiMb9n7lwRZNBgSKL3VZs9AkjI1E//FcwPjNafwAH932U92+xTqJiF3Bbw==",
"dev": true
},
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.41.2", "version": "1.41.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz",
@@ -791,12 +781,6 @@
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true "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": { "node_modules/@types/cookiejar": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
@@ -815,12 +799,6 @@
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true "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": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@@ -828,9 +806,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.20", "version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
@@ -1081,15 +1059,6 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"node_modules/batch-cluster": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-13.0.0.tgz",
"integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==",
"dev": true,
"engines": {
"node": ">=14"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1294,28 +1263,6 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "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": { "node_modules/es-define-property": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
@@ -1407,43 +1354,6 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1" "url": "https://github.com/sindresorhus/execa?sponsor=1"
} }
}, },
"node_modules/exiftool-vendored": {
"version": "24.5.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz",
"integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==",
"dev": true,
"dependencies": {
"@photostructure/tz-lookup": "^9.0.1",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"he": "^1.2.0",
"luxon": "^3.4.4"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.76.0",
"exiftool-vendored.pl": "12.76.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.76.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz",
"integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==",
"dev": true,
"optional": true,
"os": [
"win32"
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.76.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz",
"integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA==",
"dev": true,
"optional": true,
"os": [
"!win32"
]
},
"node_modules/fast-safe-stringify": { "node_modules/fast-safe-stringify": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
@@ -1637,15 +1547,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"bin": {
"he": "bin/he"
}
},
"node_modules/hexoid": { "node_modules/hexoid": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
@@ -1803,15 +1704,6 @@
"node": ">=10" "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": { "node_modules/magic-string": {
"version": "0.30.7", "version": "0.30.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
@@ -2454,34 +2346,6 @@
"url": "https://github.com/sponsors/isaacs" "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": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -2879,36 +2743,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true "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": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
-4
View File
@@ -16,15 +16,11 @@
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.41.2", "@playwright/test": "^1.41.2",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"@vitest/coverage-v8": "^1.3.0", "@vitest/coverage-v8": "^1.3.0",
"exiftool-vendored": "^24.5.0",
"luxon": "^3.4.4",
"pg": "^8.11.3", "pg": "^8.11.3",
"socket.io-client": "^4.7.4",
"supertest": "^6.3.4", "supertest": "^6.3.4",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vitest": "^1.3.0" "vitest": "^1.3.0"
+13 -13
View File
@@ -1,7 +1,7 @@
import { import {
ActivityCreateDto, ActivityCreateDto,
AlbumResponseDto, AlbumResponseDto,
AssetFileUploadResponseDto, AssetResponseDto,
LoginResponseDto, LoginResponseDto,
ReactionType, ReactionType,
createActivity as create, createActivity as create,
@@ -16,13 +16,13 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activity', () => { describe('/activity', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let nonOwner: LoginResponseDto; let nonOwner: LoginResponseDto;
let asset: AssetFileUploadResponseDto; let asset: AssetResponseDto;
let album: AlbumResponseDto; let album: AlbumResponseDto;
const createActivity = (dto: ActivityCreateDto, accessToken?: string) => const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
create( create(
{ activityCreateDto: dto }, { activityCreateDto: dto },
{ headers: asBearerAuth(accessToken || admin.accessToken) }, { headers: asBearerAuth(accessToken || admin.accessToken) }
); );
beforeAll(async () => { beforeAll(async () => {
@@ -40,7 +40,7 @@ describe('/activity', () => {
sharedWithUserIds: [nonOwner.userId], sharedWithUserIds: [nonOwner.userId],
}, },
}, },
{ headers: asBearerAuth(admin.accessToken) }, { headers: asBearerAuth(admin.accessToken) }
); );
}); });
@@ -61,7 +61,7 @@ describe('/activity', () => {
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( 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}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( 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}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( 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], assetIds: [asset.id],
}, },
}, },
{ headers: asBearerAuth(admin.accessToken) }, { headers: asBearerAuth(admin.accessToken) }
); );
const [reaction] = await Promise.all([ const [reaction] = await Promise.all([
@@ -216,7 +216,7 @@ describe('/activity', () => {
.send({ albumId: uuidDto.invalid }); .send({ albumId: uuidDto.invalid });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( 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([ errorDto.badRequest([
'comment must be a string', 'comment must be a string',
'comment should not be empty', 'comment should not be empty',
]), ])
); );
}); });
@@ -357,7 +357,7 @@ describe('/activity', () => {
describe('DELETE /activity/:id', () => { describe('DELETE /activity/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete( const { status, body } = await request(app).delete(
`/activity/${uuidDto.notFound}`, `/activity/${uuidDto.notFound}`
); );
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -421,7 +421,7 @@ describe('/activity', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( 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, type: ReactionType.Comment,
comment: 'This is a test comment', comment: 'This is a test comment',
}, },
nonOwner.accessToken, nonOwner.accessToken
); );
const { status } = await request(app) const { status } = await request(app)
+12 -12
View File
@@ -1,6 +1,6 @@
import { import {
AlbumResponseDto, AlbumResponseDto,
AssetFileUploadResponseDto, AssetResponseDto,
LoginResponseDto, LoginResponseDto,
SharedLinkType, SharedLinkType,
deleteUser, deleteUser,
@@ -21,8 +21,8 @@ const user2NotShared = 'user2NotShared';
describe('/album', () => { describe('/album', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
let user1Asset1: AssetFileUploadResponseDto; let user1Asset1: AssetResponseDto;
let user1Asset2: AssetFileUploadResponseDto; let user1Asset2: AssetResponseDto;
let user1Albums: AlbumResponseDto[]; let user1Albums: AlbumResponseDto[];
let user2: LoginResponseDto; let user2: LoginResponseDto;
let user2Albums: AlbumResponseDto[]; let user2Albums: AlbumResponseDto[];
@@ -95,7 +95,7 @@ describe('/album', () => {
await deleteUser( await deleteUser(
{ id: user3.userId }, { id: user3.userId },
{ headers: asBearerAuth(admin.accessToken) }, { headers: asBearerAuth(admin.accessToken) }
); );
}); });
@@ -112,7 +112,7 @@ describe('/album', () => {
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( 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, albumName: user2SharedUser,
shared: true, shared: true,
}), }),
]), ])
); );
}); });
@@ -175,7 +175,7 @@ describe('/album', () => {
albumName: user1NotShared, albumName: user1NotShared,
shared: false, shared: false,
}), }),
]), ])
); );
}); });
@@ -202,7 +202,7 @@ describe('/album', () => {
albumName: user2SharedUser, albumName: user2SharedUser,
shared: true, shared: true,
}), }),
]), ])
); );
}); });
@@ -219,7 +219,7 @@ describe('/album', () => {
albumName: user1NotShared, albumName: user1NotShared,
shared: false, shared: false,
}), }),
]), ])
); );
}); });
@@ -251,7 +251,7 @@ describe('/album', () => {
describe('GET /album/:id', () => { describe('GET /album/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get(
`/album/${user1Albums[0].id}`, `/album/${user1Albums[0].id}`
); );
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -361,7 +361,7 @@ describe('/album', () => {
describe('PUT /album/:id/assets', () => { describe('PUT /album/:id/assets', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put( const { status, body } = await request(app).put(
`/album/${user1Albums[0].id}/assets`, `/album/${user1Albums[0].id}/assets`
); );
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -519,7 +519,7 @@ describe('/album', () => {
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ expect.objectContaining({
sharedUsers: [expect.objectContaining({ id: user2.userId })], sharedUsers: [expect.objectContaining({ id: user2.userId })],
}), })
); );
}); });
-808
View File
@@ -1,808 +0,0 @@
import {
AssetFileUploadResponseDto,
AssetResponseDto,
AssetTypeEnum,
LoginResponseDto,
SharedLinkType,
} from '@immich/sdk';
import { exiftool } from 'exiftool-vendored';
import { DateTime } from 'luxon';
import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import {
apiUtils,
app,
dbUtils,
tempDir,
testAssetDir,
wsUtils,
} from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const sha1 = (bytes: Buffer) =>
createHash('sha1').update(bytes).digest('base64');
const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename);
await writeFile(filepath, bytes);
return exiftool.read(filepath);
};
const today = DateTime.fromObject({
year: 2023,
month: 11,
day: 3,
}) as DateTime<true>;
const yesterday = today.minus({ days: 1 });
describe('/asset', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
let userStats: LoginResponseDto;
let user1Assets: AssetFileUploadResponseDto[];
let user2Assets: AssetFileUploadResponseDto[];
let assetLocation: AssetFileUploadResponseDto;
let ws: Socket;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup({ onboarding: false });
[ws, user1, user2, userStats] = await Promise.all([
wsUtils.connect(admin.accessToken),
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
]);
// asset location
assetLocation = await apiUtils.createAsset(
admin.accessToken,
{},
{
filename: 'thompson-springs.jpg',
bytes: await readFile(locationAssetFilepath),
},
);
await wsUtils.waitForEvent({ event: 'upload', assetId: assetLocation.id });
user1Assets = 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(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
]);
user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]);
await Promise.all([
// 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: user1Assets[0].id,
personId: person1.id,
});
}, 30_000);
afterAll(() => {
wsUtils.disconnect(ws);
});
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/${user2Assets[0].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/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id });
});
it('should work with a shared link', async () => {
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
assetIds: [user1Assets[0].id],
});
const { status, body } = await request(app).get(
`/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id });
});
it('should not send people data for shared links for un-authenticated users', async () => {
const { status, body } = await request(app)
.get(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(200);
expect(body).toMatchObject({
id: user1Assets[0].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: [user1Assets[0].id],
});
const data = await request(app).get(
`/asset/${user1Assets[0].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 user1
expect([user1Assets.map(({ id }) => id)]).toContain(assets[0].id);
// assets owned by user2
expect([user1Assets.map(({ id }) => id)]).not.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([user1Assets.map(({ id }) => id)]).toContain(asset.id);
// assets owned by user2
expect([user2Assets.map(({ id }) => id)]).not.toContain(asset.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: user2Assets[0].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/${user2Assets[0].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,
user1Assets[0].id,
);
expect(before.isFavorite).toBe(false);
const { status, body } = await request(app)
.put(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isFavorite: true });
expect(body).toMatchObject({ id: user1Assets[0].id, isFavorite: true });
expect(status).toEqual(200);
});
it('should archive an asset', async () => {
const before = await apiUtils.getAssetInfo(
user1.accessToken,
user1Assets[0].id,
);
expect(before.isArchived).toBe(false);
const { status, body } = await request(app)
.put(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isArchived: true });
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
expect(status).toEqual(200);
});
it('should update date time original', async () => {
const { status, body } = await request(app)
.put(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
expect(body).toMatchObject({
id: user1Assets[0].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/${user1Assets[0].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/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ latitude: 12, longitude: 12 });
expect(body).toMatchObject({
id: user1Assets[0].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/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'Test asset description' });
expect(body).toMatchObject({
id: user1Assets[0].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/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isFavorite: true });
expect(status).toEqual(200);
expect(body).toMatchObject({
id: user1Assets[0].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);
});
});
describe('POST /asset/upload', () => {
const tests = [
{
input: 'formats/jpg/el_torcal_rocks.jpg',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'el_torcal_rocks',
resized: true,
exifInfo: {
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
exifImageWidth: 512,
exifImageHeight: 341,
latitude: null,
longitude: null,
focalLength: 75,
iso: 200,
fNumber: 11,
exposureTime: '1/160',
fileSizeInByte: 53_493,
make: 'SONY',
model: 'DSLR-A550',
orientation: null,
description: 'SONY DSC',
},
},
},
{
input: 'formats/heic/IMG_2682.heic',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'IMG_2682',
resized: true,
fileCreatedAt: '2019-03-21T16:04:22.348Z',
exifInfo: {
dateTimeOriginal: '2019-03-21T16:04:22.348Z',
exifImageWidth: 4032,
exifImageHeight: 3024,
latitude: 41.2203,
longitude: -96.071_625,
make: 'Apple',
model: 'iPhone 7',
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
fileSizeInByte: 880_703,
exposureTime: '1/887',
iso: 20,
focalLength: 3.99,
fNumber: 1.8,
timeZone: 'America/Chicago',
},
},
},
{
input: 'formats/png/density_plot.png',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'density_plot',
resized: true,
exifInfo: {
exifImageWidth: 800,
exifImageHeight: 800,
latitude: null,
longitude: null,
fileSizeInByte: 25_408,
},
},
},
{
input: 'formats/raw/Nikon/D80/glarus.nef',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'glarus',
resized: true,
fileCreatedAt: '2010-07-20T17:27:12.000Z',
exifInfo: {
make: 'NIKON CORPORATION',
model: 'NIKON D80',
exposureTime: '1/200',
fNumber: 10,
focalLength: 18,
iso: 100,
fileSizeInByte: 9_057_784,
dateTimeOriginal: '2010-07-20T17:27:12.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
{
input: 'formats/raw/Nikon/D700/philadelphia.nef',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'philadelphia',
resized: true,
fileCreatedAt: '2016-09-22T22:10:29.060Z',
exifInfo: {
make: 'NIKON CORPORATION',
model: 'NIKON D700',
exposureTime: '1/400',
fNumber: 11,
focalLength: 85,
iso: 200,
fileSizeInByte: 15_856_335,
dateTimeOriginal: '2016-09-22T22:10:29.060Z',
latitude: null,
longitude: null,
orientation: '1',
timeZone: 'UTC-5',
},
},
},
];
for (const { input, expected } of tests) {
it(`should generate a thumbnail for ${input}`, async () => {
const filepath = join(testAssetDir, input);
const { id, duplicate } = await apiUtils.createAsset(
admin.accessToken,
{},
{ bytes: await readFile(filepath), filename: basename(filepath) },
);
expect(duplicate).toBe(false);
await wsUtils.waitForEvent({ event: 'upload', assetId: id });
const asset = await apiUtils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
});
}
it('should handle a duplicate', async () => {
const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
const { duplicate } = await apiUtils.createAsset(
admin.accessToken,
{},
{
bytes: await readFile(join(testAssetDir, filepath)),
filename: basename(filepath),
},
);
expect(duplicate).toBe(true);
});
// These hashes were created by copying the image files to a Samsung phone,
// exporting the video from Samsung's stock Gallery app, and hashing them locally.
// This ensures that immich+exiftool are extracting the videos the same way Samsung does.
// DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
// into the test here.
const motionTests = [
{
filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
},
{
filepath: 'formats/motionphoto/Samsung One UI 6.jpg',
checksum: 'lT9Uviw/FFJYCjfIxAGPTjzAmmw=',
},
{
filepath: 'formats/motionphoto/Samsung One UI 6.heic',
checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
},
];
for (const { filepath, checksum } of motionTests) {
it(`should extract motionphoto video from ${filepath}`, async () => {
const response = await apiUtils.createAsset(
admin.accessToken,
{},
{
bytes: await readFile(join(testAssetDir, filepath)),
filename: basename(filepath),
},
);
await wsUtils.waitForEvent({ event: 'upload', assetId: response.id });
expect(response.duplicate).toBe(false);
const asset = await apiUtils.getAssetInfo(
admin.accessToken,
response.id,
);
expect(asset.livePhotoVideoId).toBeDefined();
const video = await apiUtils.getAssetInfo(
admin.accessToken,
asset.livePhotoVideoId as string,
);
expect(video.checksum).toStrictEqual(checksum);
});
}
});
describe('GET /asset/thumbnail/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/asset/thumbnail/${assetLocation.id}`,
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not include gps data for webp thumbnails', async () => {
const { status, body, type } = await request(app)
.get(`/asset/thumbnail/${assetLocation.id}?format=WEBP`)
.set('Authorization', `Bearer ${admin.accessToken}`);
await wsUtils.waitForEvent({
event: 'upload',
assetId: assetLocation.id,
});
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/webp');
const exifData = await readTags(body, 'thumbnail.webp');
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
it('should not include gps data for jpeg thumbnails', async () => {
const { status, body, type } = await request(app)
.get(`/asset/thumbnail/${assetLocation.id}?format=JPEG`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/jpeg');
const exifData = await readTags(body, 'thumbnail.jpg');
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
});
describe('GET /asset/file/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/asset/thumbnail/${assetLocation.id}`,
);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download the original', async () => {
const { status, body, type } = await request(app)
.get(`/asset/file/${assetLocation.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/jpeg');
const asset = await apiUtils.getAssetInfo(
admin.accessToken,
assetLocation.id,
);
const original = await readFile(locationAssetFilepath);
const originalChecksum = sha1(original);
const downloadChecksum = sha1(body);
expect(originalChecksum).toBe(downloadChecksum);
expect(downloadChecksum).toBe(asset.checksum);
});
});
});
+2 -2
View File
@@ -29,14 +29,14 @@ describe('/audit', () => {
await Promise.all([ await Promise.all([
deleteAssets( deleteAssets(
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { assetBulkDeleteDto: { ids: [trashedAsset.id] } },
{ headers: asBearerAuth(admin.accessToken) }, { headers: asBearerAuth(admin.accessToken) }
), ),
updateAsset( updateAsset(
{ {
id: archivedAsset.id, id: archivedAsset.id,
updateAssetDto: { isArchived: true }, updateAssetDto: { isArchived: true },
}, },
{ headers: asBearerAuth(admin.accessToken) }, { headers: asBearerAuth(admin.accessToken) }
), ),
]); ]);
+4 -4
View File
@@ -1,4 +1,4 @@
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk'; import { AssetResponseDto, LoginResponseDto } from '@immich/sdk';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { apiUtils, app, dbUtils } from 'src/utils'; import { apiUtils, app, dbUtils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
@@ -6,7 +6,7 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/download', () => { describe('/download', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let asset1: AssetFileUploadResponseDto; let asset1: AssetResponseDto;
beforeAll(async () => { beforeAll(async () => {
apiUtils.setup(); apiUtils.setup();
@@ -35,7 +35,7 @@ describe('/download', () => {
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ expect.objectContaining({
archives: [expect.objectContaining({ assetIds: [asset1.id] })], archives: [expect.objectContaining({ assetIds: [asset1.id] })],
}), })
); );
}); });
}); });
@@ -43,7 +43,7 @@ describe('/download', () => {
describe('POST /download/asset/:id', () => { describe('POST /download/asset/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post( const { status, body } = await request(app).post(
`/download/asset/${asset1.id}`, `/download/asset/${asset1.id}`
); );
expect(status).toBe(401); expect(status).toBe(401);
+21 -19
View File
@@ -1,9 +1,11 @@
import { import {
AlbumResponseDto, AlbumResponseDto,
AssetFileUploadResponseDto, AssetResponseDto,
LoginResponseDto, LoginResponseDto,
SharedLinkCreateDto,
SharedLinkResponseDto, SharedLinkResponseDto,
SharedLinkType, SharedLinkType,
createSharedLink as create,
createAlbum, createAlbum,
deleteUser, deleteUser,
} from '@immich/sdk'; } from '@immich/sdk';
@@ -15,8 +17,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/shared-link', () => { describe('/shared-link', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let asset1: AssetFileUploadResponseDto; let asset1: AssetResponseDto;
let asset2: AssetFileUploadResponseDto; let asset2: AssetResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
let user2: LoginResponseDto; let user2: LoginResponseDto;
let album: AlbumResponseDto; let album: AlbumResponseDto;
@@ -48,11 +50,11 @@ describe('/shared-link', () => {
[album, deletedAlbum, metadataAlbum] = await Promise.all([ [album, deletedAlbum, metadataAlbum] = await Promise.all([
createAlbum( createAlbum(
{ createAlbumDto: { albumName: 'album' } }, { createAlbumDto: { albumName: 'album' } },
{ headers: asBearerAuth(user1.accessToken) }, { headers: asBearerAuth(user1.accessToken) }
), ),
createAlbum( createAlbum(
{ createAlbumDto: { albumName: 'deleted album' } }, { createAlbumDto: { albumName: 'deleted album' } },
{ headers: asBearerAuth(user2.accessToken) }, { headers: asBearerAuth(user2.accessToken) }
), ),
createAlbum( createAlbum(
{ {
@@ -61,7 +63,7 @@ describe('/shared-link', () => {
assetIds: [asset1.id], assetIds: [asset1.id],
}, },
}, },
{ headers: asBearerAuth(user1.accessToken) }, { headers: asBearerAuth(user1.accessToken) }
), ),
]); ]);
@@ -104,7 +106,7 @@ describe('/shared-link', () => {
await deleteUser( await deleteUser(
{ id: user2.userId }, { id: user2.userId },
{ headers: asBearerAuth(admin.accessToken) }, { headers: asBearerAuth(admin.accessToken) }
); );
}); });
@@ -130,7 +132,7 @@ describe('/shared-link', () => {
expect.objectContaining({ id: linkWithPassword.id }), expect.objectContaining({ id: linkWithPassword.id }),
expect.objectContaining({ id: linkWithMetadata.id }), expect.objectContaining({ id: linkWithMetadata.id }),
expect.objectContaining({ id: linkWithoutMetadata.id }), expect.objectContaining({ id: linkWithoutMetadata.id }),
]), ])
); );
}); });
@@ -164,7 +166,7 @@ describe('/shared-link', () => {
album, album,
userId: user1.userId, userId: user1.userId,
type: SharedLinkType.Album, type: SharedLinkType.Album,
}), })
); );
}); });
@@ -206,7 +208,7 @@ describe('/shared-link', () => {
album, album,
userId: user1.userId, userId: user1.userId,
type: SharedLinkType.Album, type: SharedLinkType.Album,
}), })
); );
}); });
@@ -223,7 +225,7 @@ describe('/shared-link', () => {
localDateTime: expect.any(String), localDateTime: expect.any(String),
fileCreatedAt: expect.any(String), fileCreatedAt: expect.any(String),
exifInfo: expect.any(Object), exifInfo: expect.any(Object),
}), })
); );
expect(body.album).toBeDefined(); expect(body.album).toBeDefined();
}); });
@@ -248,7 +250,7 @@ describe('/shared-link', () => {
describe('GET /shared-link/:id', () => { describe('GET /shared-link/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get( const { status, body } = await request(app).get(
`/shared-link/${linkWithAlbum.id}`, `/shared-link/${linkWithAlbum.id}`
); );
expect(status).toBe(401); expect(status).toBe(401);
@@ -266,7 +268,7 @@ describe('/shared-link', () => {
album, album,
userId: user1.userId, userId: user1.userId,
type: SharedLinkType.Album, type: SharedLinkType.Album,
}), })
); );
}); });
@@ -277,7 +279,7 @@ describe('/shared-link', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ message: 'Shared link not found' }), expect.objectContaining({ message: 'Shared link not found' })
); );
}); });
}); });
@@ -309,7 +311,7 @@ describe('/shared-link', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ message: 'Invalid albumId' }), expect.objectContaining({ message: 'Invalid albumId' })
); );
}); });
@@ -321,7 +323,7 @@ describe('/shared-link', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ message: 'Invalid assetIds' }), expect.objectContaining({ message: 'Invalid assetIds' })
); );
}); });
@@ -336,7 +338,7 @@ describe('/shared-link', () => {
expect.objectContaining({ expect.objectContaining({
type: SharedLinkType.Album, type: SharedLinkType.Album,
userId: user1.userId, userId: user1.userId,
}), })
); );
}); });
}); });
@@ -373,7 +375,7 @@ describe('/shared-link', () => {
type: SharedLinkType.Album, type: SharedLinkType.Album,
userId: user1.userId, userId: user1.userId,
description: 'foo', description: 'foo',
}), })
); );
}); });
}); });
@@ -425,7 +427,7 @@ describe('/shared-link', () => {
describe('DELETE /shared-link/:id', () => { describe('DELETE /shared-link/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete( const { status, body } = await request(app).delete(
`/shared-link/${linkWithAlbum.id}`, `/shared-link/${linkWithAlbum.id}`
); );
expect(status).toBe(401); expect(status).toBe(401);
-107
View File
@@ -1,107 +0,0 @@
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.waitForEvent({ event: 'delete', assetId });
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);
});
});
});
+5 -2
View File
@@ -1,9 +1,12 @@
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils'; import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe(`immich server-info`, () => { describe(`immich server-info`, () => {
beforeAll(async () => { beforeAll(() => {
apiUtils.setup(); apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset(); await dbUtils.reset();
await cliUtils.login(); await cliUtils.login();
}); });
+12 -13
View File
@@ -1,5 +1,4 @@
import { getAllAlbums, getAllAssets } from '@immich/sdk'; import { getAllAlbums, getAllAssets } from '@immich/sdk';
import { mkdir, readdir, rm, symlink } from 'fs/promises';
import { import {
apiUtils, apiUtils,
asKeyAuth, asKeyAuth,
@@ -9,18 +8,18 @@ import {
testAssetDir, testAssetDir,
} from 'src/utils'; } from 'src/utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { mkdir, readdir, rm, symlink } from 'fs/promises';
describe(`immich upload`, () => { describe(`immich upload`, () => {
let key: string; let key: string;
beforeAll(async () => { beforeAll(() => {
apiUtils.setup(); apiUtils.setup();
await dbUtils.reset();
key = await cliUtils.login();
}); });
beforeEach(async () => { beforeEach(async () => {
await dbUtils.reset(['assets', 'albums']); await dbUtils.reset();
key = await cliUtils.login();
}); });
describe('immich upload --recursive', () => { describe('immich upload --recursive', () => {
@@ -34,7 +33,7 @@ describe(`immich upload`, () => {
expect(stdout.split('\n')).toEqual( expect(stdout.split('\n')).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.stringContaining('Successfully uploaded 9 assets'), expect.stringContaining('Successfully uploaded 9 assets'),
]), ])
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
@@ -56,7 +55,7 @@ describe(`immich upload`, () => {
expect.stringContaining('Successfully uploaded 9 assets'), expect.stringContaining('Successfully uploaded 9 assets'),
expect.stringContaining('Successfully created 1 new album'), expect.stringContaining('Successfully created 1 new album'),
expect.stringContaining('Successfully updated 9 assets'), expect.stringContaining('Successfully updated 9 assets'),
]), ])
); );
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
@@ -78,7 +77,7 @@ describe(`immich upload`, () => {
expect(response1.stdout.split('\n')).toEqual( expect(response1.stdout.split('\n')).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.stringContaining('Successfully uploaded 9 assets'), expect.stringContaining('Successfully uploaded 9 assets'),
]), ])
); );
expect(response1.stderr).toBe(''); expect(response1.stderr).toBe('');
expect(response1.exitCode).toBe(0); expect(response1.exitCode).toBe(0);
@@ -98,10 +97,10 @@ describe(`immich upload`, () => {
expect(response2.stdout.split('\n')).toEqual( expect(response2.stdout.split('\n')).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.stringContaining( 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.stringContaining('Successfully updated 9 assets'),
]), ])
); );
expect(response2.stderr).toBe(''); expect(response2.stderr).toBe('');
expect(response2.exitCode).toBe(0); expect(response2.exitCode).toBe(0);
@@ -128,7 +127,7 @@ describe(`immich upload`, () => {
expect.stringContaining('Successfully uploaded 9 assets'), expect.stringContaining('Successfully uploaded 9 assets'),
expect.stringContaining('Successfully created 1 new album'), expect.stringContaining('Successfully created 1 new album'),
expect.stringContaining('Successfully updated 9 assets'), expect.stringContaining('Successfully updated 9 assets'),
]), ])
); );
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
@@ -149,7 +148,7 @@ describe(`immich upload`, () => {
for (const file of filesToLink) { for (const file of filesToLink) {
await symlink( await symlink(
`${testAssetDir}/albums/nature/${file}`, `${testAssetDir}/albums/nature/${file}`,
`/tmp/albums/nature/${file}`, `/tmp/albums/nature/${file}`
); );
} }
@@ -167,7 +166,7 @@ describe(`immich upload`, () => {
expect.arrayContaining([ expect.arrayContaining([
expect.stringContaining('Successfully uploaded 9 assets'), expect.stringContaining('Successfully uploaded 9 assets'),
expect.stringContaining('Deleting assets that have been uploaded'), expect.stringContaining('Deleting assets that have been uploaded'),
]), ])
); );
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
+4 -4
View File
@@ -2,25 +2,25 @@ import { spawn, exec } from 'child_process';
export default async () => { export default async () => {
let _resolve: () => unknown; let _resolve: () => unknown;
const ready = new Promise<void>((resolve) => (_resolve = resolve)); const promise = new Promise<void>((resolve) => (_resolve = resolve));
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' }); const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
child.stdout.on('data', (data) => { child.stdout.on('data', (data) => {
const input = data.toString(); const input = data.toString();
console.log(input); console.log(input);
if (input.includes('Immich Microservices is listening')) { if (input.includes('Immich Server is listening')) {
_resolve(); _resolve();
} }
}); });
child.stderr.on('data', (data) => console.log(data.toString())); child.stderr.on('data', (data) => console.log(data.toString()));
await ready; await promise;
return async () => { return async () => {
await new Promise<void>((resolve) => await new Promise<void>((resolve) =>
exec('docker compose down', () => resolve()), exec('docker compose down', () => resolve())
); );
}; };
}; };
+27 -149
View File
@@ -1,5 +1,4 @@
import { import {
AssetFileUploadResponseDto,
AssetResponseDto, AssetResponseDto,
CreateAlbumDto, CreateAlbumDto,
CreateAssetDto, CreateAssetDto,
@@ -12,23 +11,18 @@ import {
createSharedLink, createSharedLink,
createUser, createUser,
defaults, defaults,
deleteAssets,
getAssetInfo,
login, login,
setAdminOnboarding, setAdminOnboarding,
signUpAdmin, signUpAdmin,
updatePerson, updatePerson,
} from '@immich/sdk'; } from '@immich/sdk';
import { BrowserContext } from '@playwright/test'; import { BrowserContext } from '@playwright/test';
import { exec, spawn } from 'node:child_process'; import { exec, spawn } from 'child_process';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
import { access } from 'node:fs/promises'; import { access } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { EventEmitter } from 'node:stream';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import pg from 'pg'; import pg from 'pg';
import { io, type Socket } from 'socket.io-client';
import { loginDto, signupDto } from 'src/fixtures'; import { loginDto, signupDto } from 'src/fixtures';
import request from 'supertest'; import request from 'supertest';
@@ -43,23 +37,17 @@ const directoryExists = (directory: string) =>
// TODO move test assets into e2e/assets // TODO move test assets into e2e/assets
export const testAssetDir = path.resolve(`./../server/test/assets/`); export const testAssetDir = path.resolve(`./../server/test/assets/`);
export const tempDir = tmpdir();
const serverContainerName = 'immich-e2e-server'; const serverContainerName = 'immich-e2e-server';
const mediaDir = '/usr/src/app/upload'; const uploadMediaDir = '/usr/src/app/upload/upload';
const dirs = [
`"${mediaDir}/thumbs"`,
`"${mediaDir}/upload"`,
`"${mediaDir}/library"`,
`"${mediaDir}/encoded-video"`,
].join(' ');
if (!(await directoryExists(`${testAssetDir}/albums`))) { if (!(await directoryExists(`${testAssetDir}/albums`))) {
throw new Error( 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) => ({ export const asBearerAuth = (accessToken: string) => ({
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}); });
@@ -71,7 +59,7 @@ let client: pg.Client | null = null;
export const fileUtils = { export const fileUtils = {
reset: async () => { reset: async () => {
await execPromise( await execPromise(
`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`, `docker exec -i "${serverContainerName}" rm -R "${uploadMediaDir}"`
); );
}, },
}; };
@@ -93,7 +81,7 @@ export const dbUtils = {
await client.query( await client.query(
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', 'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
[assetId, personId, embedding], [assetId, personId, embedding]
); );
}, },
setPersonThumbnail: async (personId: string) => { setPersonThumbnail: async (personId: string) => {
@@ -103,14 +91,14 @@ export const dbUtils = {
await client.query( await client.query(
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, `UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
[personId], [personId]
); );
}, },
reset: async (tables?: string[]) => { reset: async (tables?: string[]) => {
try { try {
if (!client) { if (!client) {
client = new pg.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(); await client.connect();
} }
@@ -182,94 +170,10 @@ export interface AdminSetupOptions {
onboarding?: boolean; onboarding?: boolean;
} }
export enum SocketEvent {
UPLOAD = 'upload',
DELETE = 'delete',
}
export type EventType = 'upload' | 'delete';
export interface WaitOptions {
event: EventType;
assetId: string;
timeout?: number;
}
const events: Record<EventType, Set<string>> = {
upload: new Set<string>(),
delete: new Set<string>(),
};
const callbacks: Record<string, () => void> = {};
const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => {
events[event].add(assetId);
const callback = callbacks[assetId];
if (callback) {
callback();
delete callbacks[assetId];
}
};
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: true,
forceNew: true,
});
return new Promise<Socket>((resolve) => {
websocket
.on('connect', () => resolve(websocket))
.on('on_upload_success', (data: AssetResponseDto) =>
onEvent({ event: 'upload', assetId: data.id }),
)
.on('on_asset_delete', (assetId: string) =>
onEvent({ event: 'delete', assetId }),
)
.connect();
});
},
disconnect: (ws: Socket) => {
if (ws?.connected) {
ws.disconnect();
}
for (const set of Object.values(events)) {
set.clear();
}
},
waitForEvent: async ({
event,
assetId,
timeout: ms,
}: WaitOptions): Promise<void> => {
const set = events[event];
if (set.has(assetId)) {
return;
}
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error(`Timed out waiting for ${event} event`)),
ms || 5000,
);
callbacks[assetId] = () => {
clearTimeout(timeout);
resolve();
};
});
},
};
export const apiUtils = { export const apiUtils = {
setup: () => { setup: () => {
defaults.baseUrl = app; setBaseUrl();
}, },
adminSetup: async (options?: AdminSetupOptions) => { adminSetup: async (options?: AdminSetupOptions) => {
options = options || { onboarding: true }; options = options || { onboarding: true };
@@ -283,7 +187,7 @@ export const apiUtils = {
userSetup: async (accessToken: string, dto: CreateUserDto) => { userSetup: async (accessToken: string, dto: CreateUserDto) => {
await createUser( await createUser(
{ createUserDto: dto }, { createUserDto: dto },
{ headers: asBearerAuth(accessToken) }, { headers: asBearerAuth(accessToken) }
); );
return login({ return login({
loginCredentialDto: { email: dto.email, password: dto.password }, loginCredentialDto: { email: dto.email, password: dto.password },
@@ -292,74 +196,48 @@ export const apiUtils = {
createApiKey: (accessToken: string) => { createApiKey: (accessToken: string) => {
return createApiKey( return createApiKey(
{ apiKeyCreateDto: { name: 'e2e' } }, { apiKeyCreateDto: { name: 'e2e' } },
{ headers: asBearerAuth(accessToken) }, { headers: asBearerAuth(accessToken) }
); );
}, },
createAlbum: (accessToken: string, dto: CreateAlbumDto) => createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
createAlbum( createAlbum(
{ createAlbumDto: dto }, { createAlbumDto: dto },
{ headers: asBearerAuth(accessToken) }, { headers: asBearerAuth(accessToken) }
), ),
createAsset: async ( createAsset: async (
accessToken: string, accessToken: string,
dto?: Partial<Omit<CreateAssetDto, 'assetData'>>, dto?: Omit<CreateAssetDto, 'assetData'>
data?: {
bytes?: Buffer;
filename?: string;
},
) => { ) => {
const _dto = { dto = dto || {
deviceAssetId: 'test-1', deviceAssetId: 'test-1',
deviceId: 'test', deviceId: 'test',
fileCreatedAt: new Date().toISOString(), fileCreatedAt: new Date().toISOString(),
fileModifiedAt: 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`) .post(`/asset/upload`)
.attach('assetData', _assetData.bytes, _assetData.filename) .field('deviceAssetId', dto.deviceAssetId)
.field('deviceId', dto.deviceId)
.field('fileCreatedAt', dto.fileCreatedAt)
.field('fileModifiedAt', dto.fileModifiedAt)
.attach('assetData', randomBytes(32), 'example.jpg')
.set('Authorization', `Bearer ${accessToken}`); .set('Authorization', `Bearer ${accessToken}`);
for (const [key, value] of Object.entries(_dto)) { return body as AssetResponseDto;
builder.field(key, String(value));
}
const { body } = await builder;
return body as AssetFileUploadResponseDto;
}, },
getAssetInfo: (accessToken: string, id: string) => createPerson: async (accessToken: string, dto: PersonUpdateDto) => {
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 // TODO fix createPerson to accept a body
let person = await createPerson({ headers: asBearerAuth(accessToken) }); const { id } = await createPerson({ headers: asBearerAuth(accessToken) });
await dbUtils.setPersonThumbnail(person.id); await dbUtils.setPersonThumbnail(id);
if (!dto) {
return person;
}
return updatePerson( return updatePerson(
{ id: person.id, personUpdateDto: dto }, { id, personUpdateDto: dto },
{ headers: asBearerAuth(accessToken) }, { headers: asBearerAuth(accessToken) }
); );
}, },
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) => createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
createSharedLink( createSharedLink(
{ sharedLinkCreateDto: dto }, { sharedLinkCreateDto: dto },
{ headers: asBearerAuth(accessToken) }, { headers: asBearerAuth(accessToken) }
), ),
}; };
+10 -22
View File
@@ -15,7 +15,6 @@ test.describe('Shared Links', () => {
let asset: AssetResponseDto; let asset: AssetResponseDto;
let album: AlbumResponseDto; let album: AlbumResponseDto;
let sharedLink: SharedLinkResponseDto; let sharedLink: SharedLinkResponseDto;
let sharedLinkPassword: SharedLinkResponseDto;
test.beforeAll(async () => { test.beforeAll(async () => {
apiUtils.setup(); apiUtils.setup();
@@ -30,16 +29,17 @@ test.describe('Shared Links', () => {
}, },
}, },
{ headers: asBearerAuth(admin.accessToken) } { 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 () => { test.afterAll(async () => {
@@ -55,16 +55,4 @@ test.describe('Shared Links', () => {
await page.getByRole('button', { name: 'Download' }).click(); await page.getByRole('button', { name: 'Download' }).click();
await page.getByText('DOWNLOADING').waitFor(); 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();
});
}); });
+9 -9
View File
@@ -165,14 +165,6 @@ class InferenceModel(ABC):
def providers_default(self) -> list[str]: def providers_default(self) -> list[str]:
available_providers = set(ort.get_available_providers()) available_providers = set(ort.get_available_providers())
log.debug(f"Available ORT providers: {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] return [provider for provider in SUPPORTED_PROVIDERS if provider in available_providers]
@property @property
@@ -192,7 +184,15 @@ class InferenceModel(ABC):
case "CPUExecutionProvider" | "CUDAExecutionProvider": case "CPUExecutionProvider" | "CUDAExecutionProvider":
option = {"arena_extend_strategy": "kSameAsRequested"} option = {"arena_extend_strategy": "kSameAsRequested"}
case "OpenVINOExecutionProvider": case "OpenVINOExecutionProvider":
option = {"device_type": "GPU_FP32"} 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 = {}
case _: case _:
option = {} option = {}
options.append(option) options.append(option)
+11 -15
View File
@@ -45,23 +45,11 @@ class TestBase:
assert encoder.providers == self.CUDA_EP assert encoder.providers == self.CUDA_EP
@pytest.mark.providers(OV_EP) @pytest.mark.providers(OV_EP)
def test_sets_openvino_provider_if_available(self, providers: list[str], mocker: MockerFixture) -> None: def test_sets_openvino_provider_if_available(self, providers: list[str]) -> 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") encoder = OpenCLIPEncoder("ViT-B-32__openai")
assert encoder.providers == self.OV_EP 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) @pytest.mark.providers(CUDA_EP_OUT_OF_ORDER)
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None: def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenCLIPEncoder("ViT-B-32__openai")
@@ -80,14 +68,22 @@ class TestBase:
assert encoder.providers == providers assert encoder.providers == providers
def test_sets_default_provider_options(self, mocker: MockerFixture) -> None: 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:
mocked = mocker.patch("app.models.base.ort.capi._pybind_state") mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"] mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"]) encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
assert encoder.provider_options == [ assert encoder.provider_options == [
{"device_type": "GPU_FP32"}, {"device_id": "GPU.0"},
{"arena_extend_strategy": "kSameAsRequested"}, {"arena_extend_strategy": "kSameAsRequested"},
] ]
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:6038b89363c9181215f3d9e8ce2720c880e224537f4028a854482e43a9b4998a as builder FROM mambaorg/micromamba:bookworm-slim@sha256:926cac38640709f90f3fef2a3f730733b5c350be612f0d14706be8833b79ad8c as builder
ENV NODE_ENV=production \ ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \ TRANSFORMERS_CACHE=/cache \
+75 -75
View File
@@ -1250,13 +1250,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
[[package]] [[package]]
name = "httpx" name = "httpx"
version = "0.27.0" version = "0.26.0"
description = "The next generation HTTP client." description = "The next generation HTTP client."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"},
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"},
] ]
[package.dependencies] [package.dependencies]
@@ -2101,61 +2101,61 @@ numpy = [
[[package]] [[package]]
name = "orjson" name = "orjson"
version = "3.9.15" version = "3.9.14"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{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.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:793f6c9448ab6eb7d4974b4dde3f230345c08ca6c7995330fbceeb43a5c8aa5e"},
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"}, {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bc7928d161840096adc956703494b5c0193ede887346f028216cac0af87500"},
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"}, {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b36f54da759602d8e2f7dad958752d453dfe2c7122767bc7f765e17dc59959"},
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"}, {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abcda41ecdc950399c05eff761c3de91485d9a70d8227cb599ad3a66afe93bcc"},
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"}, {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df76ecd17b1b3627bddfd689faaf206380a1a38cc9f6c4075bd884eaedcf46c2"},
{file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"}, {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d450a8e0656efb5d0fcb062157b918ab02dcca73278975b4ee9ea49e2fcf5bd5"},
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"}, {file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95c03137b0cf66517c8baa65770507a756d3a89489d8ecf864ea92348e1beabe"},
{file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"}, {file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20837e10835c98973673406d6798e10f821e7744520633811a5a3d809762d8cc"},
{file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"}, {file = "orjson-3.9.14-cp310-none-win32.whl", hash = "sha256:1f7b6f3ef10ae8e3558abb729873d033dbb5843507c66b1c0767e32502ba96bb"},
{file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"}, {file = "orjson-3.9.14-cp310-none-win_amd64.whl", hash = "sha256:ea890e6dc1711aeec0a33b8520e395c2f3d59ead5b4351a788e06bf95fc7ba81"},
{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.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c19009ff37f033c70acd04b636380379499dac2cba27ae7dfc24f304deabbc81"},
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"}, {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19cdea0664aec0b7f385be84986d4defd3334e9c3c799407686ee1c26f7b8251"},
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"}, {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135d518f73787ce323b1a5e21fb854fe22258d7a8ae562b81a49d6c7f826f2a3"},
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"}, {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2cf1d0557c61c75e18cf7d69fb689b77896e95553e212c0cc64cf2087944b84"},
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"}, {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7c11667421df2d8b18b021223505dcc3ee51be518d54e4dc49161ac88ac2b87"},
{file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"}, {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eefc41ba42e75ed88bc396d8fe997beb20477f3e7efa000cd7a47eda452fbb2"},
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"}, {file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:917311d6a64d1c327c0dfda1e41f3966a7fb72b11ca7aa2e7a68fcccc7db35d9"},
{file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"}, {file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4dc1c132259b38d12c6587d190cd09cd76e3b5273ce71fe1372437b4cbc65f6f"},
{file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"}, {file = "orjson-3.9.14-cp311-none-win32.whl", hash = "sha256:6f39a10408478f4c05736a74da63727a1ae0e83e3533d07b19443400fe8591ca"},
{file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"}, {file = "orjson-3.9.14-cp311-none-win_amd64.whl", hash = "sha256:26280a7fcb62d8257f634c16acebc3bec626454f9ab13558bbf7883b9140760e"},
{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.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:08e722a8d06b13b67a51f247a24938d1a94b4b3862e40e0eef3b2e98c99cd04c"},
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"}, {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2591faa0c031cf3f57e5bce1461cfbd6160f3f66b5a72609a130924917cb07d"},
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"}, {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2450d87dd7b4f277f4c5598faa8b49a0c197b91186c47a2c0b88e15531e4e3e"},
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"}, {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90903d2908158a2c9077a06f11e27545de610af690fb178fd3ba6b32492d4d1c"},
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"}, {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6f095eef0026eae76fc212f20f786011ecf482fc7df2f4c272a8ae6dd7b1ef"},
{file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"}, {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751250a31fef2bac05a2da2449aae7142075ea26139271f169af60456d8ad27a"},
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"}, {file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a1af21160a38ee8be3f4fcf24ee4b99e6184cadc7f915d599f073f478a94d2c"},
{file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"}, {file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:449bf090b2aa4e019371d7511a6ea8a5a248139205c27d1834bb4b1e3c44d936"},
{file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"}, {file = "orjson-3.9.14-cp312-none-win_amd64.whl", hash = "sha256:a603161318ff699784943e71f53899983b7dee571b4dd07c336437c9c5a272b0"},
{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.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:814f288c011efdf8f115c5ebcc1ab94b11da64b207722917e0ceb42f52ef30a3"},
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"}, {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88cafb100af68af3b9b29b5ccd09fdf7a48c63327916c8c923a94c336d38dd3"},
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"}, {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba3518b999f88882ade6686f1b71e207b52e23546e180499be5bbb63a2f9c6e6"},
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"}, {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978f416bbff9da8d2091e3cf011c92da68b13f2c453dcc2e8109099b2a19d234"},
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"}, {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fc593cf836f631153d0e21beaeb8d26e144445c73645889335c2247fcd71a0"},
{file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"}, {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d1528db3c7554f9d6eeb09df23cb80dd5177ec56eeb55cc5318826928de506"},
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"}, {file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7183cc68ee2113b19b0b8714221e5e3b07b3ba10ca2bb108d78fd49cefaae101"},
{file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"}, {file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df3266d54246cb56b8bb17fa908660d2a0f2e3f63fbc32451ffc1b1505051d07"},
{file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"}, {file = "orjson-3.9.14-cp38-none-win32.whl", hash = "sha256:7913079b029e1b3501854c9a78ad938ed40d61fe09bebab3c93e60ff1301b189"},
{file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"}, {file = "orjson-3.9.14-cp38-none-win_amd64.whl", hash = "sha256:29512eb925b620e5da2fd7585814485c67cc6ba4fe739a0a700c50467a8a8065"},
{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.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5bf597530544db27a8d76aced49cfc817ee9503e0a4ebf0109cd70331e7bbe0c"},
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"}, {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac650d49366fa41fe702e054cb560171a8634e2865537e91f09a8d05ea5b1d37"},
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"}, {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:236230433a9a4968ab895140514c308fdf9f607cb8bee178a04372b771123860"},
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"}, {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3014ccbda9be0b1b5f8ea895121df7e6524496b3908f4397ff02e923bcd8f6dd"},
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"}, {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac0c7eae7ad3a223bde690565442f8a3d620056bd01196f191af8be58a5248e1"},
{file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"}, {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca33fdd0b38839b01912c57546d4f412ba7bfa0faf9bf7453432219aec2df07"},
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"}, {file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f75823cc1674a840a151e999a7dfa0d86c911150dd6f951d0736ee9d383bf415"},
{file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"}, {file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f52ac2eb49e99e7373f62e2a68428c6946cda52ce89aa8fe9f890c7278e2d3a"},
{file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"}, {file = "orjson-3.9.14-cp39-none-win32.whl", hash = "sha256:0572f174f50b673b7df78680fb52cd0087a8585a6d06d295a5f790568e1064c6"},
{file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"}, {file = "orjson-3.9.14-cp39-none-win_amd64.whl", hash = "sha256:ab90c02cb264250b8a58cedcc72ed78a4a257d956c8d3c8bebe9751b818dfad8"},
{file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"}, {file = "orjson-3.9.14.tar.gz", hash = "sha256:06fb40f8e49088ecaa02f1162581d39e2cf3fd9dbbfe411eb2284147c99bad79"},
] ]
[[package]] [[package]]
@@ -2465,13 +2465,13 @@ files = [
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.0.2" version = "8.0.0"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"},
{file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"},
] ]
[package.dependencies] [package.dependencies]
@@ -2836,28 +2836,28 @@ files = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.2.2" version = "0.2.1"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{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.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dd81b911d28925e7e8b323e8d06951554655021df8dd4ac3045d7212ac4ba080"},
{file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, {file = "ruff-0.2.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:dc586724a95b7d980aa17f671e173df00f0a2eef23f8babbeee663229a938fec"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, {file = "ruff-0.2.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c92db7101ef5bfc18e96777ed7bc7c822d545fa5977e90a585accac43d22f18a"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, {file = "ruff-0.2.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13471684694d41ae0f1e8e3a7497e14cd57ccb7dd72ae08d56a159d6c9c3e30e"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, {file = "ruff-0.2.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a11567e20ea39d1f51aebd778685582d4c56ccb082c1161ffc10f79bebe6df35"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:00a818e2db63659570403e44383ab03c529c2b9678ba4ba6c105af7854008105"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, {file = "ruff-0.2.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be60592f9d218b52f03384d1325efa9d3b41e4c4d55ea022cd548547cc42cd2b"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, {file = "ruff-0.2.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbd2288890b88e8aab4499e55148805b58ec711053588cc2f0196a44f6e3d855"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, {file = "ruff-0.2.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ef052283da7dec1987bba8d8733051c2325654641dfe5877a4022108098683"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, {file = "ruff-0.2.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7022d66366d6fded4ba3889f73cd791c2d5621b2ccf34befc752cb0df70f5fad"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, {file = "ruff-0.2.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0a725823cb2a3f08ee743a534cb6935727d9e47409e4ad72c10a3faf042ad5ba"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, {file = "ruff-0.2.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0034d5b6323e6e8fe91b2a1e55b02d92d0b582d2953a2b37a67a2d7dedbb7acc"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, {file = "ruff-0.2.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e5cb5526d69bb9143c2e4d2a115d08ffca3d8e0fddc84925a7b54931c96f5c02"},
{file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, {file = "ruff-0.2.1-py3-none-win32.whl", hash = "sha256:6b95ac9ce49b4fb390634d46d6ece32ace3acdd52814671ccaf20b7f60adb232"},
{file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, {file = "ruff-0.2.1-py3-none-win_amd64.whl", hash = "sha256:e3affdcbc2afb6f5bd0eb3130139ceedc5e3f28d206fe49f63073cb9e65988e0"},
{file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, {file = "ruff-0.2.1-py3-none-win_arm64.whl", hash = "sha256:efababa8e12330aa94a53e90a81eb6e2d55f348bc2e71adbf17d9cad23c03ee6"},
{file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, {file = "ruff-0.2.1.tar.gz", hash = "sha256:3b42b5d8677cd0c72b99fcaf068ffc62abb5a19e71b4a3b9cfa50658a0af02f1"},
] ]
[[package]] [[package]]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.97.0" version = "1.95.1"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 125, "android.injected.version.code" => 123,
"android.injected.version.name" => "1.97.0", "android.injected.version.name" => "1.95.1",
} }
) )
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
+3 -3
View File
@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000271"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000232">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="74.334294"> <testcase classname="fastlane.lanes" name="1: bundleRelease" time="78.881681">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.507669"> <testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.080999">
</testcase> </testcase>
+3 -3
View File
@@ -379,7 +379,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 140; CURRENT_PROJECT_VERSION = 139;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -515,7 +515,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 140; CURRENT_PROJECT_VERSION = 139;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 140; CURRENT_PROJECT_VERSION = 139;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
+2 -2
View File
@@ -55,11 +55,11 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.96.0</string> <string>1.95.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>140</string> <string>139</string>
<key>FLTEnableImpeller</key> <key>FLTEnableImpeller</key>
<true /> <true />
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
+1 -1
View File
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta" desc "iOS Beta"
lane :beta do lane :beta do
increment_version_number( increment_version_number(
version_number: "1.97.0" version_number: "1.95.1"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,
+6 -6
View File
@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000316"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000255">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.190055"> <testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.157832">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.109364"> <testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.825919">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.15926"> <testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.18815">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="80.90681"> <testcase classname="fastlane.lanes" name="4: build_app" time="110.912709">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="71.634559"> <testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.396901">
</testcase> </testcase>
@@ -1,4 +1,3 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui; import 'dart:ui' as ui;
@@ -133,7 +132,7 @@ class GalleryViewerPage extends HookConsumerWidget {
void toggleFavorite(Asset asset) => void toggleFavorite(Asset asset) =>
ref.read(assetProvider.notifier).toggleFavorite([asset]); ref.read(assetProvider.notifier).toggleFavorite([asset]);
Future<void> precacheNextImage(int index) async { void precacheNextImage(int index) {
void onError(Object exception, StackTrace? stackTrace) { void onError(Object exception, StackTrace? stackTrace) {
// swallow error silently // swallow error silently
debugPrint('Error precaching next image: $exception, $stackTrace'); debugPrint('Error precaching next image: $exception, $stackTrace');
@@ -141,7 +140,7 @@ class GalleryViewerPage extends HookConsumerWidget {
if (index < totalAssets && index >= 0) { if (index < totalAssets && index >= 0) {
final asset = loadAsset(index); final asset = loadAsset(index);
await precacheImage( precacheImage(
ImmichImage.imageProvider(asset: asset), ImmichImage.imageProvider(asset: asset),
context, context,
onError: onError, onError: onError,
@@ -712,21 +711,6 @@ class GalleryViewerPage extends HookConsumerWidget {
[], [],
); );
useEffect(
() {
// No need to await this
unawaited(
// Delay this a bit so we can finish loading the page
Future.delayed(const Duration(milliseconds: 400)).then(
// Precache the next image
(_) => precacheNextImage(currentIndex.value + 1),
),
);
return null;
},
[],
);
ref.listen(showControlsProvider, (_, show) { ref.listen(showControlsProvider, (_, show) {
if (show) { if (show) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
@@ -751,21 +735,14 @@ class GalleryViewerPage extends HookConsumerWidget {
isZoomed.value = state != PhotoViewScaleState.initial; isZoomed.value = state != PhotoViewScaleState.initial;
ref.read(showControlsProvider.notifier).show = !isZoomed.value; ref.read(showControlsProvider.notifier).show = !isZoomed.value;
}, },
loadingBuilder: (context, event, index) => ClipRect( loadingBuilder: (context, event, index) => ImageFiltered(
child: Stack( imageFilter: ui.ImageFilter.blur(
fit: StackFit.expand, sigmaX: 1,
children: [ sigmaY: 1,
BackdropFilter( ),
filter: ui.ImageFilter.blur( child: ImmichThumbnail(
sigmaX: 10, asset: asset(),
sigmaY: 10, fit: BoxFit.contain,
),
),
ImmichThumbnail(
asset: asset(),
fit: BoxFit.contain,
),
],
), ),
), ),
pageController: controller, pageController: controller,
@@ -777,16 +754,12 @@ class GalleryViewerPage extends HookConsumerWidget {
), ),
itemCount: totalAssets, itemCount: totalAssets,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
onPageChanged: (value) async { onPageChanged: (value) {
final next = currentIndex.value < value ? value + 1 : value - 1; final next = currentIndex.value < value ? value + 1 : value - 1;
HapticFeedback.selectionClick(); precacheNextImage(next);
currentIndex.value = value; currentIndex.value = value;
stackIndex.value = -1; stackIndex.value = -1;
HapticFeedback.selectionClick();
// Wait for page change animation to finish
await Future.delayed(const Duration(milliseconds: 400));
// Then precache the next image
unawaited(precacheNextImage(next));
}, },
builder: (context, index) { builder: (context, index) {
final a = final a =
@@ -845,7 +818,7 @@ class GalleryViewerPage extends HookConsumerWidget {
isMotionVideo: isPlayingMotionVideo.value, isMotionVideo: isPlayingMotionVideo.value,
placeholder: Image( placeholder: Image(
image: provider, image: provider,
fit: BoxFit.contain, fit: BoxFit.fitWidth,
height: context.height, height: context.height,
width: context.width, width: context.width,
alignment: Alignment.center, alignment: Alignment.center,
@@ -59,12 +59,8 @@ class VideoViewerPage extends HookWidget {
return Stack( return Stack(
children: [ children: [
if (placeholder != null) placeholder!, if (placeholder != null) placeholder!,
const Positioned.fill( const DelayedLoadingIndicator(
child: Center( fadeInDuration: Duration(milliseconds: 500),
child: DelayedLoadingIndicator(
fadeInDuration: Duration(milliseconds: 500),
),
),
), ),
], ],
); );
@@ -0,0 +1,60 @@
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/ui/hooks/blurhash_hook.dart';
class BlurhashThumb extends HookWidget {
final double height;
final double width;
final Asset asset;
final EdgeInsets margin;
const BlurhashThumb({
super.key,
required this.height,
required this.width,
required this.asset,
required this.margin,
});
@override
Widget build(BuildContext context) {
final blurhash = useBlurHashRef(asset).value;
if (blurhash == null) {
return SizedBox(
height: height,
width: width,
);
}
return Padding(
padding: margin,
child: Image.memory(
blurhash,
gaplessPlayback: true,
frameBuilder: (
BuildContext context,
Widget child,
int? frame,
bool wasSynchronouslyLoaded,
) {
if (wasSynchronouslyLoaded) {
return child;
}
return AnimatedSwitcher(
duration: const Duration(milliseconds: 100),
child: frame != null
? child
: SizedBox(
height: height,
width: width,
),
);
},
fit: BoxFit.cover,
height: height,
width: width,
),
);
}
}
@@ -1,4 +1,5 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:math'; import 'dart:math';
@@ -6,12 +7,15 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/blurhash_thumb.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'asset_grid_data_structure.dart'; import 'asset_grid_data_structure.dart';
@@ -325,32 +329,33 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
} }
/// A single row of all placeholder widgets /// A single row of all placeholder widgets
class _PlaceholderRow extends StatelessWidget { class _PlaceholderRow extends HookWidget {
final int number;
final double width; final double width;
final double height; final double height;
final double margin; final double margin;
final List<Asset> assets;
const _PlaceholderRow({ const _PlaceholderRow({
super.key, super.key,
required this.number,
required this.width, required this.width,
required this.height, required this.height,
required this.margin, required this.margin,
required this.assets,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
for (int i = 0; i < number; i++) for (int i = 0; i < assets.length; i++)
ThumbnailPlaceholder( BlurhashThumb(
key: ValueKey(i), key: ValueKey(i),
asset: assets[i],
width: width, width: width,
height: height, height: height,
margin: EdgeInsets.only( margin: EdgeInsets.only(
bottom: margin, bottom: margin,
right: i + 1 == number ? 0.0 : margin, right: i + 1 == assets.length ? 0.0 : margin,
), ),
), ),
], ],
@@ -401,9 +406,9 @@ class _Section extends StatelessWidget {
final width = constraints.maxWidth / assetsPerRow - final width = constraints.maxWidth / assetsPerRow -
margin * (assetsPerRow - 1) / assetsPerRow; margin * (assetsPerRow - 1) / assetsPerRow;
final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow; final rows = (section.count + assetsPerRow - 1) ~/ assetsPerRow;
final List<Asset> assetsToRender = scrolling final List<Asset> assetsToRender = //scrolling
? [] //? []
: renderList.loadAssets(section.offset, section.count); renderList.loadAssets(section.offset, section.count);
return Column( return Column(
key: ValueKey(section.offset), key: ValueKey(section.offset),
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -422,18 +427,22 @@ class _Section extends StatelessWidget {
selectAssets: selectAssets, selectAssets: selectAssets,
deselectAssets: deselectAssets, deselectAssets: deselectAssets,
), ),
for (int i = 0; i < rows; i++) Stack(
scrolling children: [
? _PlaceholderRow( for (int i = 0; i < rows; i++)
key: ValueKey(i), _PlaceholderRow(
number: i + 1 == rows key: ValueKey('placeholder-$i'),
? section.count - i * assetsPerRow assets: assetsToRender.nestedSlice(
: assetsPerRow, i * assetsPerRow,
width: width, min((i + 1) * assetsPerRow, section.count),
height: width, ),
margin: margin, width: width,
) height: width,
: _AssetRow( margin: margin,
),
if (!scrolling)
for (int i = 0; i < rows; i++)
_AssetRow(
key: ValueKey(i), key: ValueKey(i),
assets: assetsToRender.nestedSlice( assets: assetsToRender.nestedSlice(
i * assetsPerRow, i * assetsPerRow,
@@ -454,6 +463,8 @@ class _Section extends StatelessWidget {
onSelect: (asset) => selectAssets([asset]), onSelect: (asset) => selectAssets([asset]),
onDeselect: (asset) => deselectAssets([asset]), onDeselect: (asset) => deselectAssets([asset]),
), ),
],
),
], ],
); );
}, },
+17 -51
View File
@@ -1,12 +1,12 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; 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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.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/asset.dart';
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.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_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
class MemoryCard extends StatelessWidget { class MemoryCard extends StatelessWidget {
final Asset asset; final Asset asset;
@@ -22,6 +22,8 @@ class MemoryCard extends StatelessWidget {
super.key, super.key,
}); });
String get accessToken => Store.get(StoreKey.accessToken);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
@@ -36,8 +38,19 @@ class MemoryCard extends StatelessWidget {
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: Stack( child: Stack(
children: [ children: [
SizedBox.expand( ImageFiltered(
child: _BlurredBackdrop(asset: asset), imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: ImmichThumbnail.imageProvider(
asset: asset,
),
fit: BoxFit.cover,
),
),
child: Container(color: Colors.black.withOpacity(0.2)),
),
), ),
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
@@ -100,50 +113,3 @@ 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),
),
),
);
}
}
}
@@ -10,6 +10,7 @@ import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart';
import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart'; import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart';
import 'package:immich_mobile/shared/models/asset.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_image.dart';
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
@RoutePage() @RoutePage()
class MemoryPage extends HookConsumerWidget { class MemoryPage extends HookConsumerWidget {
@@ -109,13 +110,24 @@ class MemoryPage extends HookConsumerWidget {
asset = memories[nextMemoryIndex].assets.first; asset = memories[nextMemoryIndex].assets.first;
} }
// Precache the asset // Gets the thumbnail url and precaches it
await precacheImage( final precaches = <Future<dynamic>>[];
ImmichImage.imageProvider(
asset: asset, precaches.addAll([
precacheImage(
ImmichImage.imageProvider(
asset: asset,
),
context,
), ),
context, precacheImage(
); ImmichThumbnail.imageProvider(
asset: asset,
),
context,
),
]);
await Future.wait(precaches);
} }
// Precache the next page right away if we are on the first page // Precache the next page right away if we are on the first page
@@ -124,14 +136,11 @@ class MemoryPage extends HookConsumerWidget {
.then((_) => precacheAsset(1)); .then((_) => precacheAsset(1));
} }
Future<void> onAssetChanged(int otherIndex) async { onAssetChanged(int otherIndex) {
HapticFeedback.selectionClick(); HapticFeedback.selectionClick();
currentAssetPage.value = otherIndex; currentAssetPage.value = otherIndex;
precacheAsset(otherIndex + 1);
updateProgressText(); updateProgressText();
// Wait for page change animation to finish
await Future.delayed(const Duration(milliseconds: 400));
// And then precache the next asset
await precacheAsset(otherIndex + 1);
} }
/* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called /* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called
@@ -20,24 +20,21 @@ class DelayedLoadingIndicator extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( return AnimatedSwitcher(
future: Future.delayed(delay), duration: fadeInDuration ?? Duration.zero,
builder: (context, snapshot) { child: FutureBuilder(
late Widget c; future: Future.delayed(delay),
if (snapshot.connectionState == ConnectionState.done) { builder: (context, snapshot) {
c = child ?? if (snapshot.connectionState == ConnectionState.done) {
const ImmichLoadingIndicator( return child ??
key: ValueKey('loading'), const ImmichLoadingIndicator(
); key: ValueKey('loading'),
} else { );
c = Container(key: const ValueKey('hiding')); }
}
return AnimatedSwitcher( return Container(key: const ValueKey('hiding'));
duration: fadeInDuration ?? Duration.zero, },
child: c, ),
);
},
); );
} }
} }
-2
View File
@@ -58,11 +58,9 @@ class ImmichImage extends StatelessWidget {
} }
} }
// Whether to use the local asset image provider or a remote one
static bool useLocal(Asset asset) => static bool useLocal(Asset asset) =>
!asset.isRemote || !asset.isRemote ||
asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false); asset.isLocal && !Store.get(StoreKey.preferRemoteImage, false);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (asset == null) { if (asset == null) {
+3 -2
View File
@@ -6,7 +6,6 @@ import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_
import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_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/models/asset.dart';
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart'; import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
import 'package:immich_mobile/shared/ui/immich_image.dart';
import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart'; import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart';
import 'package:octo_image/octo_image.dart'; import 'package:octo_image/octo_image.dart';
@@ -44,7 +43,7 @@ class ImmichThumbnail extends HookWidget {
); );
} }
if (ImmichImage.useLocal(asset)) { if (useLocal(asset)) {
return ImmichLocalThumbnailProvider( return ImmichLocalThumbnailProvider(
asset: asset, asset: asset,
height: thumbnailSize, height: thumbnailSize,
@@ -58,6 +57,8 @@ class ImmichThumbnail extends HookWidget {
} }
} }
static bool useLocal(Asset asset) => !asset.isRemote || asset.isLocal;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Uint8List? blurhash = useBlurHashRef(asset).value; Uint8List? blurhash = useBlurHashRef(asset).value;
@@ -23,8 +23,7 @@ OctoPlaceholderBuilder blurHashPlaceholderBuilder(
}) { }) {
return (context) => blurhash == null return (context) => blurhash == null
? const ThumbnailPlaceholder() ? const ThumbnailPlaceholder()
: FadeInPlaceholderImage( : Image(
placeholder: const ThumbnailPlaceholder(),
image: MemoryImage(blurhash), image: MemoryImage(blurhash),
fit: fit ?? BoxFit.cover, fit: fit ?? BoxFit.cover,
); );
+1 -1
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.97.0 - API version: 1.95.1
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements ## Requirements
+2
View File
@@ -9,6 +9,8 @@ import 'package:openapi/api.dart';
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**enabled** | **bool** | | **enabled** | **bool** | |
**interval** | **int** | |
**usePolling** | **bool** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+19 -3
View File
@@ -14,25 +14,37 @@ class SystemConfigLibraryWatchDto {
/// Returns a new [SystemConfigLibraryWatchDto] instance. /// Returns a new [SystemConfigLibraryWatchDto] instance.
SystemConfigLibraryWatchDto({ SystemConfigLibraryWatchDto({
required this.enabled, required this.enabled,
required this.interval,
required this.usePolling,
}); });
bool enabled; bool enabled;
int interval;
bool usePolling;
@override @override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigLibraryWatchDto && bool operator ==(Object other) => identical(this, other) || other is SystemConfigLibraryWatchDto &&
other.enabled == enabled; other.enabled == enabled &&
other.interval == interval &&
other.usePolling == usePolling;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(enabled.hashCode); (enabled.hashCode) +
(interval.hashCode) +
(usePolling.hashCode);
@override @override
String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled]'; String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled, interval=$interval, usePolling=$usePolling]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'enabled'] = this.enabled; json[r'enabled'] = this.enabled;
json[r'interval'] = this.interval;
json[r'usePolling'] = this.usePolling;
return json; return json;
} }
@@ -45,6 +57,8 @@ class SystemConfigLibraryWatchDto {
return SystemConfigLibraryWatchDto( return SystemConfigLibraryWatchDto(
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
interval: mapValueOfType<int>(json, r'interval')!,
usePolling: mapValueOfType<bool>(json, r'usePolling')!,
); );
} }
return null; return null;
@@ -93,6 +107,8 @@ class SystemConfigLibraryWatchDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'enabled', 'enabled',
'interval',
'usePolling',
}; };
} }
@@ -21,6 +21,16 @@ void main() {
// TODO // TODO
}); });
// int interval
test('to test the property `interval`', () async {
// TODO
});
// bool usePolling
test('to test the property `usePolling`', () async {
// TODO
});
}); });
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: "none" publish_to: "none"
version: 1.97.0+125 version: 1.95.1+123
isar_version: &isar_version 3.1.0+1 isar_version: &isar_version 3.1.0+1
environment: environment:
+10 -2
View File
@@ -6458,7 +6458,7 @@
"info": { "info": {
"title": "Immich", "title": "Immich",
"description": "Immich API", "description": "Immich API",
"version": "1.97.0", "version": "1.95.1",
"contact": {} "contact": {}
}, },
"tags": [], "tags": [],
@@ -9831,10 +9831,18 @@
"properties": { "properties": {
"enabled": { "enabled": {
"type": "boolean" "type": "boolean"
},
"interval": {
"type": "integer"
},
"usePolling": {
"type": "boolean"
} }
}, },
"required": [ "required": [
"enabled" "enabled",
"interval",
"usePolling"
], ],
"type": "object" "type": "object"
}, },
+13 -1
View File
@@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.97.0 * The version of the OpenAPI document: 1.95.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -4401,6 +4401,18 @@ export interface SystemConfigLibraryWatchDto {
* @memberof SystemConfigLibraryWatchDto * @memberof SystemConfigLibraryWatchDto
*/ */
'enabled': boolean; 'enabled': boolean;
/**
*
* @type {number}
* @memberof SystemConfigLibraryWatchDto
*/
'interval': number;
/**
*
* @type {boolean}
* @memberof SystemConfigLibraryWatchDto
*/
'usePolling': boolean;
} }
/** /**
* *
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.97.0 * The version of the OpenAPI document: 1.95.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.97.0 * The version of the OpenAPI document: 1.95.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.97.0 * The version of the OpenAPI document: 1.95.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+1 -1
View File
@@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.97.0 * The version of the OpenAPI document: 1.95.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+3 -1
View File
@@ -1,6 +1,6 @@
/** /**
* Immich * Immich
* 1.97.0 * 1.95.1
* DO NOT MODIFY - This file has been generated using oazapfts. * DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts * See https://www.npmjs.com/package/oazapfts
*/ */
@@ -835,6 +835,8 @@ export type SystemConfigLibraryScanDto = {
}; };
export type SystemConfigLibraryWatchDto = { export type SystemConfigLibraryWatchDto = {
enabled: boolean; enabled: boolean;
interval: number;
usePolling: boolean;
}; };
export type SystemConfigLibraryDto = { export type SystemConfigLibraryDto = {
scan: SystemConfigLibraryScanDto; scan: SystemConfigLibraryScanDto;
+6 -6
View File
@@ -23,15 +23,15 @@
} }
}, },
"node_modules/@oazapfts/runtime": { "node_modules/@oazapfts/runtime": {
"version": "1.0.1", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.0.tgz",
"integrity": "sha512-CMl7f1gXYpjIyEtDhg4YfXwr2MXfbadbvqwKbMsaHkVtSglmuz5A8jSyefTqaJlmh0MOA2ZNS9jnbfIdtcoDiw==", "integrity": "sha512-1ovqeaeEvShbYge5/7ctJokpvqB0anBdfDNfU5jWstjV2/Gbe+vvcBM274Z0abM3IM0b9MmSNWYBXnJXYO8KCw==",
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.20", "version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
+2 -2
View File
@@ -1,5 +1,5 @@
# dev build # dev build
FROM ghcr.io/immich-app/base-server-dev:20240227@sha256:b1e212c106ce2318a587e0b2ef377215c958e877f61993ed9310534e4589cce4 as dev FROM ghcr.io/immich-app/base-server-dev:20240222@sha256:2ff467d6ae5c00a2317eb7b13cb40ba5be0fd33c160175dba621b1bf72bc1cd1 as dev
RUN apt-get install --no-install-recommends -yqq tini RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -40,7 +40,7 @@ RUN npm run build
# prod build # prod build
FROM ghcr.io/immich-app/base-server-prod:20240227@sha256:d47f5f7f2b6c53957c6353352b2fa24f2845da50e6491a7c74eb779ace10628c FROM ghcr.io/immich-app/base-server-prod:20240222@sha256:9ae5eebf95cf7759eec9dcfbd9e48a722701075ac855209f2e0b01c631b76f5c
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NODE_ENV=production \ ENV NODE_ENV=production \
+364
View File
@@ -538,6 +538,90 @@ 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>(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', () => { describe('POST /asset/upload', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server) const { status, body } = await request(server)
@@ -675,6 +759,286 @@ 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>(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', () => { describe('GET /asset/time-buckets', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH }); const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH });
+5 -1
View File
@@ -1,4 +1,4 @@
import { AssetResponseDto } from '@app/domain'; import { AssetBulkDeleteDto, AssetResponseDto } from '@app/domain';
import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto'; 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 { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
import { randomBytes } from 'node:crypto'; import { randomBytes } from 'node:crypto';
@@ -74,4 +74,8 @@ export const assetApi = {
expect(status).toBe(200); expect(status).toBe(200);
return body; 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);
},
}; };
+158
View File
@@ -0,0 +1,158 @@
import { LoginResponseDto } from '@app/domain';
import { AssetType } from '@app/infra/entities';
import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { IMMICH_TEST_ASSET_PATH, testApp } from '../../../src/test-utils/utils';
import { api } from '../../client';
const JPEG = {
type: AssetType.IMAGE,
originalFileName: 'el_torcal_rocks',
resized: true,
exifInfo: {
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
exifImageWidth: 512,
exifImageHeight: 341,
latitude: null,
longitude: null,
focalLength: 75,
iso: 200,
fNumber: 11,
exposureTime: '1/160',
fileSizeInByte: 53_493,
make: 'SONY',
model: 'DSLR-A550',
orientation: null,
description: 'SONY DSC',
},
};
const tests = [
{ input: 'formats/jpg/el_torcal_rocks.jpg', expected: JPEG },
{ input: 'formats/jpeg/el_torcal_rocks.jpeg', expected: JPEG },
{
input: 'formats/heic/IMG_2682.heic',
expected: {
type: AssetType.IMAGE,
originalFileName: 'IMG_2682',
resized: true,
fileCreatedAt: '2019-03-21T16:04:22.348Z',
exifInfo: {
dateTimeOriginal: '2019-03-21T16:04:22.348Z',
exifImageWidth: 4032,
exifImageHeight: 3024,
latitude: 41.2203,
longitude: -96.071_625,
make: 'Apple',
model: 'iPhone 7',
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
fileSizeInByte: 880_703,
exposureTime: '1/887',
iso: 20,
focalLength: 3.99,
fNumber: 1.8,
timeZone: 'America/Chicago',
},
},
},
{
input: 'formats/png/density_plot.png',
expected: {
type: AssetType.IMAGE,
originalFileName: 'density_plot',
resized: true,
exifInfo: {
exifImageWidth: 800,
exifImageHeight: 800,
latitude: null,
longitude: null,
fileSizeInByte: 25_408,
},
},
},
{
input: 'formats/raw/Nikon/D80/glarus.nef',
expected: {
type: AssetType.IMAGE,
originalFileName: 'glarus',
resized: true,
fileCreatedAt: '2010-07-20T17:27:12.000Z',
exifInfo: {
make: 'NIKON CORPORATION',
model: 'NIKON D80',
exposureTime: '1/200',
fNumber: 10,
focalLength: 18,
iso: 100,
fileSizeInByte: 9_057_784,
dateTimeOriginal: '2010-07-20T17:27:12.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
{
input: 'formats/raw/Nikon/D700/philadelphia.nef',
expected: {
type: AssetType.IMAGE,
originalFileName: 'philadelphia',
resized: true,
fileCreatedAt: '2016-09-22T22:10:29.060Z',
exifInfo: {
make: 'NIKON CORPORATION',
model: 'NIKON D700',
exposureTime: '1/400',
fNumber: 11,
focalLength: 85,
iso: 200,
fileSizeInByte: 15_856_335,
dateTimeOriginal: '2016-09-22T22:10:29.060Z',
latitude: null,
longitude: null,
orientation: '1',
timeZone: 'UTC-5',
},
},
},
];
describe(`Format (e2e)`, () => {
let server: any;
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();
});
for (const { input, expected } of tests) {
it(`should generate a thumbnail for ${input}`, async () => {
const filepath = join(IMMICH_TEST_ASSET_PATH, input);
const content = await readFile(filepath);
await api.assetApi.upload(server, admin.accessToken, 'test-device-id', {
content,
filename: basename(filepath),
});
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(assets).toHaveLength(1);
const asset = assets[0];
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
});
}
});
+102
View File
@@ -0,0 +1,102 @@
import { AssetResponseDto, LoginResponseDto } from '@app/domain';
import { AssetController } from '@app/immich';
import { exiftool } from 'exiftool-vendored';
import { readFile, writeFile } from 'fs/promises';
import {
IMMICH_TEST_ASSET_PATH,
IMMICH_TEST_ASSET_TEMP_PATH,
db,
restoreTempFolder,
testApp,
} from '../../../src/test-utils/utils';
import { api } from '../../client';
describe(`${AssetController.name} (e2e)`, () => {
let server: any;
let admin: LoginResponseDto;
beforeAll(async () => {
server = (await testApp.create()).getHttpServer();
});
beforeEach(async () => {
await testApp.reset();
await restoreTempFolder();
await api.authApi.adminSignUp(server);
admin = await api.authApi.adminLogin(server);
});
afterAll(async () => {
await testApp.teardown();
await restoreTempFolder();
});
describe('should strip metadata of', () => {
let assetWithLocation: AssetResponseDto;
beforeEach(async () => {
const fileContent = await readFile(`${IMMICH_TEST_ASSET_PATH}/metadata/gps-position/thompson-springs.jpg`);
await api.assetApi.upload(server, admin.accessToken, 'test-asset-id', { content: fileContent });
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
expect(assets).toHaveLength(1);
assetWithLocation = assets[0];
expect(assetWithLocation).toEqual(
expect.objectContaining({
exifInfo: expect.objectContaining({ latitude: 39.115, longitude: -108.400968333333 }),
}),
);
});
it('small webp thumbnails', async () => {
const assetId = assetWithLocation.id;
const thumbnail = await api.assetApi.getWebpThumbnail(server, admin.accessToken, assetId);
await writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`, thumbnail);
const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`);
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
it('large jpeg thumbnails', async () => {
const assetId = assetWithLocation.id;
const thumbnail = await api.assetApi.getJpegThumbnail(server, admin.accessToken, assetId);
await writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`, thumbnail);
const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`);
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
});
describe.each([
// These hashes were created by copying the image files to a Samsung phone,
// exporting the video from Samsung's stock Gallery app, and hashing them locally.
// This ensures that immich+exiftool are extracting the videos the same way Samsung does.
// DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
// into the test here.
['Samsung One UI 5.jpg', 'fr14niqCq6N20HB8rJYEvpsUVtI='],
['Samsung One UI 6.jpg', 'lT9Uviw/FFJYCjfIxAGPTjzAmmw='],
['Samsung One UI 6.heic', '/ejgzywvgvzvVhUYVfvkLzFBAF0='],
])('should extract motionphoto video', (file, checksum) => {
it(`with checksum ${checksum} from ${file}`, async () => {
const fileContent = await readFile(`${IMMICH_TEST_ASSET_PATH}/formats/motionphoto/${file}`);
const response = await api.assetApi.upload(server, admin.accessToken, 'test-asset-id', { content: fileContent });
const asset = await api.assetApi.get(server, admin.accessToken, response.id);
expect(asset).toHaveProperty('livePhotoVideoId');
const video = await api.assetApi.get(server, admin.accessToken, asset.livePhotoVideoId as string);
expect(video.checksum).toStrictEqual(checksum);
});
});
});
+80
View File
@@ -0,0 +1,80 @@
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);
});
});
+110 -110
View File
@@ -1,12 +1,12 @@
{ {
"name": "immich", "name": "immich",
"version": "1.97.0", "version": "1.95.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich", "name": "immich",
"version": "1.97.0", "version": "1.95.1",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.22.11", "@babel/runtime": "^7.22.11",
@@ -3179,9 +3179,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.20", "version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@@ -3281,9 +3281,9 @@
} }
}, },
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.8", "version": "7.5.7",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
"dev": true "dev": true
}, },
"node_modules/@types/send": { "node_modules/@types/send": {
@@ -3398,16 +3398,16 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
"integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.5.1", "@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "7.0.2", "@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/type-utils": "7.0.2", "@typescript-eslint/type-utils": "7.0.1",
"@typescript-eslint/utils": "7.0.2", "@typescript-eslint/utils": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.2", "@typescript-eslint/visitor-keys": "7.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.2.4", "ignore": "^5.2.4",
@@ -3433,15 +3433,15 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
"integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.0.2", "@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.2", "@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.2", "@typescript-eslint/visitor-keys": "7.0.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -3461,13 +3461,13 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
"integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.2" "@typescript-eslint/visitor-keys": "7.0.1"
}, },
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
@@ -3478,13 +3478,13 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
"integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.0.2", "@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/utils": "7.0.2", "@typescript-eslint/utils": "7.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.0.1" "ts-api-utils": "^1.0.1"
}, },
@@ -3505,9 +3505,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
"integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
@@ -3518,13 +3518,13 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
"integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.2", "@typescript-eslint/visitor-keys": "7.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -3570,17 +3570,17 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
"integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12", "@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0", "@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "7.0.2", "@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.2", "@typescript-eslint/typescript-estree": "7.0.1",
"semver": "^7.5.4" "semver": "^7.5.4"
}, },
"engines": { "engines": {
@@ -3595,12 +3595,12 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
"integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"eslint-visitor-keys": "^3.4.1" "eslint-visitor-keys": "^3.4.1"
}, },
"engines": { "engines": {
@@ -5504,9 +5504,9 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.5", "version": "16.4.4",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", "integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -8139,9 +8139,9 @@
} }
}, },
"node_modules/joi": { "node_modules/joi": {
"version": "17.12.2", "version": "17.12.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz",
"integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==",
"dependencies": { "dependencies": {
"@hapi/hoek": "^9.3.0", "@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0", "@hapi/topo": "^5.1.0",
@@ -14730,9 +14730,9 @@
} }
}, },
"@types/node": { "@types/node": {
"version": "20.11.20", "version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"requires": { "requires": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@@ -14819,9 +14819,9 @@
} }
}, },
"@types/semver": { "@types/semver": {
"version": "7.5.8", "version": "7.5.7",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
"dev": true "dev": true
}, },
"@types/send": { "@types/send": {
@@ -14936,16 +14936,16 @@
"dev": true "dev": true
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
"integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@eslint-community/regexpp": "^4.5.1", "@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "7.0.2", "@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/type-utils": "7.0.2", "@typescript-eslint/type-utils": "7.0.1",
"@typescript-eslint/utils": "7.0.2", "@typescript-eslint/utils": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.2", "@typescript-eslint/visitor-keys": "7.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.2.4", "ignore": "^5.2.4",
@@ -14955,54 +14955,54 @@
} }
}, },
"@typescript-eslint/parser": { "@typescript-eslint/parser": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
"integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/scope-manager": "7.0.2", "@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.2", "@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.2", "@typescript-eslint/visitor-keys": "7.0.1",
"debug": "^4.3.4" "debug": "^4.3.4"
} }
}, },
"@typescript-eslint/scope-manager": { "@typescript-eslint/scope-manager": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
"integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.2" "@typescript-eslint/visitor-keys": "7.0.1"
} }
}, },
"@typescript-eslint/type-utils": { "@typescript-eslint/type-utils": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
"integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/typescript-estree": "7.0.2", "@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/utils": "7.0.2", "@typescript-eslint/utils": "7.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.0.1" "ts-api-utils": "^1.0.1"
} }
}, },
"@typescript-eslint/types": { "@typescript-eslint/types": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
"integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
"dev": true "dev": true
}, },
"@typescript-eslint/typescript-estree": { "@typescript-eslint/typescript-estree": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
"integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.0.2", "@typescript-eslint/visitor-keys": "7.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -15032,27 +15032,27 @@
} }
}, },
"@typescript-eslint/utils": { "@typescript-eslint/utils": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
"integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
"dev": true, "dev": true,
"requires": { "requires": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12", "@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0", "@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "7.0.2", "@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.0.2", "@typescript-eslint/typescript-estree": "7.0.1",
"semver": "^7.5.4" "semver": "^7.5.4"
} }
}, },
"@typescript-eslint/visitor-keys": { "@typescript-eslint/visitor-keys": {
"version": "7.0.2", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
"integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/types": "7.0.2", "@typescript-eslint/types": "7.0.1",
"eslint-visitor-keys": "^3.4.1" "eslint-visitor-keys": "^3.4.1"
} }
}, },
@@ -16494,9 +16494,9 @@
} }
}, },
"dotenv": { "dotenv": {
"version": "16.4.5", "version": "16.4.4",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" "integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg=="
}, },
"dotenv-expand": { "dotenv-expand": {
"version": "10.0.0", "version": "10.0.0",
@@ -18453,9 +18453,9 @@
} }
}, },
"joi": { "joi": {
"version": "17.12.2", "version": "17.12.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz", "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz",
"integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==", "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==",
"requires": { "requires": {
"@hapi/hoek": "^9.3.0", "@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0", "@hapi/topo": "^5.1.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich", "name": "immich",
"version": "1.97.0", "version": "1.95.1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
+8 -1
View File
@@ -112,13 +112,20 @@ export class LibraryService extends EventEmitter {
ignore: library.exclusionPatterns, ignore: library.exclusionPatterns,
}); });
const config = await this.configCore.getConfig();
const { usePolling, interval } = config.library.watch;
this.logger.debug(`Settings for watcher: usePolling: ${usePolling}, interval: ${interval}`);
let _resolve: () => void; let _resolve: () => void;
const ready$ = new Promise<void>((resolve) => (_resolve = resolve)); const ready$ = new Promise<void>((resolve) => (_resolve = resolve));
this.watchers[id] = this.storageRepository.watch( this.watchers[id] = this.storageRepository.watch(
library.importPaths, library.importPaths,
{ {
usePolling: false, usePolling,
interval,
binaryInterval: interval,
ignoreInitial: true, ignoreInitial: true,
}, },
{ {
+18 -9
View File
@@ -1799,9 +1799,9 @@ describe(MediaService.name, () => {
'/original/path.ext', '/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4', 'upload/encoded-video/user-id/as/se/asset-id.mp4',
{ {
inputOptions: ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'], inputOptions: [],
outputOptions: [ outputOptions: [
`-c:v hevc_rkmpp`, `-c:v hevc_rkmpp_encoder`,
'-c:a copy', '-c:a copy',
'-movflags faststart', '-movflags faststart',
'-fps_mode passthrough', '-fps_mode passthrough',
@@ -1810,12 +1810,17 @@ describe(MediaService.name, () => {
'-g 256', '-g 256',
'-tag:v hvc1', '-tag:v hvc1',
'-v verbose', '-v verbose',
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
'-level 153', '-level 153',
'-rc_mode AVBR', '-rc_mode 3',
'-quality_min 0',
'-quality_max 100',
'-b:v 10000k', '-b:v 10000k',
'-width 1280',
'-height 720',
], ],
twoPass: false, twoPass: false,
ffmpegPath: 'ffmpeg_mpp',
ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp',
}, },
); );
}); });
@@ -1834,9 +1839,9 @@ describe(MediaService.name, () => {
'/original/path.ext', '/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4', 'upload/encoded-video/user-id/as/se/asset-id.mp4',
{ {
inputOptions: ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'], inputOptions: [],
outputOptions: [ outputOptions: [
`-c:v h264_rkmpp`, `-c:v h264_rkmpp_encoder`,
'-c:a copy', '-c:a copy',
'-movflags faststart', '-movflags faststart',
'-fps_mode passthrough', '-fps_mode passthrough',
@@ -1844,12 +1849,16 @@ describe(MediaService.name, () => {
'-map 0:1', '-map 0:1',
'-g 256', '-g 256',
'-v verbose', '-v verbose',
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
'-level 51', '-level 51',
'-rc_mode CQP', '-rc_mode 2',
'-qp_init 30', '-quality_min 51',
'-quality_max 51',
'-width 1280',
'-height 720',
], ],
twoPass: false, twoPass: false,
ffmpegPath: 'ffmpeg_mpp',
ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp',
}, },
); );
}); });
+26 -19
View File
@@ -14,7 +14,7 @@ class BaseConfig implements VideoCodecSWConfig {
getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
const options = { const options = {
inputOptions: this.getBaseInputOptions(videoStream), inputOptions: this.getBaseInputOptions(),
outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'], outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'],
twoPass: this.eligibleForTwoPass(), twoPass: this.eligibleForTwoPass(),
} as TranscodeOptions; } as TranscodeOptions;
@@ -30,8 +30,7 @@ class BaseConfig implements VideoCodecSWConfig {
return options; return options;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars getBaseInputOptions(): string[] {
getBaseInputOptions(videoStream: VideoStreamInfo): string[] {
return []; return [];
} }
@@ -608,28 +607,35 @@ export class VAAPIConfig extends BaseHWConfig {
} }
export class RKMPPConfig 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 { eligibleForTwoPass(): boolean {
return false; return false;
} }
getBaseInputOptions(videoStream: VideoStreamInfo) { getBaseInputOptions() {
if (this.devices.length === 0) { if (this.devices.length === 0) {
throw new Error('No RKMPP device found'); throw new Error('No RKMPP device found');
} }
if (this.shouldToneMap(videoStream)) { return [];
// disable hardware decoding
return [];
}
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'];
} }
getFilterOptions(videoStream: VideoStreamInfo) { getFilterOptions(videoStream: VideoStreamInfo) {
if (this.shouldToneMap(videoStream)) { return this.shouldToneMap(videoStream) ? this.getToneMapping() : [];
// use software filter options }
return super.getFilterOptions(videoStream);
} getSizeOptions(videoStream: VideoStreamInfo) {
if (this.shouldScale(videoStream)) { if (this.shouldScale(videoStream)) {
return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`]; const { width, height } = this.getSize(videoStream);
return [`-width ${width}`, `-height ${height}`];
} }
return []; return [];
} }
@@ -653,11 +659,12 @@ export class RKMPPConfig extends BaseHWConfig {
getBitrateOptions() { getBitrateOptions() {
const bitrate = this.getMaxBitrateValue(); const bitrate = this.getMaxBitrateValue();
if (bitrate > 0) { if (bitrate > 0) {
// -b:v specifies max bitrate, average bitrate is derived automatically... return ['-rc_mode 3', '-quality_min 0', '-quality_max 100', `-b:v ${bitrate}${this.getBitrateUnit()}`];
return ['-rc_mode AVBR', `-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}`];
} }
// use CRF value as QP value
return ['-rc_mode CQP', `-qp_init ${this.config.crf}`];
} }
getSupportedCodecs() { getSupportedCodecs() {
@@ -665,6 +672,6 @@ export class RKMPPConfig extends BaseHWConfig {
} }
getVideoCodec(): string { getVideoCodec(): string {
return `${this.config.targetVideoCodec}_rkmpp`; return `${this.config.targetVideoCodec}_rkmpp_encoder`;
} }
} }
@@ -51,6 +51,8 @@ export interface TranscodeOptions {
inputOptions: string[]; inputOptions: string[];
outputOptions: string[]; outputOptions: string[];
twoPass: boolean; twoPass: boolean;
ffmpegPath?: string;
ldLibraryPath?: string;
} }
export interface BitrateDistribution { export interface BitrateDistribution {
@@ -187,5 +187,4 @@ export interface ISearchRepository {
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>; searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>; upsert(smartInfo: Partial<SmartInfoEntity>, embedding?: Embedding): Promise<void>;
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>; searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
deleteAllSearchEmbeddings(): Promise<void>;
} }
@@ -71,7 +71,6 @@ describe(SmartInfoService.name, () => {
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]); 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(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH);
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
}); });
it('should queue all the assets', async () => { it('should queue all the assets', async () => {
@@ -84,7 +83,6 @@ describe(SmartInfoService.name, () => {
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]); expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
expect(assetMock.getAll).toHaveBeenCalled(); expect(assetMock.getAll).toHaveBeenCalled();
expect(searchMock.deleteAllSearchEmbeddings).toHaveBeenCalled();
}); });
}); });
@@ -50,10 +50,6 @@ export class SmartInfoService {
return true; return true;
} }
if (force) {
await this.repository.deleteAllSearchEmbeddings();
}
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force return force
? this.assetRepository.getAll(pagination) ? this.assetRepository.getAll(pagination)
@@ -1,9 +1,12 @@
import { validateCronExpression } from '@app/domain'; import { validateCronExpression } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
IsInt,
IsNotEmpty, IsNotEmpty,
IsObject, IsObject,
IsPositive,
IsString, IsString,
Validate, Validate,
ValidateIf, ValidateIf,
@@ -35,6 +38,14 @@ export class SystemConfigLibraryScanDto {
export class SystemConfigLibraryWatchDto { export class SystemConfigLibraryWatchDto {
@IsBoolean() @IsBoolean()
enabled!: boolean; enabled!: boolean;
@IsBoolean()
usePolling!: boolean;
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
interval!: number;
} }
export class SystemConfigLibraryDto { export class SystemConfigLibraryDto {
@@ -132,6 +132,8 @@ export const defaults = Object.freeze<SystemConfig>({
}, },
watch: { watch: {
enabled: false, enabled: false,
usePolling: false,
interval: 10_000,
}, },
}, },
server: { server: {
@@ -136,6 +136,8 @@ const updatedConfig = Object.freeze<SystemConfig>({
}, },
watch: { watch: {
enabled: false, enabled: false,
usePolling: false,
interval: 10_000,
}, },
}, },
}); });
@@ -51,6 +51,8 @@ export enum SystemConfigKey {
LIBRARY_SCAN_CRON_EXPRESSION = 'library.scan.cronExpression', LIBRARY_SCAN_CRON_EXPRESSION = 'library.scan.cronExpression',
LIBRARY_WATCH_ENABLED = 'library.watch.enabled', LIBRARY_WATCH_ENABLED = 'library.watch.enabled',
LIBRARY_WATCH_USE_POLLING = 'library.watch.usePolling',
LIBRARY_WATCH_INTERVAL = 'library.watch.interval',
LOGGING_ENABLED = 'logging.enabled', LOGGING_ENABLED = 'logging.enabled',
LOGGING_LEVEL = 'logging.level', LOGGING_LEVEL = 'logging.level',
@@ -266,6 +268,8 @@ export interface SystemConfig {
}; };
watch: { watch: {
enabled: boolean; enabled: boolean;
usePolling: boolean;
interval: number;
}; };
}; };
server: { server: {
@@ -1,12 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveLibraryWatchPollingOption1709150004123 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'library.watch.usePolling'`);
await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'library.watch.interval'`);
}
public async down(): Promise<void> {
// noop
}
}
@@ -76,7 +76,18 @@ export class MediaRepository implements IMediaRepository {
transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise<void> { transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise<void> {
if (!options.twoPass) { if (!options.twoPass) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run(); 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;
}
}
}); });
} }
@@ -110,6 +121,7 @@ export class MediaRepository implements IMediaRepository {
configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) { configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) {
return ffmpeg(input, { niceness: 10 }) return ffmpeg(input, { niceness: 10 })
.setFfmpegPath(options.ffmpegPath || 'ffmpeg')
.inputOptions(options.inputOptions) .inputOptions(options.inputOptions)
.outputOptions(options.outputOptions) .outputOptions(options.outputOptions)
.output(output) .output(output)
@@ -40,11 +40,11 @@ export class PersonRepository implements IPersonRepository {
} }
async deleteAll(): Promise<void> { async deleteAll(): Promise<void> {
await this.personRepository.clear(); await this.personRepository.delete({});
} }
async deleteAllFaces(): Promise<void> { async deleteAllFaces(): Promise<void> {
await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE'); await this.assetFaceRepository.delete({});
} }
getAllFaces( getAllFaces(
@@ -229,17 +229,25 @@ export class SearchRepository implements ISearchRepository {
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`); this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
await this.smartSearchRepository.manager.transaction(async (manager) => { await this.smartSearchRepository.manager.transaction(async (manager) => {
await manager.clear(SmartSearchEntity); await manager.query(`DROP TABLE smart_search`);
await manager.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`);
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
$$)`);
}); });
this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`); this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`);
} }
deleteAllSearchEmbeddings(): Promise<void> {
return this.smartSearchRepository.clear();
}
private async getDimSize(): Promise<number> { private async getDimSize(): Promise<number> {
const res = await this.smartSearchRepository.manager.query(` const res = await this.smartSearchRepository.manager.query(`
SELECT atttypmod as dimsize SELECT atttypmod as dimsize
@@ -8,6 +8,5 @@ export const newSearchRepositoryMock = (): jest.Mocked<ISearchRepository> => {
searchFaces: jest.fn(), searchFaces: jest.fn(),
upsert: jest.fn(), upsert: jest.fn(),
searchPlaces: jest.fn(), searchPlaces: jest.fn(),
deleteAllSearchEmbeddings: jest.fn(),
}; };
}; };
+7 -13
View File
@@ -13,7 +13,6 @@ module.exports = {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2022, ecmaVersion: 2022,
extraFileExtensions: ['.svelte'], extraFileExtensions: ['.svelte'],
project: ['./tsconfig.json'],
}, },
env: { env: {
browser: true, browser: true,
@@ -33,6 +32,13 @@ module.exports = {
NodeJS: true, NodeJS: true,
}, },
rules: { 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': [ '@typescript-eslint/no-unused-vars': [
'warn', 'warn',
{ {
@@ -42,17 +48,5 @@ module.exports = {
}, },
], ],
curly: 2, 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',
}, },
}; };
+171 -175
View File
@@ -1,67 +1,67 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.3.0", "version": "1.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-web", "name": "immich-web",
"version": "1.3.0", "version": "1.1.1",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.3.67",
"@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/core": "^5.7.0",
"@zoom-image/svelte": "^0.2.6", "@zoom-image/svelte": "^0.2.0",
"axios": "^1.6.7", "axios": "^1.6.7",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2", "copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.7",
"justified-layout": "^4.1.0", "justified-layout": "^4.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"luxon": "^3.4.4", "luxon": "^3.2.1",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.6.1",
"svelte-local-storage-store": "^0.6.4", "svelte-local-storage-store": "^0.6.0",
"svelte-maplibre": "^0.8.1", "svelte-maplibre": "^0.8.0",
"thumbhash": "^0.1.1" "thumbhash": "^0.1.1"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.6.3", "@floating-ui/dom": "^1.5.1",
"@socket.io/component-emitter": "^3.1.0", "@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.1.8", "@sveltejs/enhanced-img": "^0.1.8",
"@sveltejs/kit": "^2.5.2", "@sveltejs/kit": "^2.5.1",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.1.5",
"@testing-library/svelte": "^4.1.0", "@testing-library/svelte": "^4.0.3",
"@types/dom-to-image": "^2.6.7", "@types/dom-to-image": "^2.6.4",
"@types/justified-layout": "^4.1.4", "@types/justified-layout": "^4.1.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.6",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.3.1", "@vitest/coverage-v8": "^1.0.4",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.13",
"eslint": "^8.57.0", "eslint": "^8.34.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.30.0",
"eslint-plugin-unicorn": "^51.0.1", "eslint-plugin-unicorn": "^51.0.0",
"factory.ts": "^1.4.1", "factory.ts": "^1.3.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"postcss": "^8.4.35", "postcss": "^8.4.21",
"prettier": "^3.2.5", "prettier": "^3.1.0",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-svelte": "^3.2.1", "prettier-plugin-svelte": "^3.1.2",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"svelte": "^4.2.12", "svelte": "^4.2.11",
"svelte-check": "^3.6.5", "svelte-check": "^3.6.4",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.2.7",
"tslib": "^2.6.2", "tslib": "^2.5.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.1.4", "vite": "^5.1.1",
"vitest": "^1.3.1" "vitest": "^1.0.4"
} }
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
@@ -898,9 +898,9 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "8.57.0", "version": "8.56.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
"integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -948,13 +948,13 @@
"dev": true "dev": true
}, },
"node_modules/@humanwhocodes/config-array": { "node_modules/@humanwhocodes/config-array": {
"version": "0.11.14", "version": "0.11.13",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@humanwhocodes/object-schema": "^2.0.2", "@humanwhocodes/object-schema": "^2.0.1",
"debug": "^4.3.1", "debug": "^4.1.1",
"minimatch": "^3.0.5" "minimatch": "^3.0.5"
}, },
"engines": { "engines": {
@@ -975,9 +975,9 @@
} }
}, },
"node_modules/@humanwhocodes/object-schema": { "node_modules/@humanwhocodes/object-schema": {
"version": "2.0.2", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
"dev": true "dev": true
}, },
"node_modules/@img/sharp-darwin-arm64": { "node_modules/@img/sharp-darwin-arm64": {
@@ -1859,9 +1859,9 @@
} }
}, },
"node_modules/@sveltejs/kit": { "node_modules/@sveltejs/kit": {
"version": "2.5.2", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.2.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.1.tgz",
"integrity": "sha512-1Pm2lsBYURQsjnLyZa+jw75eVD4gYHxGRwPyFe4DAmB3FjTVR8vRNWGeuDLGFcKMh/B1ij6FTUrc9GrerogCng==", "integrity": "sha512-TKj08o3mJCoQNLTdRdGkHPePTCPUGTgkew65RDqjVU3MtPVxljsofXQYfXndHfq0P7KoPRO/0/reF6HesU0Djw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
@@ -2284,9 +2284,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "7.5.8", "version": "7.5.7",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
"dev": true "dev": true
}, },
"node_modules/@types/supercluster": { "node_modules/@types/supercluster": {
@@ -2298,16 +2298,16 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.1.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
"integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==", "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.5.1", "@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "7.1.0", "@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/type-utils": "7.1.0", "@typescript-eslint/type-utils": "7.0.1",
"@typescript-eslint/utils": "7.1.0", "@typescript-eslint/utils": "7.0.1",
"@typescript-eslint/visitor-keys": "7.1.0", "@typescript-eslint/visitor-keys": "7.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.2.4", "ignore": "^5.2.4",
@@ -2366,15 +2366,15 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.1.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
"integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==", "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.1.0", "@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.1.0", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.1.0", "@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/visitor-keys": "7.1.0", "@typescript-eslint/visitor-keys": "7.0.1",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -2394,13 +2394,13 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.1.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
"integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==", "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.1.0", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.1.0" "@typescript-eslint/visitor-keys": "7.0.1"
}, },
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
@@ -2411,13 +2411,13 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.1.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
"integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==", "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.1.0", "@typescript-eslint/typescript-estree": "7.0.1",
"@typescript-eslint/utils": "7.1.0", "@typescript-eslint/utils": "7.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.0.1" "ts-api-utils": "^1.0.1"
}, },
@@ -2438,9 +2438,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.1.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
"integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==", "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^16.0.0 || >=18.0.0" "node": "^16.0.0 || >=18.0.0"
@@ -2451,13 +2451,13 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.1.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
"integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==", "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.1.0", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/visitor-keys": "7.1.0", "@typescript-eslint/visitor-keys": "7.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -2536,17 +2536,17 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.1.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
"integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==", "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12", "@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0", "@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "7.1.0", "@typescript-eslint/scope-manager": "7.0.1",
"@typescript-eslint/types": "7.1.0", "@typescript-eslint/types": "7.0.1",
"@typescript-eslint/typescript-estree": "7.1.0", "@typescript-eslint/typescript-estree": "7.0.1",
"semver": "^7.5.4" "semver": "^7.5.4"
}, },
"engines": { "engines": {
@@ -2594,12 +2594,12 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.1.0", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
"integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==", "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.1.0", "@typescript-eslint/types": "7.0.1",
"eslint-visitor-keys": "^3.4.1" "eslint-visitor-keys": "^3.4.1"
}, },
"engines": { "engines": {
@@ -2617,23 +2617,24 @@
"dev": true "dev": true
}, },
"node_modules/@vitest/browser": { "node_modules/@vitest/browser": {
"version": "1.3.1", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.0.4.tgz",
"integrity": "sha512-pRof8G8nqRWwg3ouyIctyhfIVk5jXgF056uF//sqdi37+pVtDz9kBI/RMu0xlc8tgCyJ2aEMfbgJZPUydlEVaQ==", "integrity": "sha512-qMT1NhClex73eA2sOwnlwLcSIVCW8B7NFVzIKuXLKxSJD3LsNq8PCKhwOkBxklbSAcZdkOgL/d3/gzQT7k9eng==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"@vitest/utils": "1.3.1", "estree-walker": "^3.0.3",
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
"sirv": "^2.0.4" "sirv": "^2.0.3"
}, },
"funding": { "funding": {
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
}, },
"peerDependencies": { "peerDependencies": {
"playwright": "*", "playwright": "*",
"vitest": "1.3.1", "safaridriver": "*",
"vitest": "^1.0.0",
"webdriverio": "*" "webdriverio": "*"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
@@ -2649,9 +2650,9 @@
} }
}, },
"node_modules/@vitest/coverage-v8": { "node_modules/@vitest/coverage-v8": {
"version": "1.3.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.2.tgz",
"integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", "integrity": "sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.2.1",
@@ -2672,17 +2673,17 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
}, },
"peerDependencies": { "peerDependencies": {
"vitest": "1.3.1" "vitest": "^1.0.0"
} }
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "1.3.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
"integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/spy": "1.3.1", "@vitest/spy": "1.2.2",
"@vitest/utils": "1.3.1", "@vitest/utils": "1.2.2",
"chai": "^4.3.10" "chai": "^4.3.10"
}, },
"funding": { "funding": {
@@ -2690,12 +2691,12 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "1.3.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
"integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/utils": "1.3.1", "@vitest/utils": "1.2.2",
"p-limit": "^5.0.0", "p-limit": "^5.0.0",
"pathe": "^1.1.1" "pathe": "^1.1.1"
}, },
@@ -2731,9 +2732,9 @@
} }
}, },
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "1.3.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
"integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
@@ -2777,9 +2778,9 @@
"dev": true "dev": true
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "1.3.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
"integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"tinyspy": "^2.2.0" "tinyspy": "^2.2.0"
@@ -2789,9 +2790,9 @@
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "1.3.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
"integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"diff-sequences": "^29.6.3", "diff-sequences": "^29.6.3",
@@ -2836,9 +2837,9 @@
"dev": true "dev": true
}, },
"node_modules/@zoom-image/core": { "node_modules/@zoom-image/core": {
"version": "0.33.0", "version": "0.32.1",
"resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.33.0.tgz", "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.32.1.tgz",
"integrity": "sha512-wkMV8+aE7PeknLFhpIb/6vwRl09Z2gWM4UqKdnXO6Mb0pP9BiuDLcLvGGGB4o++uAPINgDwmNn+Loo641XSjDA==", "integrity": "sha512-R56D749Ck+/1yLWlEJ2FctxjdpTQEje3jPhOAbeEZGzLndIumskO42UqRNixcER6sAzCi01oYopmqnCpDElF0g==",
"dependencies": { "dependencies": {
"@namnode/store": "^0.1.0" "@namnode/store": "^0.1.0"
}, },
@@ -2848,11 +2849,11 @@
} }
}, },
"node_modules/@zoom-image/svelte": { "node_modules/@zoom-image/svelte": {
"version": "0.2.6", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.6.tgz", "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.4.tgz",
"integrity": "sha512-dEpA/egmTjVcptwhtcKHvkhVMTzQCpH17erfcXuJByt+nn5Oo4LnZOxE8gwSVEdPp65Ns6Y/byYD0GSQ/vv+DQ==", "integrity": "sha512-rgfgn7Q60VrwmE4MPBzDWaFplc+411Lxg1nMdAnq/UTv4HTWSpiwm1IOg8gQZjRp92a8RXcRmUYXU+wFKEMjSg==",
"dependencies": { "dependencies": {
"@zoom-image/core": "0.33.0" "@zoom-image/core": "0.32.1"
}, },
"funding": { "funding": {
"type": "github", "type": "github",
@@ -4054,16 +4055,16 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "8.57.0", "version": "8.56.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4", "@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.57.0", "@eslint/js": "8.56.0",
"@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8", "@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0", "@ungap/structured-clone": "^1.2.0",
@@ -7990,23 +7991,17 @@
} }
}, },
"node_modules/strip-literal": { "node_modules/strip-literal": {
"version": "2.0.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
"integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"js-tokens": "^8.0.2" "acorn": "^8.10.0"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "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": { "node_modules/sucrase": {
"version": "3.34.0", "version": "3.34.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
@@ -8082,9 +8077,9 @@
} }
}, },
"node_modules/svelte": { "node_modules/svelte": {
"version": "4.2.12", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.11.tgz",
"integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==", "integrity": "sha512-YIQk3J4X89wOLhjsqIW8tqY3JHPuBdtdOIkASP2PZeAMcSW9RsIjQzMesCrxOF3gdWYC0mKknlKF7OqmLM+Zqg==",
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/sourcemap-codec": "^1.4.15",
@@ -8106,9 +8101,9 @@
} }
}, },
"node_modules/svelte-check": { "node_modules/svelte-check": {
"version": "3.6.5", "version": "3.6.4",
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.5.tgz", "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz",
"integrity": "sha512-5aLgoQEdadvp8ypvKQ2avhnQ+V9YPQQaWrTFlXFw5g/v8xIQBvo+X/WqxTyD+V/ItDqXg3+abUA53rdDHgUjCA==", "integrity": "sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/trace-mapping": "^0.3.17", "@jridgewell/trace-mapping": "^0.3.17",
@@ -8178,9 +8173,9 @@
} }
}, },
"node_modules/svelte-maplibre": { "node_modules/svelte-maplibre": {
"version": "0.8.1", "version": "0.8.0",
"resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.1.tgz", "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.0.tgz",
"integrity": "sha512-CTm/s0+mJzBHSoO5zPKBo3ORmUyiWS3Ex4xvVdNgVg+sDesHasEAJ0N1/NUrd56S33zgRdFZGzRnRguCnKFAzw==", "integrity": "sha512-sRSj/zQa7LTfHNIzKcYe+sa9qHClt/OAXcdPQ0w3ksLbCMmVHGk4B2yIXHCVk0g4sc18M85N8KGsHVtZoNC+Mw==",
"dependencies": { "dependencies": {
"d3-geo": "^3.1.0", "d3-geo": "^3.1.0",
"just-compare": "^2.3.0", "just-compare": "^2.3.0",
@@ -8461,9 +8456,9 @@
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
}, },
"node_modules/tinyspy": { "node_modules/tinyspy": {
"version": "2.2.1", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
"integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
@@ -8738,9 +8733,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.1.4", "version": "5.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
"integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==", "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.19.3", "esbuild": "^0.19.3",
@@ -8806,9 +8801,9 @@
} }
}, },
"node_modules/vite-node": { "node_modules/vite-node": {
"version": "1.3.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
"integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"cac": "^6.7.14", "cac": "^6.7.14",
@@ -8842,17 +8837,18 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "1.3.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz",
"integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/expect": "1.3.1", "@vitest/expect": "1.2.2",
"@vitest/runner": "1.3.1", "@vitest/runner": "1.2.2",
"@vitest/snapshot": "1.3.1", "@vitest/snapshot": "1.2.2",
"@vitest/spy": "1.3.1", "@vitest/spy": "1.2.2",
"@vitest/utils": "1.3.1", "@vitest/utils": "1.2.2",
"acorn-walk": "^8.3.2", "acorn-walk": "^8.3.2",
"cac": "^6.7.14",
"chai": "^4.3.10", "chai": "^4.3.10",
"debug": "^4.3.4", "debug": "^4.3.4",
"execa": "^8.0.1", "execa": "^8.0.1",
@@ -8861,11 +8857,11 @@
"pathe": "^1.1.1", "pathe": "^1.1.1",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"std-env": "^3.5.0", "std-env": "^3.5.0",
"strip-literal": "^2.0.0", "strip-literal": "^1.3.0",
"tinybench": "^2.5.1", "tinybench": "^2.5.1",
"tinypool": "^0.8.2", "tinypool": "^0.8.2",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-node": "1.3.1", "vite-node": "1.2.2",
"why-is-node-running": "^2.2.2" "why-is-node-running": "^2.2.2"
}, },
"bin": { "bin": {
@@ -8880,8 +8876,8 @@
"peerDependencies": { "peerDependencies": {
"@edge-runtime/vm": "*", "@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0", "@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "1.3.1", "@vitest/browser": "^1.0.0",
"@vitest/ui": "1.3.1", "@vitest/ui": "^1.0.0",
"happy-dom": "*", "happy-dom": "*",
"jsdom": "*" "jsdom": "*"
}, },
+35 -35
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-web", "name": "immich-web",
"version": "1.3.0", "version": "1.1.1",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"scripts": { "scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000", "dev": "vite dev --host 0.0.0.0 --port 3000",
@@ -22,59 +22,59 @@
"prepare": "svelte-kit sync" "prepare": "svelte-kit sync"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.0.0",
"@floating-ui/dom": "^1.6.3", "@floating-ui/dom": "^1.5.1",
"@socket.io/component-emitter": "^3.1.0", "@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.1.8", "@sveltejs/enhanced-img": "^0.1.8",
"@sveltejs/kit": "^2.5.2", "@sveltejs/kit": "^2.5.1",
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@sveltejs/vite-plugin-svelte": "^3.0.2",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.1.5",
"@testing-library/svelte": "^4.1.0", "@testing-library/svelte": "^4.0.3",
"@types/dom-to-image": "^2.6.7", "@types/dom-to-image": "^2.6.4",
"@types/justified-layout": "^4.1.4", "@types/justified-layout": "^4.1.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.6",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.3.1", "@vitest/coverage-v8": "^1.0.4",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.13",
"eslint": "^8.57.0", "eslint": "^8.34.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.30.0",
"eslint-plugin-unicorn": "^51.0.1", "eslint-plugin-unicorn": "^51.0.0",
"factory.ts": "^1.4.1", "factory.ts": "^1.3.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"postcss": "^8.4.35", "postcss": "^8.4.21",
"prettier": "^3.2.5", "prettier": "^3.1.0",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-svelte": "^3.2.1", "prettier-plugin-svelte": "^3.1.2",
"rollup-plugin-visualizer": "^5.12.0", "rollup-plugin-visualizer": "^5.12.0",
"svelte": "^4.2.12", "svelte": "^4.2.11",
"svelte-check": "^3.6.5", "svelte-check": "^3.6.4",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.2.7",
"tslib": "^2.6.2", "tslib": "^2.5.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.1.4", "vite": "^5.1.1",
"vitest": "^1.3.1" "vitest": "^1.0.4"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.3.67",
"@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/core": "^5.7.0",
"@zoom-image/svelte": "^0.2.6", "@zoom-image/svelte": "^0.2.0",
"axios": "^1.6.7", "axios": "^1.6.7",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2", "copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.7",
"justified-layout": "^4.1.0", "justified-layout": "^4.1.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"luxon": "^3.4.4", "luxon": "^3.2.1",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.6.1",
"svelte-local-storage-store": "^0.6.4", "svelte-local-storage-store": "^0.6.0",
"svelte-maplibre": "^0.8.1", "svelte-maplibre": "^0.8.0",
"thumbhash": "^0.1.1" "thumbhash": "^0.1.1"
} }
} }
+2 -1
View File
@@ -1,6 +1,7 @@
import { isHttpError } from '@immich/sdk'; import { isHttpError } from '@immich/sdk';
import type { HandleClientError } from '@sveltejs/kit'; import type { HandleClientError } from '@sveltejs/kit';
const LOG_PREFIX = '[hooks.client.ts]';
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?'; const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
const parseError = (error: unknown) => { const parseError = (error: unknown) => {
@@ -22,6 +23,6 @@ const parseError = (error: unknown) => {
export const handleError: HandleClientError = ({ error }) => { export const handleError: HandleClientError = ({ error }) => {
const result = parseError(error); const result = parseError(error);
console.error(`[hooks.client.ts]:handleError ${result.message}`); console.error(`${LOG_PREFIX}:handleError ${result.message}`);
return result; return result;
}; };
@@ -48,11 +48,11 @@
await handleCommand(jobId, dto); await handleCommand(jobId, dto);
}; };
const onConfirm = async () => { const onConfirm = () => {
if (!confirmJob) { if (!confirmJob) {
return; return;
} }
await handleCommand(confirmJob, { command: JobCommand.Start, force: true }); handleCommand(confirmJob, { command: JobCommand.Start, force: true });
confirmJob = null; confirmJob = null;
}; };
@@ -54,7 +54,7 @@
}); });
}; };
const resetToDefault = (configKeys: Array<keyof SystemConfigDto>) => { const resetToDefault = async (configKeys: Array<keyof SystemConfigDto>) => {
for (const key of configKeys) { for (const key of configKeys) {
config = { ...config, [key]: defaultConfig[key] }; config = { ...config, [key]: defaultConfig[key] };
} }
@@ -42,6 +42,28 @@
subtitle="Watch external libraries for file changes" subtitle="Watch external libraries for file changes"
bind:checked={config.library.watch.enabled} bind:checked={config.library.watch.enabled}
/> />
<SettingSwitch
title="Use filesystem polling (EXPERIMENTAL)"
disabled={disabled || !config.library.watch.enabled}
subtitle="Use polling instead of native filesystem watching. This is required for network shares but can be very resource intensive. Use with care!"
bind:checked={config.library.watch.usePolling}
/>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
required={config.library.watch.usePolling}
disabled={disabled || !config.library.watch.usePolling || !config.library.watch.enabled}
label="Polling interval"
bind:value={config.library.watch.interval}
isEdited={config.library.watch.interval !== savedConfig.library.watch.interval}
>
<svelte:fragment slot="desc">
<p class="text-sm dark:text-immich-dark-fg">
Interval of filesystem polling, in milliseconds. Lower values will result in higher CPU usage.
</p>
</svelte:fragment>
</SettingInputField>
</div> </div>
<div class="ml-4"> <div class="ml-4">
@@ -1,45 +0,0 @@
<script lang="ts">
import { autoGrowHeight } from '$lib/utils/autogrow';
import { updateAlbumInfo } from '@immich/sdk';
import { handleError } from '$lib/utils/handle-error';
export let id: string;
export let description: string;
export let isOwned: boolean;
$: newDescription = description;
const handleUpdateDescription = async () => {
if (newDescription === description) {
return;
}
try {
await updateAlbumInfo({
id,
updateAlbumDto: {
description: newDescription,
},
});
} catch (error) {
handleError(error, 'Error updating album description');
return;
}
description = newDescription;
};
</script>
{#if isOwned}
<textarea
class="w-full mt-2 resize-none overflow-hidden text-black dark:text-white border-b-2 border-transparent border-gray-500 bg-transparent text-base outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:focus:border-immich-dark-primary hover:border-gray-400"
bind:value={newDescription}
on:input={(e) => autoGrowHeight(e.currentTarget)}
on:focusout={handleUpdateDescription}
use:autoGrowHeight
placeholder="Add description"
/>
{:else if description}
<p class="break-words whitespace-pre-line w-full text-black dark:text-white text-base">
{description}
</p>
{/if}
@@ -1,42 +0,0 @@
<script lang="ts">
import { updateAlbumInfo } from '@immich/sdk';
import { handleError } from '$lib/utils/handle-error';
export let id: string;
export let albumName: string;
export let isOwned: boolean;
$: newAlbumName = albumName;
const handleUpdateName = async () => {
if (newAlbumName === albumName) {
return;
}
try {
await updateAlbumInfo({
id,
updateAlbumDto: {
albumName: newAlbumName,
},
});
} catch (error) {
handleError(error, 'Unable to update album name');
return;
}
albumName = newAlbumName;
};
</script>
<input
on:keydown={(e) => e.key === 'Enter' && e.currentTarget.blur()}
on:blur={handleUpdateName}
class="w-[99%] mb-2 border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned
? 'hover:border-gray-400'
: 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray"
type="text"
bind:value={newAlbumName}
disabled={!isOwned}
title="Edit Title"
placeholder="Add a title"
/>
@@ -21,7 +21,6 @@
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js'; import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js';
import UpdatePanel from '../shared-components/update-panel.svelte'; import UpdatePanel from '../shared-components/update-panel.svelte';
import { handlePromiseError } from '$lib/utils';
export let sharedLink: SharedLinkResponseDto; export let sharedLink: SharedLinkResponseDto;
export let user: UserResponseDto | undefined = undefined; export let user: UserResponseDto | undefined = undefined;
@@ -36,7 +35,7 @@
dragAndDropFilesStore.subscribe((value) => { dragAndDropFilesStore.subscribe((value) => {
if (value.isDragging && value.files.length > 0) { if (value.isDragging && value.files.length > 0) {
handlePromiseError(fileUploadHandler(value.files, album.id)); fileUploadHandler(value.files, album.id);
dragAndDropFilesStore.set({ isDragging: false, files: [] }); dragAndDropFilesStore.set({ isDragging: false, files: [] });
} }
}); });
@@ -68,7 +67,7 @@
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
onMount(() => { onMount(async () => {
document.addEventListener('keydown', onKeyboardPress); document.addEventListener('keydown', onKeyboardPress);
}); });
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { timeBeforeShowLoadingSpinner } from '$lib/constants'; import { timeBeforeShowLoadingSpinner } from '$lib/constants';
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; import { getAssetThumbnailUrl } from '$lib/utils';
import { getAssetType } from '$lib/utils/asset-utils'; import { getAssetType } from '$lib/utils/asset-utils';
import { autoGrowHeight } from '$lib/utils/autogrow'; import { autoGrowHeight } from '$lib/utils/autogrow';
import { clickOutside } from '$lib/utils/click-outside'; import { clickOutside } from '$lib/utils/click-outside';
@@ -79,7 +79,7 @@
$: { $: {
if (assetId && previousAssetId != assetId) { if (assetId && previousAssetId != assetId) {
handlePromiseError(getReactions()); getReactions();
previousAssetId = assetId; previousAssetId = assetId;
} }
} }
@@ -95,10 +95,10 @@
} }
}; };
const handleEnter = async (event: KeyboardEvent) => { const handleEnter = (event: KeyboardEvent) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
await handleSendComment(); handleSendComment();
return; return;
} }
}; };
@@ -10,7 +10,7 @@
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils'; import { getAssetJobMessage, isSharedLink } from '$lib/utils';
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils'; import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
@@ -174,8 +174,8 @@
$: { $: {
if (isShared && asset.id) { if (isShared && asset.id) {
handlePromiseError(getFavorite()); getFavorite();
handlePromiseError(getNumberOfComments()); getNumberOfComments();
} }
} }
@@ -184,9 +184,9 @@
if (value === SlideshowState.PlaySlideshow) { if (value === SlideshowState.PlaySlideshow) {
slideshowHistory.reset(); slideshowHistory.reset();
slideshowHistory.queue(asset.id); slideshowHistory.queue(asset.id);
handlePromiseError(handlePlaySlideshow()); handlePlaySlideshow();
} else if (value === SlideshowState.StopSlideshow) { } else if (value === SlideshowState.StopSlideshow) {
handlePromiseError(handleStopSlideshow()); handleStopSlideshow();
} }
}); });
@@ -226,7 +226,7 @@
} }
}); });
$: asset.id && !sharedLink && handlePromiseError(handleGetAllAlbums()); // Update the album information when the asset ID changes $: asset.id && !sharedLink && handleGetAllAlbums(); // Update the album information when the asset ID changes
const handleGetAllAlbums = async () => { const handleGetAllAlbums = async () => {
if (isSharedLink()) { if (isSharedLink()) {
@@ -247,7 +247,7 @@
isShowActivity = !isShowActivity; isShowActivity = !isShowActivity;
}; };
const handleKeypress = async (event: KeyboardEvent) => { const handleKeypress = (event: KeyboardEvent) => {
if (shouldIgnoreShortcut(event)) { if (shouldIgnoreShortcut(event)) {
return; return;
} }
@@ -264,7 +264,7 @@
case 'a': case 'a':
case 'A': { case 'A': {
if (shiftKey) { if (shiftKey) {
await toggleArchive(); toggleArchive();
} }
return; return;
} }
@@ -273,18 +273,18 @@
return; return;
} }
case 'ArrowRight': { case 'ArrowRight': {
await navigateAssetForward(); navigateAssetForward();
return; return;
} }
case 'd': case 'd':
case 'D': { case 'D': {
if (shiftKey) { if (shiftKey) {
await downloadFile(asset); downloadFile(asset);
} }
return; return;
} }
case 'Delete': { case 'Delete': {
await trashOrDelete(shiftKey); trashOrDelete(shiftKey);
return; return;
} }
case 'Escape': { case 'Escape': {
@@ -296,7 +296,7 @@
return; return;
} }
case 'f': { case 'f': {
await toggleFavorite(); toggleFavorite();
return; return;
} }
case 'i': { case 'i': {
@@ -326,7 +326,7 @@
slideshowHistory.queue(asset.id); slideshowHistory.queue(asset.id);
await setAssetId(asset.id); setAssetId(asset.id);
$restartSlideshowProgress = true; $restartSlideshowProgress = true;
}; };
@@ -369,17 +369,17 @@
$isShowDetail = !$isShowDetail; $isShowDetail = !$isShowDetail;
}; };
const trashOrDelete = async (force: boolean = false) => { const trashOrDelete = (force: boolean = false) => {
if (force || !isTrashEnabled) { if (force || !isTrashEnabled) {
if ($showDeleteModal) { if ($showDeleteModal) {
isShowDeleteConfirmation = true; isShowDeleteConfirmation = true;
return; return;
} }
await deleteAsset(); deleteAsset();
return; return;
} }
await trashAsset(); trashAsset();
return; return;
}; };
@@ -432,7 +432,7 @@
message: asset.isFavorite ? `Added to favorites` : `Removed from favorites`, message: asset.isFavorite ? `Added to favorites` : `Removed from favorites`,
}); });
} catch (error) { } catch (error) {
handleError(error, `Unable to ${asset.isFavorite ? `add asset to` : `remove asset from`} favorites`); await handleError(error, `Unable to ${asset.isFavorite ? `add asset to` : `remove asset from`} favorites`);
} }
}; };
@@ -472,7 +472,7 @@
message: asset.isArchived ? `Added to archive` : `Removed from archive`, message: asset.isArchived ? `Added to archive` : `Removed from archive`,
}); });
} catch (error) { } catch (error) {
handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`); await handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`);
} }
}; };
@@ -481,7 +481,7 @@
await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } }); await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
notificationController.show({ type: NotificationType.Info, message: getAssetJobMessage(name) }); notificationController.show({ type: NotificationType.Info, message: getAssetJobMessage(name) });
} catch (error) { } catch (error) {
handleError(error, `Unable to submit job`); await handleError(error, `Unable to submit job`);
} }
}; };
@@ -492,7 +492,7 @@
let assetViewerHtmlElement: HTMLElement; let assetViewerHtmlElement: HTMLElement;
const slideshowHistory = new SlideshowHistory((assetId: string) => { const slideshowHistory = new SlideshowHistory((assetId: string) => {
handlePromiseError(setAssetId(assetId)); setAssetId(assetId);
$restartSlideshowProgress = true; $restartSlideshowProgress = true;
}); });
@@ -550,7 +550,7 @@
dispatch('close'); dispatch('close');
notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 }); notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 });
} catch (error) { } catch (error) {
handleError(error, `Unable to unstack`); await handleError(error, `Unable to unstack`);
} }
}; };
</script> </script>
@@ -7,7 +7,7 @@
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink, handlePromiseError } from '$lib/utils'; import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink } from '$lib/utils';
import { delay, getAssetFilename } from '$lib/utils/asset-utils'; import { delay, getAssetFilename } from '$lib/utils/asset-utils';
import { autoGrowHeight } from '$lib/utils/autogrow'; import { autoGrowHeight } from '$lib/utils/autogrow';
import { clickOutside } from '$lib/utils/click-outside'; import { clickOutside } from '$lib/utils/click-outside';
@@ -78,7 +78,7 @@
originalDescription = description; originalDescription = description;
}; };
$: handlePromiseError(handleNewAsset(asset)); $: handleNewAsset(asset);
$: latlng = (() => { $: latlng = (() => {
const lat = asset.exifInfo?.latitude; const lat = asset.exifInfo?.latitude;
@@ -113,7 +113,7 @@
switch (event.key) { switch (event.key) {
case 'Enter': { case 'Enter': {
if (ctrl && event.target === textArea) { if (ctrl && event.target === textArea) {
await handleFocusOut(); handleFocusOut();
} }
} }
} }
@@ -4,7 +4,7 @@
import { boundingBoxesArray } from '$lib/stores/people.store'; import { boundingBoxesArray } from '$lib/stores/people.store';
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store'; import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
import { photoZoomState } from '$lib/stores/zoom-image.store'; import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getKey, handlePromiseError } from '$lib/utils'; import { getKey } from '$lib/utils';
import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { isWebCompatibleImage } from '$lib/utils/asset-utils';
import { getBoundingBox } from '$lib/utils/people-utils'; import { getBoundingBox } from '$lib/utils/people-utils';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
@@ -102,7 +102,7 @@
} }
}; };
const doZoomImage = () => { const doZoomImage = async () => {
setZoomImageWheelState({ setZoomImageWheelState({
currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1, currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1,
}); });
@@ -120,7 +120,7 @@
if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed && !$alwaysLoadOriginalFile) { if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed && !$alwaysLoadOriginalFile) {
hasZoomed = true; hasZoomed = true;
handlePromiseError(loadAssetData({ loadOriginal: true })); loadAssetData({ loadOriginal: true });
} }
}); });
</script> </script>
@@ -20,7 +20,7 @@
video.muted = false; video.muted = false;
dispatch('onVideoStarted'); dispatch('onVideoStarted');
} catch (error) { } catch (error) {
handleError(error, 'Unable to play video'); await handleError(error, 'Unable to play video');
} finally { } finally {
isVideoLoading = false; isVideoLoading = false;
} }
@@ -49,7 +49,7 @@
if (assetType === AssetTypeEnum.Image) { if (assetType === AssetTypeEnum.Image) {
image = $photoViewer; image = $photoViewer;
} else if (assetType === AssetTypeEnum.Video) { } else if (assetType === AssetTypeEnum.Video) {
const data = getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp); const data = await getAssetThumbnailUrl(assetId, ThumbnailFormat.Webp);
const img: HTMLImageElement = new Image(); const img: HTMLImageElement = new Image();
img.src = data; img.src = data;
@@ -43,10 +43,10 @@
dispatch('back'); dispatch('back');
}; };
const handleSwapPeople = async () => { const handleSwapPeople = () => {
[person, selectedPeople[0]] = [selectedPeople[0], person]; [person, selectedPeople[0]] = [selectedPeople[0], person];
$page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE); $page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
await goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`); goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
}; };
const onSelect = (selected: PersonResponseDto) => { const onSelect = (selected: PersonResponseDto) => {
@@ -3,7 +3,7 @@
import { timeBeforeShowLoadingSpinner } from '$lib/constants'; import { timeBeforeShowLoadingSpinner } from '$lib/constants';
import { boundingBoxesArray } from '$lib/stores/people.store'; import { boundingBoxesArray } from '$lib/stores/people.store';
import { websocketEvents } from '$lib/stores/websocket'; import { websocketEvents } from '$lib/stores/websocket';
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getPersonNameWithHiddenValue } from '$lib/utils/person'; import { getPersonNameWithHiddenValue } from '$lib/utils/person';
import { import {
@@ -46,8 +46,8 @@
let allPeople: PersonResponseDto[] = []; let allPeople: PersonResponseDto[] = [];
// timers // timers
let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>; let loaderLoadingDoneTimeout: NodeJS.Timeout;
let automaticRefreshTimeout: ReturnType<typeof setTimeout>; let automaticRefreshTimeout: NodeJS.Timeout;
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
close: void; close: void;
@@ -85,7 +85,7 @@
}; };
onMount(() => { onMount(() => {
handlePromiseError(loadPeople()); loadPeople();
return websocketEvents.on('on_person_thumbnail', onPersonThumbnail); return websocketEvents.on('on_person_thumbnail', onPersonThumbnail);
}); });
@@ -164,7 +164,7 @@
} }
}; };
const handlePersonPicker = (index: number) => { const handlePersonPicker = async (index: number) => {
editedPersonIndex = index; editedPersonIndex = index;
showSeletecFaces = true; showSeletecFaces = true;
}; };
@@ -132,7 +132,9 @@
title={'Assign selected assets to a new person'} title={'Assign selected assets to a new person'}
size={'sm'} size={'sm'}
disabled={disableButtons || hasSelection} disabled={disableButtons || hasSelection}
on:click={handleCreate} on:click={() => {
handleCreate();
}}
> >
{#if !showLoadingSpinnerCreate} {#if !showLoadingSpinnerCreate}
<Icon path={mdiPlus} size={18} /> <Icon path={mdiPlus} size={18} />
@@ -145,7 +147,9 @@
size={'sm'} size={'sm'}
title={'Assign selected assets to an existing person'} title={'Assign selected assets to an existing person'}
disabled={disableButtons || !hasSelection} disabled={disableButtons || !hasSelection}
on:click={handleReassign} on:click={() => {
handleReassign();
}}
> >
{#if !showLoadingSpinnerReassign} {#if !showLoadingSpinnerReassign}
<div> <div>

Some files were not shown because too many files have changed in this diff Show More