diff --git a/cli/Dockerfile b/cli/Dockerfile index 2eab54831..6195d8d82 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine3.19@sha256:bf77dc26e48ea95fca9d1aceb5acfa69d2e546b765ec2abfb502975f1a2d4def as core +FROM node:20-alpine3.19@sha256:ef3f47741e161900ddd07addcaca7e76534a9205e4cd73b2ed091ba339004a75 as core WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/cli/package-lock.json b/cli/package-lock.json index dd9c2cc00..02a6d37f5 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1251,16 +1251,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz", - "integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/type-utils": "7.3.1", - "@typescript-eslint/utils": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1286,15 +1286,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz", - "integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" }, "engines": { @@ -1314,13 +1314,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz", - "integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1331,13 +1331,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz", - "integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/utils": "7.3.1", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1358,9 +1358,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz", - "integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1371,13 +1371,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz", - "integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1399,17 +1399,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz", - "integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" }, "engines": { @@ -1424,12 +1424,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz", - "integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2555,16 +2555,16 @@ } }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -3411,12 +3411,12 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "dev": true, "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -4456,13 +4456,13 @@ } }, "node_modules/vite": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.3.tgz", - "integrity": "sha512-+i1oagbvkVIhEy9TnEV+fgXsng13nZM90JQbrcPrf6DvW2mXARlz+DK7DLiDP+qeKoD1FCVx/1SpFL1CLq9Mhw==", + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz", + "integrity": "sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==", "dev": true, "dependencies": { "esbuild": "^0.20.1", - "postcss": "^8.4.36", + "postcss": "^8.4.38", "rollup": "^4.13.0" }, "bin": { diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index b8a6b7633..1bcb5e327 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -76,7 +76,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:5ccad477d0057e62a7cd1981ffcc43785ac10c5a35522dc207466ff7e7ec845f + image: prom/prometheus@sha256:dec2018ae55885fed717f25c289b8c9cff0bf5fbb9e619fb49b6161ac493c016 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 884beee4b..98b3db996 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -253,8 +253,19 @@ The initial backup is the most intensive due to the number of jobs running. The ### Can I limit the amount of CPU and RAM usage? -By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. -You can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit) to learn how to limit this. +By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. To limit this, you can add the following to the `docker-compose.yml` block of any containers that you want to have limited resources. + +```yaml +deploy: + resources: + limits: + # Number of CPU threads + cpus: '1.00' + # Gigabytes of memory + memory: '1G' +``` + +For more details, you can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit). ### How can I boost machine learning speed? @@ -296,6 +307,17 @@ You may need to add mount points or docker volumes for the following internal co The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`. +For a further hardened system, you can add the following block to every container except for `immich_postgres`. + +```yaml +security_opt: + # Prevent escalation of privileges after container is started + - no-new-privileges:true +cap_drop: + # Prevent access to raw network traffic + - NET_RAW +``` + ### How can I **purge** data from Immich? Data for Immich comes in two forms: diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index dc61c37d4..d553ca7d8 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -18,12 +18,11 @@ Thanks for being interested in contributing 😊 ### Server and web app -This environment includes the following services: +This environment includes the services below. Additional details are available in each service's README. -- Core server - `/server/src/immich` -- Machine learning - `/machine-learning` -- Microservices - `/server/src/microservicess` -- Web app - `/web` +- Server - [`/server`](https://github.com/immich-app/immich/tree/main/server) +- Web app - [`/web`](https://github.com/immich-app/immich/tree/main/web) +- Machine learning - [`/machine-learning`](https://github.com/immich-app/immich/tree/main/machine-learning) - Redis - PostgreSQL development database with exposed port `5432` so you can use any database client to acess it diff --git a/docs/docs/features/supported-formats.md b/docs/docs/features/supported-formats.md new file mode 100644 index 000000000..a2dc56b66 --- /dev/null +++ b/docs/docs/features/supported-formats.md @@ -0,0 +1,42 @@ +# Supported formats + +Immich supports a number of image and video formats, the most common of which are outlined here. + +:::note +For the full list, you can refer to the [Immich source code](https://github.com/immich-app/immich/blob/main/server/src/utils/mime-types.ts). +::: + +## Image formats + +| Format | Extension(s) | Supported? | Notes | +| :-------- | :---------------------------- | :----------------: | :-------------- | +| `AVIF` | `.avif` | :white_check_mark: | | +| `BMP` | `.bmp` | :white_check_mark: | | +| `GIF` | `.gif` | :white_check_mark: | | +| `HEIC` | `.heic` | :white_check_mark: | | +| `HEIF` | `.heif` | :white_check_mark: | | +| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | | +| `JPEG XL` | `.jxl` | :white_check_mark: | | +| `PNG` | `.png` | :white_check_mark: | | +| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop | +| `RAW` | `.raw` | :white_check_mark: | | +| `RW2` | `.rw2` | :white_check_mark: | | +| `SVG` | `.svg` | :white_check_mark: | | +| `TIFF` | `.tif` `.tiff` | :white_check_mark: | | +| `WEBP` | `.webp` | :white_check_mark: | | + +## Video formats + +| Format | Extension(s) | Supported? | Notes | +| :---------- | :-------------------- | :----------------: | :---- | +| `3GPP` | `.3gp` `.3gpp` | :white_check_mark: | | +| `AVI` | `.avi` | :white_check_mark: | | +| `FLV` | `.flv` | :white_check_mark: | | +| `M4V` | `.m4v` | :white_check_mark: | | +| `MATROSKA` | `.mkv` | :white_check_mark: | | +| `MP2T` | `.mts` `.m2ts` | :white_check_mark: | | +| `MP4` | `.mp4` `.insv` | :white_check_mark: | | +| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | | +| `QUICKTIME` | `.mov` | :white_check_mark: | | +| `WEBM` | `.webm` | :white_check_mark: | | +| `WMV` | `.wmv` | :white_check_mark: | | diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index fe369f899..e8252f25d 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -45,7 +45,7 @@ SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "ex ``` ```sql title="Without thumbnails" -SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL; +SELECT * FROM "assets" WHERE "assets"."previewPath" IS NULL OR "assets"."thumbnailPath" IS NULL; ``` ```sql title="By type" diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index 9a1d1acb1..a890d674b 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -114,9 +114,11 @@ The default configuration looks like this: "hashVerificationEnabled": true, "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}" }, - "thumbnail": { - "webpSize": 250, - "jpegSize": 1440, + "image": { + "thumbnailFormat": "webp", + "thumbnailSize": 250, + "previewFormat": "jpeg", + "previewSize": 1440, "quality": 80, "colorspace": "p3" }, diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 9da1f3ce9..a8cbe6059 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -17,10 +17,10 @@ If this should not work, try running `docker compose up -d --force-recreate`. ## Docker Compose -| Variable | Description | Default | Services | -| :---------------- | :-------------------- | :-------: | :-------------------------------------------------- | -| `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning, web, proxy | -| `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices | +| Variable | Description | Default | Services | +| :---------------- | :-------------------- | :-------: | :-------------------------------------- | +| `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning | +| `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices | :::tip @@ -30,15 +30,15 @@ These environment variables are used by the `docker-compose.yml` file and do **N ## General -| Variable | Description | Default | Services | -| :------------------------------ | :------------------------------------------- | :------------------: | :------------------------------------------- | -| `TZ` | Timezone | | microservices | -| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning, web | -| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices | -| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload` | server, microservices | -| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices | -| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | -| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices | +| Variable | Description | Default | Services | +| :------------------------------ | :------------------------------------------- | :------------------: | :-------------------------------------- | +| `TZ` | Timezone | | microservices | +| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning | +| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning | +| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload` | server, microservices | +| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices | +| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | +| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices | :::tip `TZ` should be set to a `TZ identifier` from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). For example, `TZ="Etc/UTC"`. @@ -50,7 +50,6 @@ These environment variables are used by the `docker-compose.yml` file and do **N | Variable | Description | Default | Services | | :---------------------- | :-------------------- | :-------: | :--------------- | -| `PORT` | Web Port | `3000` | web | | `SERVER_PORT` | Server Port | `3001` | server | | `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices | | `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning | @@ -164,13 +163,14 @@ The following variables support the use of [Docker secrets](https://docs.docker. To use any of these, replace the regular environment variable with the equivalent `_FILE` environment variable. The value of the `_FILE` variable should be set to the path of a file containing the variable value. -| Regular Variable | Equivalent Docker Secrets '\_FILE' Variable | -| :----------------: | :-----------------------------------------: | -| `DB_HOSTNAME` | `DB_HOSTNAME_FILE`\*1 | -| `DB_DATABASE_NAME` | `DB_DATABASE_NAME_FILE`\*1 | -| `DB_USERNAME` | `DB_USERNAME_FILE`\*1 | -| `DB_PASSWORD` | `DB_PASSWORD_FILE`\*1 | -| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`\*2 | +| Regular Variable | Equivalent Docker Secrets '\_FILE' Variable | +| :----------------- | :------------------------------------------ | +| `DB_HOSTNAME` | `DB_HOSTNAME_FILE`\*1 | +| `DB_DATABASE_NAME` | `DB_DATABASE_NAME_FILE`\*1 | +| `DB_USERNAME` | `DB_USERNAME_FILE`\*1 | +| `DB_PASSWORD` | `DB_PASSWORD_FILE`\*1 | +| `DB_URL` | `DB_URL_FILE`\*1 | +| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`\*2 | \*1: See the [official documentation](https://github.com/docker-library/docs/tree/master/postgres#docker-secrets) for details on how to use Docker Secrets in the Postgres image. diff --git a/docs/package-lock.json b/docs/package-lock.json index f173cef2c..d86f69ac9 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -3429,9 +3429,9 @@ } }, "node_modules/@tsconfig/docusaurus": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/docusaurus/-/docusaurus-2.0.2.tgz", - "integrity": "sha512-12HWfYmgUl4M2o76/TFufGtI68wl2k/b8qPrIrG7ci9YJLrpAtadpy897Bz5v29Mlkr7a1Hq4KHdQTKtU+2rhQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/docusaurus/-/docusaurus-2.0.3.tgz", + "integrity": "sha512-3l1L5PzWVa7l0691TjnsZ0yOIEwG9DziSqu5IPZPlI5Dowi7z42cEym8Y35GHbgHvPcBfNxfrbxm7Cncn4nByQ==", "dev": true }, "node_modules/@types/acorn": { @@ -15781,9 +15781,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -15793,7 +15793,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 127ef5767..894029102 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1277,16 +1277,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz", - "integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/type-utils": "7.3.1", - "@typescript-eslint/utils": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1312,15 +1312,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz", - "integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" }, "engines": { @@ -1340,13 +1340,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz", - "integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1357,13 +1357,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz", - "integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/utils": "7.3.1", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1384,9 +1384,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz", - "integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1397,13 +1397,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz", - "integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1449,17 +1449,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz", - "integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" }, "engines": { @@ -1474,12 +1474,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz", - "integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { diff --git a/e2e/src/api/specs/memory.e2e-spec.ts b/e2e/src/api/specs/memory.e2e-spec.ts new file mode 100644 index 000000000..35af1fea9 --- /dev/null +++ b/e2e/src/api/specs/memory.e2e-spec.ts @@ -0,0 +1,376 @@ +import { + AssetFileUploadResponseDto, + LoginResponseDto, + MemoryResponseDto, + MemoryType, + createMemory, + getMemory, +} from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, asBearerAuth, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, describe, expect, it } from 'vitest'; + +describe('/memories', () => { + let admin: LoginResponseDto; + let user: LoginResponseDto; + let adminAsset: AssetFileUploadResponseDto; + let userAsset1: AssetFileUploadResponseDto; + let userAsset2: AssetFileUploadResponseDto; + let userMemory: MemoryResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + + admin = await utils.adminSetup(); + user = await utils.userSetup(admin.accessToken, createUserDto.user1); + [adminAsset, userAsset1, userAsset2] = await Promise.all([ + utils.createAsset(admin.accessToken), + utils.createAsset(user.accessToken), + utils.createAsset(user.accessToken), + ]); + userMemory = await createMemory( + { + memoryCreateDto: { + type: MemoryType.OnThisDay, + memoryAt: new Date(2021).toISOString(), + data: { year: 2021 }, + assetIds: [], + }, + }, + { headers: asBearerAuth(user.accessToken) }, + ); + }); + + describe('GET /memories', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/memories'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + }); + + describe('POST /memories', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/memories'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should validate data when type is on this day', async () => { + const { status, body } = await request(app) + .post('/memories') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ + type: 'on_this_day', + data: {}, + memoryAt: new Date(2021).toISOString(), + }); + + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), + ); + }); + + it('should create a new memory', async () => { + const { status, body } = await request(app) + .post('/memories') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ + type: 'on_this_day', + data: { year: 2021 }, + memoryAt: new Date(2021).toISOString(), + }); + + expect(status).toBe(201); + expect(body).toEqual({ + id: expect.any(String), + type: 'on_this_day', + data: { year: 2021 }, + createdAt: expect.any(String), + updatedAt: expect.any(String), + deletedAt: null, + seenAt: null, + isSaved: false, + memoryAt: expect.any(String), + ownerId: user.userId, + assets: [], + }); + }); + + it('should create a new memory (with assets)', async () => { + const { status, body } = await request(app) + .post('/memories') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ + type: 'on_this_day', + data: { year: 2021 }, + memoryAt: new Date(2021).toISOString(), + assetIds: [userAsset1.id, userAsset2.id], + }); + + expect(status).toBe(201); + expect(body).toMatchObject({ + id: expect.any(String), + assets: expect.arrayContaining([ + expect.objectContaining({ id: userAsset1.id }), + expect.objectContaining({ id: userAsset2.id }), + ]), + }); + expect(body.assets).toHaveLength(2); + }); + + it('should create a new memory and ignore assets the user does not have access to', async () => { + const { status, body } = await request(app) + .post('/memories') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ + type: 'on_this_day', + data: { year: 2021 }, + memoryAt: new Date(2021).toISOString(), + assetIds: [userAsset1.id, adminAsset.id], + }); + + expect(status).toBe(201); + expect(body).toMatchObject({ + id: expect.any(String), + assets: [expect.objectContaining({ id: userAsset1.id })], + }); + expect(body.assets).toHaveLength(1); + }); + }); + + describe('GET /memories/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get(`/memories/${uuidDto.invalid}`); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .get(`/memories/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${user.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(`/memories/${userMemory.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should get the memory', async () => { + const { status, body } = await request(app) + .get(`/memories/${userMemory.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toMatchObject({ id: userMemory.id }); + }); + }); + + describe('PUT /memories/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put(`/memories/${uuidDto.invalid}`).send({ isSaved: true }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .put(`/memories/${uuidDto.invalid}`) + .send({ isSaved: true }) + .set('Authorization', `Bearer ${user.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(`/memories/${userMemory.id}`) + .send({ isSaved: true }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should update the memory', async () => { + const before = await getMemory({ id: userMemory.id }, { headers: asBearerAuth(user.accessToken) }); + expect(before.isSaved).toBe(false); + + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}`) + .send({ isSaved: true }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toMatchObject({ + id: userMemory.id, + isSaved: true, + }); + }); + }); + + describe('PUT /memories/:id/assets', () => { + it('should require authentication', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .put(`/memories/${uuidDto.invalid}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${user.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(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require a valid asset id', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}/assets`) + .send({ ids: [uuidDto.invalid] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + }); + + it('should require asset access', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}/assets`) + .send({ ids: [adminAsset.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toHaveLength(1); + expect(body[0]).toEqual({ + id: adminAsset.id, + success: false, + error: 'no_permission', + }); + }); + + it('should add assets to the memory', async () => { + const { status, body } = await request(app) + .put(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toHaveLength(1); + expect(body[0]).toEqual({ id: userAsset1.id, success: true }); + }); + }); + + describe('DELETE /memories/:id/assets', () => { + it('should require authentication', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .delete(`/memories/${uuidDto.invalid}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${user.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) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require a valid asset id', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [uuidDto.invalid] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + }); + + it('should only remove assets in the memory', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [adminAsset.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toHaveLength(1); + expect(body[0]).toEqual({ + id: adminAsset.id, + success: false, + error: 'not_found', + }); + }); + + it('should remove assets from the memory', async () => { + const { status, body } = await request(app) + .delete(`/memories/${userMemory.id}/assets`) + .send({ ids: [userAsset1.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toHaveLength(1); + expect(body[0]).toEqual({ id: userAsset1.id, success: true }); + }); + }); + + describe('DELETE /memories/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).delete(`/memories/${uuidDto.invalid}`); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .delete(`/memories/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${user.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) + .delete(`/memories/${userMemory.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should delete the memory', async () => { + const { status } = await request(app) + .delete(`/memories/${userMemory.id}`) + .send({ isSaved: true }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + }); + }); +}); diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index afe131228..a613cdd28 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -133,7 +133,7 @@ describe('/search', () => { assetLast = assets.at(-1) as AssetFileUploadResponseDto; await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) }); - }); + }, 30_000); afterAll(async () => { utils.disconnectWebsocket(websocket); diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 54dd16292..a6efaa1e6 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -64,33 +64,33 @@ trio = ["trio (>=0.23)"] [[package]] name = "black" -version = "24.2.0" +version = "24.3.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, + {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, + {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, + {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, + {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, + {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, + {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, + {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, + {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, + {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, + {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, + {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, + {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, + {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, + {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, + {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, + {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, + {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, + {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, + {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, + {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, + {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, + {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, ] [package.dependencies] @@ -2110,61 +2110,62 @@ numpy = [ [[package]] name = "orjson" -version = "3.9.15" +version = "3.10.0" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" 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.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4feeb41882e8aa17634b589533baafdceb387e01e117b1ec65534ec724023d04"}, - {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fbbeb3c9b2edb5fd044b2a070f127a0ac456ffd079cb82746fc84af01ef021a4"}, - {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b66bcc5670e8a6b78f0313bcb74774c8291f6f8aeef10fe70e910b8040f3ab75"}, - {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2973474811db7b35c30248d1129c64fd2bdf40d57d84beed2a9a379a6f57d0ab"}, - {file = "orjson-3.9.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fe41b6f72f52d3da4db524c8653e46243c8c92df826ab5ffaece2dba9cccd58"}, - {file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4228aace81781cc9d05a3ec3a6d2673a1ad0d8725b4e915f1089803e9efd2b99"}, - {file = "orjson-3.9.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6f7b65bfaf69493c73423ce9db66cfe9138b2f9ef62897486417a8fcb0a92bfe"}, - {file = "orjson-3.9.15-cp310-none-win32.whl", hash = "sha256:2d99e3c4c13a7b0fb3792cc04c2829c9db07838fb6973e578b85c1745e7d0ce7"}, - {file = "orjson-3.9.15-cp310-none-win_amd64.whl", hash = "sha256:b725da33e6e58e4a5d27958568484aa766e825e93aa20c26c91168be58e08cbb"}, - {file = "orjson-3.9.15-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c8e8fe01e435005d4421f183038fc70ca85d2c1e490f51fb972db92af6e047c2"}, - {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87f1097acb569dde17f246faa268759a71a2cb8c96dd392cd25c668b104cad2f"}, - {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff0f9913d82e1d1fadbd976424c316fbc4d9c525c81d047bbdd16bd27dd98cfc"}, - {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8055ec598605b0077e29652ccfe9372247474375e0e3f5775c91d9434e12d6b1"}, - {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6768a327ea1ba44c9114dba5fdda4a214bdb70129065cd0807eb5f010bfcbb5"}, - {file = "orjson-3.9.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12365576039b1a5a47df01aadb353b68223da413e2e7f98c02403061aad34bde"}, - {file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:71c6b009d431b3839d7c14c3af86788b3cfac41e969e3e1c22f8a6ea13139404"}, - {file = "orjson-3.9.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e18668f1bd39e69b7fed19fa7cd1cd110a121ec25439328b5c89934e6d30d357"}, - {file = "orjson-3.9.15-cp311-none-win32.whl", hash = "sha256:62482873e0289cf7313461009bf62ac8b2e54bc6f00c6fabcde785709231a5d7"}, - {file = "orjson-3.9.15-cp311-none-win_amd64.whl", hash = "sha256:b3d336ed75d17c7b1af233a6561cf421dee41d9204aa3cfcc6c9c65cd5bb69a8"}, - {file = "orjson-3.9.15-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:82425dd5c7bd3adfe4e94c78e27e2fa02971750c2b7ffba648b0f5d5cc016a73"}, - {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c51378d4a8255b2e7c1e5cc430644f0939539deddfa77f6fac7b56a9784160a"}, - {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae4e06be04dc00618247c4ae3f7c3e561d5bc19ab6941427f6d3722a0875ef7"}, - {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcef128f970bb63ecf9a65f7beafd9b55e3aaf0efc271a4154050fc15cdb386e"}, - {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b72758f3ffc36ca566ba98a8e7f4f373b6c17c646ff8ad9b21ad10c29186f00d"}, - {file = "orjson-3.9.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c57bc7b946cf2efa67ac55766e41764b66d40cbd9489041e637c1304400494"}, - {file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:946c3a1ef25338e78107fba746f299f926db408d34553b4754e90a7de1d44068"}, - {file = "orjson-3.9.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2f256d03957075fcb5923410058982aea85455d035607486ccb847f095442bda"}, - {file = "orjson-3.9.15-cp312-none-win_amd64.whl", hash = "sha256:5bb399e1b49db120653a31463b4a7b27cf2fbfe60469546baf681d1b39f4edf2"}, - {file = "orjson-3.9.15-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b17f0f14a9c0ba55ff6279a922d1932e24b13fc218a3e968ecdbf791b3682b25"}, - {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f6cbd8e6e446fb7e4ed5bac4661a29e43f38aeecbf60c4b900b825a353276a1"}, - {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76bc6356d07c1d9f4b782813094d0caf1703b729d876ab6a676f3aaa9a47e37c"}, - {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfa97090e2d6f73dced247a2f2d8004ac6449df6568f30e7fa1a045767c69a6"}, - {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7413070a3e927e4207d00bd65f42d1b780fb0d32d7b1d951f6dc6ade318e1b5a"}, - {file = "orjson-3.9.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cf1596680ac1f01839dba32d496136bdd5d8ffb858c280fa82bbfeb173bdd40"}, - {file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:809d653c155e2cc4fd39ad69c08fdff7f4016c355ae4b88905219d3579e31eb7"}, - {file = "orjson-3.9.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:920fa5a0c5175ab14b9c78f6f820b75804fb4984423ee4c4f1e6d748f8b22bc1"}, - {file = "orjson-3.9.15-cp38-none-win32.whl", hash = "sha256:2b5c0f532905e60cf22a511120e3719b85d9c25d0e1c2a8abb20c4dede3b05a5"}, - {file = "orjson-3.9.15-cp38-none-win_amd64.whl", hash = "sha256:67384f588f7f8daf040114337d34a5188346e3fae6c38b6a19a2fe8c663a2f9b"}, - {file = "orjson-3.9.15-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6fc2fe4647927070df3d93f561d7e588a38865ea0040027662e3e541d592811e"}, - {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34cbcd216e7af5270f2ffa63a963346845eb71e174ea530867b7443892d77180"}, - {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f541587f5c558abd93cb0de491ce99a9ef8d1ae29dd6ab4dbb5a13281ae04cbd"}, - {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92255879280ef9c3c0bcb327c5a1b8ed694c290d61a6a532458264f887f052cb"}, - {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a1f57fb601c426635fcae9ddbe90dfc1ed42245eb4c75e4960440cac667262"}, - {file = "orjson-3.9.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ede0bde16cc6e9b96633df1631fbcd66491d1063667f260a4f2386a098393790"}, - {file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e88b97ef13910e5f87bcbc4dd7979a7de9ba8702b54d3204ac587e83639c0c2b"}, - {file = "orjson-3.9.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57d5d8cf9c27f7ef6bc56a5925c7fbc76b61288ab674eb352c26ac780caa5b10"}, - {file = "orjson-3.9.15-cp39-none-win32.whl", hash = "sha256:001f4eb0ecd8e9ebd295722d0cbedf0748680fb9998d3993abaed2f40587257a"}, - {file = "orjson-3.9.15-cp39-none-win_amd64.whl", hash = "sha256:ea0b183a5fe6b2b45f3b854b0d19c4e932d6f5934ae1f723b07cf9560edd4ec7"}, - {file = "orjson-3.9.15.tar.gz", hash = "sha256:95cae920959d772f30ab36d3b25f83bb0f3be671e986c72ce22f8fa700dae061"}, + {file = "orjson-3.10.0-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47af5d4b850a2d1328660661f0881b67fdbe712aea905dadd413bdea6f792c33"}, + {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c90681333619d78360d13840c7235fdaf01b2b129cb3a4f1647783b1971542b6"}, + {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:400c5b7c4222cb27b5059adf1fb12302eebcabf1978f33d0824aa5277ca899bd"}, + {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5dcb32e949eae80fb335e63b90e5808b4b0f64e31476b3777707416b41682db5"}, + {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7d507c7493252c0a0264b5cc7e20fa2f8622b8a83b04d819b5ce32c97cf57b"}, + {file = "orjson-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e286a51def6626f1e0cc134ba2067dcf14f7f4b9550f6dd4535fd9d79000040b"}, + {file = "orjson-3.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8acd4b82a5f3a3ec8b1dc83452941d22b4711964c34727eb1e65449eead353ca"}, + {file = "orjson-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:30707e646080dd3c791f22ce7e4a2fc2438765408547c10510f1f690bd336217"}, + {file = "orjson-3.10.0-cp310-none-win32.whl", hash = "sha256:115498c4ad34188dcb73464e8dc80e490a3e5e88a925907b6fedcf20e545001a"}, + {file = "orjson-3.10.0-cp310-none-win_amd64.whl", hash = "sha256:6735dd4a5a7b6df00a87d1d7a02b84b54d215fb7adac50dd24da5997ffb4798d"}, + {file = "orjson-3.10.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9587053e0cefc284e4d1cd113c34468b7d3f17666d22b185ea654f0775316a26"}, + {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bef1050b1bdc9ea6c0d08468e3e61c9386723633b397e50b82fda37b3563d72"}, + {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d16c6963ddf3b28c0d461641517cd312ad6b3cf303d8b87d5ef3fa59d6844337"}, + {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4251964db47ef090c462a2d909f16c7c7d5fe68e341dabce6702879ec26d1134"}, + {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73bbbdc43d520204d9ef0817ac03fa49c103c7f9ea94f410d2950755be2c349c"}, + {file = "orjson-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:414e5293b82373606acf0d66313aecb52d9c8c2404b1900683eb32c3d042dbd7"}, + {file = "orjson-3.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:feaed5bb09877dc27ed0d37f037ddef6cb76d19aa34b108db270d27d3d2ef747"}, + {file = "orjson-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5127478260db640323cea131ee88541cb1a9fbce051f0b22fa2f0892f44da302"}, + {file = "orjson-3.10.0-cp311-none-win32.whl", hash = "sha256:b98345529bafe3c06c09996b303fc0a21961820d634409b8639bc16bd4f21b63"}, + {file = "orjson-3.10.0-cp311-none-win_amd64.whl", hash = "sha256:658ca5cee3379dd3d37dbacd43d42c1b4feee99a29d847ef27a1cb18abdfb23f"}, + {file = "orjson-3.10.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4329c1d24fd130ee377e32a72dc54a3c251e6706fccd9a2ecb91b3606fddd998"}, + {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef0f19fdfb6553342b1882f438afd53c7cb7aea57894c4490c43e4431739c700"}, + {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4f60db24161534764277f798ef53b9d3063092f6d23f8f962b4a97edfa997a0"}, + {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1de3fd5c7b208d836f8ecb4526995f0d5877153a4f6f12f3e9bf11e49357de98"}, + {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f93e33f67729d460a177ba285002035d3f11425ed3cebac5f6ded4ef36b28344"}, + {file = "orjson-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:237ba922aef472761acd697eef77fef4831ab769a42e83c04ac91e9f9e08fa0e"}, + {file = "orjson-3.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98c1bfc6a9bec52bc8f0ab9b86cc0874b0299fccef3562b793c1576cf3abb570"}, + {file = "orjson-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30d795a24be16c03dca0c35ca8f9c8eaaa51e3342f2c162d327bd0225118794a"}, + {file = "orjson-3.10.0-cp312-none-win32.whl", hash = "sha256:6a3f53dc650bc860eb26ec293dfb489b2f6ae1cbfc409a127b01229980e372f7"}, + {file = "orjson-3.10.0-cp312-none-win_amd64.whl", hash = "sha256:983db1f87c371dc6ffc52931eb75f9fe17dc621273e43ce67bee407d3e5476e9"}, + {file = "orjson-3.10.0-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a667769a96a72ca67237224a36faf57db0c82ab07d09c3aafc6f956196cfa1b"}, + {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade1e21dfde1d37feee8cf6464c20a2f41fa46c8bcd5251e761903e46102dc6b"}, + {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23c12bb4ced1c3308eff7ba5c63ef8f0edb3e4c43c026440247dd6c1c61cea4b"}, + {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2d014cf8d4dc9f03fc9f870de191a49a03b1bcda51f2a957943fb9fafe55aac"}, + {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eadecaa16d9783affca33597781328e4981b048615c2ddc31c47a51b833d6319"}, + {file = "orjson-3.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd583341218826f48bd7c6ebf3310b4126216920853cbc471e8dbeaf07b0b80e"}, + {file = "orjson-3.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:90bfc137c75c31d32308fd61951d424424426ddc39a40e367704661a9ee97095"}, + {file = "orjson-3.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13b5d3c795b09a466ec9fcf0bd3ad7b85467d91a60113885df7b8d639a9d374b"}, + {file = "orjson-3.10.0-cp38-none-win32.whl", hash = "sha256:5d42768db6f2ce0162544845facb7c081e9364a5eb6d2ef06cd17f6050b048d8"}, + {file = "orjson-3.10.0-cp38-none-win_amd64.whl", hash = "sha256:33e6655a2542195d6fd9f850b428926559dee382f7a862dae92ca97fea03a5ad"}, + {file = "orjson-3.10.0-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4050920e831a49d8782a1720d3ca2f1c49b150953667eed6e5d63a62e80f46a2"}, + {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1897aa25a944cec774ce4a0e1c8e98fb50523e97366c637b7d0cddabc42e6643"}, + {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9bf565a69e0082ea348c5657401acec3cbbb31564d89afebaee884614fba36b4"}, + {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6ebc17cfbbf741f5c1a888d1854354536f63d84bee537c9a7c0335791bb9009"}, + {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2817877d0b69f78f146ab305c5975d0618df41acf8811249ee64231f5953fee"}, + {file = "orjson-3.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57d017863ec8aa4589be30a328dacd13c2dc49de1c170bc8d8c8a98ece0f2925"}, + {file = "orjson-3.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:22c2f7e377ac757bd3476ecb7480c8ed79d98ef89648f0176deb1da5cd014eb7"}, + {file = "orjson-3.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e62ba42bfe64c60c1bc84799944f80704e996592c6b9e14789c8e2a303279912"}, + {file = "orjson-3.10.0-cp39-none-win32.whl", hash = "sha256:60c0b1bdbccd959ebd1575bd0147bd5e10fc76f26216188be4a36b691c937077"}, + {file = "orjson-3.10.0-cp39-none-win_amd64.whl", hash = "sha256:175a41500ebb2fdf320bf78e8b9a75a1279525b62ba400b2b2444e274c2c8bee"}, + {file = "orjson-3.10.0.tar.gz", hash = "sha256:ba4d8cac5f2e2cff36bea6b6481cdb92b38c202bcec603d6f5ff91960595a1ed"}, ] [[package]] diff --git a/mobile/lib/shared/models/album.dart b/mobile/lib/shared/models/album.dart index 3ed8f69ea..55c105c74 100644 --- a/mobile/lib/shared/models/album.dart +++ b/mobile/lib/shared/models/album.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/utils/datetime_comparison.dart'; import 'package:isar/isar.dart'; import 'package:openapi/api.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -72,21 +73,18 @@ class Album { @override bool operator ==(other) { if (other is! Album) return false; - - final lastModifiedAssetTimestampIsSetAndEqual = - lastModifiedAssetTimestamp != null && - other.lastModifiedAssetTimestamp != null - ? lastModifiedAssetTimestamp! - .isAtSameMomentAs(other.lastModifiedAssetTimestamp!) - : true; - return id == other.id && remoteId == other.remoteId && localId == other.localId && name == other.name && createdAt.isAtSameMomentAs(other.createdAt) && modifiedAt.isAtSameMomentAs(other.modifiedAt) && - lastModifiedAssetTimestampIsSetAndEqual && + isAtSameMomentAs(startDate, other.startDate) && + isAtSameMomentAs(endDate, other.endDate) && + isAtSameMomentAs( + lastModifiedAssetTimestamp, + other.lastModifiedAssetTimestamp, + ) && shared == other.shared && activityEnabled == other.activityEnabled && owner.value == other.owner.value && @@ -104,6 +102,8 @@ class Album { name.hashCode ^ createdAt.hashCode ^ modifiedAt.hashCode ^ + startDate.hashCode ^ + endDate.hashCode ^ lastModifiedAssetTimestamp.hashCode ^ shared.hashCode ^ activityEnabled.hashCode ^ diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart index a441091d3..e547eb012 100644 --- a/mobile/lib/shared/services/sync.service.dart +++ b/mobile/lib/shared/services/sync.service.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/hash.service.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; +import 'package:immich_mobile/utils/datetime_comparison.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; @@ -343,8 +344,13 @@ class SyncService { album.name = dto.albumName; album.shared = dto.shared; + album.createdAt = dto.createdAt; album.modifiedAt = dto.updatedAt; + album.startDate = dto.startDate; + album.endDate = dto.endDate; album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; + album.shared = dto.shared; + album.activityEnabled = dto.isActivityEnabled; if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) { album.thumbnail.value = await _db.assets .where() @@ -863,12 +869,10 @@ bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) { dto.shared != a.shared || dto.sharedUsers.length != a.sharedUsers.length || !dto.updatedAt.isAtSameMomentAs(a.modifiedAt) || - (dto.lastModifiedAssetTimestamp == null && - a.lastModifiedAssetTimestamp != null) || - (dto.lastModifiedAssetTimestamp != null && - a.lastModifiedAssetTimestamp == null) || - (dto.lastModifiedAssetTimestamp != null && - a.lastModifiedAssetTimestamp != null && - !dto.lastModifiedAssetTimestamp! - .isAtSameMomentAs(a.lastModifiedAssetTimestamp!)); + !isAtSameMomentAs(dto.startDate, a.startDate) || + !isAtSameMomentAs(dto.endDate, a.endDate) || + !isAtSameMomentAs( + dto.lastModifiedAssetTimestamp, + a.lastModifiedAssetTimestamp, + ); } diff --git a/mobile/lib/utils/datetime_comparison.dart b/mobile/lib/utils/datetime_comparison.dart new file mode 100644 index 000000000..8c53ea45b --- /dev/null +++ b/mobile/lib/utils/datetime_comparison.dart @@ -0,0 +1,3 @@ +bool isAtSameMomentAs(DateTime? a, DateTime? b) => + (a == null && b == null) || + ((a != null && b != null) && a.isAtSameMomentAs(b)); diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 9ec77670f..263687549 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -72,6 +72,7 @@ doc/FileChecksumResponseDto.md doc/FileReportDto.md doc/FileReportFixDto.md doc/FileReportItemDto.md +doc/ImageFormat.md doc/JobApi.md doc/JobCommand.md doc/JobCommandDto.md @@ -89,7 +90,12 @@ doc/LoginResponseDto.md doc/LogoutResponseDto.md doc/MapMarkerResponseDto.md doc/MapTheme.md +doc/MemoryApi.md +doc/MemoryCreateDto.md doc/MemoryLaneResponseDto.md +doc/MemoryResponseDto.md +doc/MemoryType.md +doc/MemoryUpdateDto.md doc/MergePersonDto.md doc/MetadataSearchDto.md doc/ModelType.md @@ -97,6 +103,7 @@ doc/OAuthApi.md doc/OAuthAuthorizeResponseDto.md doc/OAuthCallbackDto.md doc/OAuthConfigDto.md +doc/OnThisDayDto.md doc/PartnerApi.md doc/PartnerResponseDto.md doc/PathEntityType.md @@ -145,6 +152,7 @@ doc/SmartSearchDto.md doc/SystemConfigApi.md doc/SystemConfigDto.md doc/SystemConfigFFmpegDto.md +doc/SystemConfigImageDto.md doc/SystemConfigJobDto.md doc/SystemConfigLibraryDto.md doc/SystemConfigLibraryScanDto.md @@ -160,7 +168,6 @@ doc/SystemConfigServerDto.md doc/SystemConfigStorageTemplateDto.md doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigThemeDto.md -doc/SystemConfigThumbnailDto.md doc/SystemConfigTrashDto.md doc/SystemConfigUserDto.md doc/TagApi.md @@ -204,6 +211,7 @@ lib/api/download_api.dart lib/api/face_api.dart lib/api/job_api.dart lib/api/library_api.dart +lib/api/memory_api.dart lib/api/o_auth_api.dart lib/api/partner_api.dart lib/api/person_api.dart @@ -284,6 +292,7 @@ lib/model/file_checksum_response_dto.dart lib/model/file_report_dto.dart lib/model/file_report_fix_dto.dart lib/model/file_report_item_dto.dart +lib/model/image_format.dart lib/model/job_command.dart lib/model/job_command_dto.dart lib/model/job_counts_dto.dart @@ -299,13 +308,18 @@ lib/model/login_response_dto.dart lib/model/logout_response_dto.dart lib/model/map_marker_response_dto.dart lib/model/map_theme.dart +lib/model/memory_create_dto.dart lib/model/memory_lane_response_dto.dart +lib/model/memory_response_dto.dart +lib/model/memory_type.dart +lib/model/memory_update_dto.dart lib/model/merge_person_dto.dart lib/model/metadata_search_dto.dart lib/model/model_type.dart lib/model/o_auth_authorize_response_dto.dart lib/model/o_auth_callback_dto.dart lib/model/o_auth_config_dto.dart +lib/model/on_this_day_dto.dart lib/model/partner_response_dto.dart lib/model/path_entity_type.dart lib/model/path_type.dart @@ -348,6 +362,7 @@ lib/model/smart_info_response_dto.dart lib/model/smart_search_dto.dart lib/model/system_config_dto.dart lib/model/system_config_f_fmpeg_dto.dart +lib/model/system_config_image_dto.dart lib/model/system_config_job_dto.dart lib/model/system_config_library_dto.dart lib/model/system_config_library_scan_dto.dart @@ -363,7 +378,6 @@ lib/model/system_config_server_dto.dart lib/model/system_config_storage_template_dto.dart lib/model/system_config_template_storage_option_dto.dart lib/model/system_config_theme_dto.dart -lib/model/system_config_thumbnail_dto.dart lib/model/system_config_trash_dto.dart lib/model/system_config_user_dto.dart lib/model/tag_response_dto.dart @@ -461,6 +475,7 @@ test/file_checksum_response_dto_test.dart test/file_report_dto_test.dart test/file_report_fix_dto_test.dart test/file_report_item_dto_test.dart +test/image_format_test.dart test/job_api_test.dart test/job_command_dto_test.dart test/job_command_test.dart @@ -478,7 +493,12 @@ test/login_response_dto_test.dart test/logout_response_dto_test.dart test/map_marker_response_dto_test.dart test/map_theme_test.dart +test/memory_api_test.dart +test/memory_create_dto_test.dart test/memory_lane_response_dto_test.dart +test/memory_response_dto_test.dart +test/memory_type_test.dart +test/memory_update_dto_test.dart test/merge_person_dto_test.dart test/metadata_search_dto_test.dart test/model_type_test.dart @@ -486,6 +506,7 @@ test/o_auth_api_test.dart test/o_auth_authorize_response_dto_test.dart test/o_auth_callback_dto_test.dart test/o_auth_config_dto_test.dart +test/on_this_day_dto_test.dart test/partner_api_test.dart test/partner_response_dto_test.dart test/path_entity_type_test.dart @@ -534,6 +555,7 @@ test/smart_search_dto_test.dart test/system_config_api_test.dart test/system_config_dto_test.dart test/system_config_f_fmpeg_dto_test.dart +test/system_config_image_dto_test.dart test/system_config_job_dto_test.dart test/system_config_library_dto_test.dart test/system_config_library_scan_dto_test.dart @@ -549,7 +571,6 @@ test/system_config_server_dto_test.dart test/system_config_storage_template_dto_test.dart test/system_config_template_storage_option_dto_test.dart test/system_config_theme_dto_test.dart -test/system_config_thumbnail_dto_test.dart test/system_config_trash_dto_test.dart test/system_config_user_dto_test.dart test/tag_api_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index bfdac06c4..a64183077 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -140,6 +140,13 @@ Class | Method | HTTP request | Description *LibraryApi* | [**scanLibrary**](doc//LibraryApi.md#scanlibrary) | **POST** /library/{id}/scan | *LibraryApi* | [**updateLibrary**](doc//LibraryApi.md#updatelibrary) | **PUT** /library/{id} | *LibraryApi* | [**validate**](doc//LibraryApi.md#validate) | **POST** /library/{id}/validate | +*MemoryApi* | [**addMemoryAssets**](doc//MemoryApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | +*MemoryApi* | [**createMemory**](doc//MemoryApi.md#creatememory) | **POST** /memories | +*MemoryApi* | [**deleteMemory**](doc//MemoryApi.md#deletememory) | **DELETE** /memories/{id} | +*MemoryApi* | [**getMemory**](doc//MemoryApi.md#getmemory) | **GET** /memories/{id} | +*MemoryApi* | [**removeMemoryAssets**](doc//MemoryApi.md#removememoryassets) | **DELETE** /memories/{id}/assets | +*MemoryApi* | [**searchMemories**](doc//MemoryApi.md#searchmemories) | **GET** /memories | +*MemoryApi* | [**updateMemory**](doc//MemoryApi.md#updatememory) | **PUT** /memories/{id} | *OAuthApi* | [**finishOAuth**](doc//OAuthApi.md#finishoauth) | **POST** /oauth/callback | *OAuthApi* | [**linkOAuthAccount**](doc//OAuthApi.md#linkoauthaccount) | **POST** /oauth/link | *OAuthApi* | [**redirectOAuthToMobile**](doc//OAuthApi.md#redirectoauthtomobile) | **GET** /oauth/mobile-redirect | @@ -277,6 +284,7 @@ Class | Method | HTTP request | Description - [FileReportDto](doc//FileReportDto.md) - [FileReportFixDto](doc//FileReportFixDto.md) - [FileReportItemDto](doc//FileReportItemDto.md) + - [ImageFormat](doc//ImageFormat.md) - [JobCommand](doc//JobCommand.md) - [JobCommandDto](doc//JobCommandDto.md) - [JobCountsDto](doc//JobCountsDto.md) @@ -292,13 +300,18 @@ Class | Method | HTTP request | Description - [LogoutResponseDto](doc//LogoutResponseDto.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapTheme](doc//MapTheme.md) + - [MemoryCreateDto](doc//MemoryCreateDto.md) - [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md) + - [MemoryResponseDto](doc//MemoryResponseDto.md) + - [MemoryType](doc//MemoryType.md) + - [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MergePersonDto](doc//MergePersonDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md) - [ModelType](doc//ModelType.md) - [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md) - [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md) + - [OnThisDayDto](doc//OnThisDayDto.md) - [PartnerResponseDto](doc//PartnerResponseDto.md) - [PathEntityType](doc//PathEntityType.md) - [PathType](doc//PathType.md) @@ -341,6 +354,7 @@ Class | Method | HTTP request | Description - [SmartSearchDto](doc//SmartSearchDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) + - [SystemConfigImageDto](doc//SystemConfigImageDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md) - [SystemConfigLibraryScanDto](doc//SystemConfigLibraryScanDto.md) @@ -356,7 +370,6 @@ Class | Method | HTTP request | Description - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) - [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md) - [SystemConfigThemeDto](doc//SystemConfigThemeDto.md) - - [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md) - [SystemConfigTrashDto](doc//SystemConfigTrashDto.md) - [SystemConfigUserDto](doc//SystemConfigUserDto.md) - [TagResponseDto](doc//TagResponseDto.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 0778485c3..297a4cdba 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -886,7 +886,7 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **searchAssets** -> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) +> List searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, previewPath, resizePath, size, state, takenAfter, takenBefore, thumbnailPath, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked) @@ -936,11 +936,13 @@ final originalFileName = originalFileName_example; // String | final originalPath = originalPath_example; // String | final page = 8.14; // num | final personIds = []; // List | +final previewPath = previewPath_example; // String | final resizePath = resizePath_example; // String | final size = 8.14; // num | final state = state_example; // String | final takenAfter = 2013-10-20T19:20:30+01:00; // DateTime | final takenBefore = 2013-10-20T19:20:30+01:00; // DateTime | +final thumbnailPath = thumbnailPath_example; // String | final trashedAfter = 2013-10-20T19:20:30+01:00; // DateTime | final trashedBefore = 2013-10-20T19:20:30+01:00; // DateTime | final type = ; // AssetTypeEnum | @@ -954,7 +956,7 @@ final withPeople = true; // bool | final withStacked = true; // bool | try { - final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); + final result = api_instance.searchAssets(checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, previewPath, resizePath, size, state, takenAfter, takenBefore, thumbnailPath, trashedAfter, trashedBefore, type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked); print(result); } catch (e) { print('Exception when calling AssetApi->searchAssets: $e\n'); @@ -992,11 +994,13 @@ Name | Type | Description | Notes **originalPath** | **String**| | [optional] **page** | **num**| | [optional] **personIds** | [**List**](String.md)| | [optional] [default to const []] + **previewPath** | **String**| | [optional] **resizePath** | **String**| | [optional] **size** | **num**| | [optional] **state** | **String**| | [optional] **takenAfter** | **DateTime**| | [optional] **takenBefore** | **DateTime**| | [optional] + **thumbnailPath** | **String**| | [optional] **trashedAfter** | **DateTime**| | [optional] **trashedBefore** | **DateTime**| | [optional] **type** | [**AssetTypeEnum**](.md)| | [optional] diff --git a/mobile/openapi/doc/ImageFormat.md b/mobile/openapi/doc/ImageFormat.md new file mode 100644 index 000000000..312e501c1 --- /dev/null +++ b/mobile/openapi/doc/ImageFormat.md @@ -0,0 +1,14 @@ +# openapi.model.ImageFormat + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/MemoryApi.md b/mobile/openapi/doc/MemoryApi.md new file mode 100644 index 000000000..5795669a5 --- /dev/null +++ b/mobile/openapi/doc/MemoryApi.md @@ -0,0 +1,406 @@ +# openapi.api.MemoryApi + +## Load the API package +```dart +import 'package:openapi/api.dart'; +``` + +All URIs are relative to */api* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**addMemoryAssets**](MemoryApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | +[**createMemory**](MemoryApi.md#creatememory) | **POST** /memories | +[**deleteMemory**](MemoryApi.md#deletememory) | **DELETE** /memories/{id} | +[**getMemory**](MemoryApi.md#getmemory) | **GET** /memories/{id} | +[**removeMemoryAssets**](MemoryApi.md#removememoryassets) | **DELETE** /memories/{id}/assets | +[**searchMemories**](MemoryApi.md#searchmemories) | **GET** /memories | +[**updateMemory**](MemoryApi.md#updatememory) | **PUT** /memories/{id} | + + +# **addMemoryAssets** +> List addMemoryAssets(id, bulkIdsDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final bulkIdsDto = BulkIdsDto(); // BulkIdsDto | + +try { + final result = api_instance.addMemoryAssets(id, bulkIdsDto); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->addMemoryAssets: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)| | + +### Return type + +[**List**](BulkIdResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **createMemory** +> MemoryResponseDto createMemory(memoryCreateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final memoryCreateDto = MemoryCreateDto(); // MemoryCreateDto | + +try { + final result = api_instance.createMemory(memoryCreateDto); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->createMemory: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **memoryCreateDto** | [**MemoryCreateDto**](MemoryCreateDto.md)| | + +### Return type + +[**MemoryResponseDto**](MemoryResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **deleteMemory** +> deleteMemory(id) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | + +try { + api_instance.deleteMemory(id); +} catch (e) { + print('Exception when calling MemoryApi->deleteMemory: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + +### Return type + +void (empty response body) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **getMemory** +> MemoryResponseDto getMemory(id) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | + +try { + final result = api_instance.getMemory(id); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->getMemory: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + +### Return type + +[**MemoryResponseDto**](MemoryResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **removeMemoryAssets** +> List removeMemoryAssets(id, bulkIdsDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final bulkIdsDto = BulkIdsDto(); // BulkIdsDto | + +try { + final result = api_instance.removeMemoryAssets(id, bulkIdsDto); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->removeMemoryAssets: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **bulkIdsDto** | [**BulkIdsDto**](BulkIdsDto.md)| | + +### Return type + +[**List**](BulkIdResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **searchMemories** +> List searchMemories() + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); + +try { + final result = api_instance.searchMemories(); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->searchMemories: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**List**](MemoryResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **updateMemory** +> MemoryResponseDto updateMemory(id, memoryUpdateDto) + + + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = MemoryApi(); +final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final memoryUpdateDto = MemoryUpdateDto(); // MemoryUpdateDto | + +try { + final result = api_instance.updateMemory(id, memoryUpdateDto); + print(result); +} catch (e) { + print('Exception when calling MemoryApi->updateMemory: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **id** | **String**| | + **memoryUpdateDto** | [**MemoryUpdateDto**](MemoryUpdateDto.md)| | + +### Return type + +[**MemoryResponseDto**](MemoryResponseDto.md) + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/mobile/openapi/doc/MemoryCreateDto.md b/mobile/openapi/doc/MemoryCreateDto.md new file mode 100644 index 000000000..e0b0ef5a5 --- /dev/null +++ b/mobile/openapi/doc/MemoryCreateDto.md @@ -0,0 +1,20 @@ +# openapi.model.MemoryCreateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**assetIds** | **List** | | [optional] [default to const []] +**data** | [**OnThisDayDto**](OnThisDayDto.md) | | +**isSaved** | **bool** | | [optional] +**memoryAt** | [**DateTime**](DateTime.md) | | +**seenAt** | [**DateTime**](DateTime.md) | | [optional] +**type** | [**MemoryType**](MemoryType.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) + + diff --git a/mobile/openapi/doc/MemoryResponseDto.md b/mobile/openapi/doc/MemoryResponseDto.md new file mode 100644 index 000000000..59a836311 --- /dev/null +++ b/mobile/openapi/doc/MemoryResponseDto.md @@ -0,0 +1,25 @@ +# openapi.model.MemoryResponseDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**assets** | [**List**](AssetResponseDto.md) | | [default to const []] +**createdAt** | [**DateTime**](DateTime.md) | | +**data** | [**OnThisDayDto**](OnThisDayDto.md) | | +**deletedAt** | [**DateTime**](DateTime.md) | | [optional] +**id** | **String** | | +**isSaved** | **bool** | | +**memoryAt** | [**DateTime**](DateTime.md) | | +**ownerId** | **String** | | +**seenAt** | [**DateTime**](DateTime.md) | | [optional] +**type** | **String** | | +**updatedAt** | [**DateTime**](DateTime.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) + + diff --git a/mobile/openapi/doc/MemoryType.md b/mobile/openapi/doc/MemoryType.md new file mode 100644 index 000000000..c8dea25be --- /dev/null +++ b/mobile/openapi/doc/MemoryType.md @@ -0,0 +1,14 @@ +# openapi.model.MemoryType + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/MemoryUpdateDto.md b/mobile/openapi/doc/MemoryUpdateDto.md new file mode 100644 index 000000000..7a48e84e8 --- /dev/null +++ b/mobile/openapi/doc/MemoryUpdateDto.md @@ -0,0 +1,17 @@ +# openapi.model.MemoryUpdateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**isSaved** | **bool** | | [optional] +**memoryAt** | [**DateTime**](DateTime.md) | | [optional] +**seenAt** | [**DateTime**](DateTime.md) | | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/MetadataSearchDto.md b/mobile/openapi/doc/MetadataSearchDto.md index d1d098fb0..5dc50c00f 100644 --- a/mobile/openapi/doc/MetadataSearchDto.md +++ b/mobile/openapi/doc/MetadataSearchDto.md @@ -35,11 +35,13 @@ Name | Type | Description | Notes **originalPath** | **String** | | [optional] **page** | **num** | | [optional] **personIds** | **List** | | [optional] [default to const []] +**previewPath** | **String** | | [optional] **resizePath** | **String** | | [optional] **size** | **num** | | [optional] **state** | **String** | | [optional] **takenAfter** | [**DateTime**](DateTime.md) | | [optional] **takenBefore** | [**DateTime**](DateTime.md) | | [optional] +**thumbnailPath** | **String** | | [optional] **trashedAfter** | [**DateTime**](DateTime.md) | | [optional] **trashedBefore** | [**DateTime**](DateTime.md) | | [optional] **type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | [optional] diff --git a/mobile/openapi/doc/OnThisDayDto.md b/mobile/openapi/doc/OnThisDayDto.md new file mode 100644 index 000000000..178b4482b --- /dev/null +++ b/mobile/openapi/doc/OnThisDayDto.md @@ -0,0 +1,15 @@ +# openapi.model.OnThisDayDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**year** | **num** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index ad1afbe9f..fc3d3dd0b 100644 --- a/mobile/openapi/doc/SystemConfigDto.md +++ b/mobile/openapi/doc/SystemConfigDto.md @@ -9,6 +9,7 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **ffmpeg** | [**SystemConfigFFmpegDto**](SystemConfigFFmpegDto.md) | | +**image** | [**SystemConfigImageDto**](SystemConfigImageDto.md) | | **job** | [**SystemConfigJobDto**](SystemConfigJobDto.md) | | **library_** | [**SystemConfigLibraryDto**](SystemConfigLibraryDto.md) | | **logging** | [**SystemConfigLoggingDto**](SystemConfigLoggingDto.md) | | @@ -21,7 +22,6 @@ Name | Type | Description | Notes **server** | [**SystemConfigServerDto**](SystemConfigServerDto.md) | | **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | | **theme** | [**SystemConfigThemeDto**](SystemConfigThemeDto.md) | | -**thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) | | **trash** | [**SystemConfigTrashDto**](SystemConfigTrashDto.md) | | **user** | [**SystemConfigUserDto**](SystemConfigUserDto.md) | | diff --git a/mobile/openapi/doc/SystemConfigThumbnailDto.md b/mobile/openapi/doc/SystemConfigImageDto.md similarity index 65% rename from mobile/openapi/doc/SystemConfigThumbnailDto.md rename to mobile/openapi/doc/SystemConfigImageDto.md index 491bf9f12..1b9bbe726 100644 --- a/mobile/openapi/doc/SystemConfigThumbnailDto.md +++ b/mobile/openapi/doc/SystemConfigImageDto.md @@ -1,4 +1,4 @@ -# openapi.model.SystemConfigThumbnailDto +# openapi.model.SystemConfigImageDto ## Load the model package ```dart @@ -9,9 +9,11 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **colorspace** | [**Colorspace**](Colorspace.md) | | -**jpegSize** | **int** | | +**previewFormat** | [**ImageFormat**](ImageFormat.md) | | +**previewSize** | **int** | | **quality** | **int** | | -**webpSize** | **int** | | +**thumbnailFormat** | [**ImageFormat**](ImageFormat.md) | | +**thumbnailSize** | **int** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 2abc20fde..ae458f5de 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -39,6 +39,7 @@ part 'api/download_api.dart'; part 'api/face_api.dart'; part 'api/job_api.dart'; part 'api/library_api.dart'; +part 'api/memory_api.dart'; part 'api/o_auth_api.dart'; part 'api/partner_api.dart'; part 'api/person_api.dart'; @@ -112,6 +113,7 @@ part 'model/file_checksum_response_dto.dart'; part 'model/file_report_dto.dart'; part 'model/file_report_fix_dto.dart'; part 'model/file_report_item_dto.dart'; +part 'model/image_format.dart'; part 'model/job_command.dart'; part 'model/job_command_dto.dart'; part 'model/job_counts_dto.dart'; @@ -127,13 +129,18 @@ part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; part 'model/map_marker_response_dto.dart'; part 'model/map_theme.dart'; +part 'model/memory_create_dto.dart'; part 'model/memory_lane_response_dto.dart'; +part 'model/memory_response_dto.dart'; +part 'model/memory_type.dart'; +part 'model/memory_update_dto.dart'; part 'model/merge_person_dto.dart'; part 'model/metadata_search_dto.dart'; part 'model/model_type.dart'; part 'model/o_auth_authorize_response_dto.dart'; part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_config_dto.dart'; +part 'model/on_this_day_dto.dart'; part 'model/partner_response_dto.dart'; part 'model/path_entity_type.dart'; part 'model/path_type.dart'; @@ -176,6 +183,7 @@ part 'model/smart_info_response_dto.dart'; part 'model/smart_search_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; +part 'model/system_config_image_dto.dart'; part 'model/system_config_job_dto.dart'; part 'model/system_config_library_dto.dart'; part 'model/system_config_library_scan_dto.dart'; @@ -191,7 +199,6 @@ part 'model/system_config_server_dto.dart'; part 'model/system_config_storage_template_dto.dart'; part 'model/system_config_template_storage_option_dto.dart'; part 'model/system_config_theme_dto.dart'; -part 'model/system_config_thumbnail_dto.dart'; part 'model/system_config_trash_dto.dart'; part 'model/system_config_user_dto.dart'; part 'model/tag_response_dto.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index e16ccc73e..10f1c0c10 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -931,6 +931,8 @@ class AssetApi { /// /// * [List] personIds: /// + /// * [String] previewPath: + /// /// * [String] resizePath: /// /// * [num] size: @@ -941,6 +943,8 @@ class AssetApi { /// /// * [DateTime] takenBefore: /// + /// * [String] thumbnailPath: + /// /// * [DateTime] trashedAfter: /// /// * [DateTime] trashedBefore: @@ -962,7 +966,7 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + Future searchAssetsWithHttpInfo({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? previewPath, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, String? thumbnailPath, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/assets'; @@ -1054,6 +1058,9 @@ class AssetApi { if (personIds != null) { queryParams.addAll(_queryParams('multi', 'personIds', personIds)); } + if (previewPath != null) { + queryParams.addAll(_queryParams('', 'previewPath', previewPath)); + } if (resizePath != null) { queryParams.addAll(_queryParams('', 'resizePath', resizePath)); } @@ -1069,6 +1076,9 @@ class AssetApi { if (takenBefore != null) { queryParams.addAll(_queryParams('', 'takenBefore', takenBefore)); } + if (thumbnailPath != null) { + queryParams.addAll(_queryParams('', 'thumbnailPath', thumbnailPath)); + } if (trashedAfter != null) { queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter)); } @@ -1173,6 +1183,8 @@ class AssetApi { /// /// * [List] personIds: /// + /// * [String] previewPath: + /// /// * [String] resizePath: /// /// * [num] size: @@ -1183,6 +1195,8 @@ class AssetApi { /// /// * [DateTime] takenBefore: /// + /// * [String] thumbnailPath: + /// /// * [DateTime] trashedAfter: /// /// * [DateTime] trashedBefore: @@ -1204,8 +1218,8 @@ class AssetApi { /// * [bool] withPeople: /// /// * [bool] withStacked: - Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { - final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, personIds: personIds, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); + Future?> searchAssets({ String? checksum, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceAssetId, String? deviceId, String? encodedVideoPath, String? id, bool? isArchived, bool? isEncoded, bool? isExternal, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, bool? isReadOnly, bool? isVisible, String? lensModel, String? libraryId, String? make, String? model, AssetOrder? order, String? originalFileName, String? originalPath, num? page, List? personIds, String? previewPath, String? resizePath, num? size, String? state, DateTime? takenAfter, DateTime? takenBefore, String? thumbnailPath, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, String? webpPath, bool? withArchived, bool? withDeleted, bool? withExif, bool? withPeople, bool? withStacked, }) async { + final response = await searchAssetsWithHttpInfo( checksum: checksum, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceAssetId: deviceAssetId, deviceId: deviceId, encodedVideoPath: encodedVideoPath, id: id, isArchived: isArchived, isEncoded: isEncoded, isExternal: isExternal, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, isReadOnly: isReadOnly, isVisible: isVisible, lensModel: lensModel, libraryId: libraryId, make: make, model: model, order: order, originalFileName: originalFileName, originalPath: originalPath, page: page, personIds: personIds, previewPath: previewPath, resizePath: resizePath, size: size, state: state, takenAfter: takenAfter, takenBefore: takenBefore, thumbnailPath: thumbnailPath, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, webpPath: webpPath, withArchived: withArchived, withDeleted: withDeleted, withExif: withExif, withPeople: withPeople, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/memory_api.dart b/mobile/openapi/lib/api/memory_api.dart new file mode 100644 index 000000000..6b4a619b5 --- /dev/null +++ b/mobile/openapi/lib/api/memory_api.dart @@ -0,0 +1,359 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class MemoryApi { + MemoryApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'PUT /memories/{id}/assets' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future addMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}/assets' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = bulkIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future?> addMemoryAssets(String id, BulkIdsDto bulkIdsDto,) async { + final response = await addMemoryAssetsWithHttpInfo(id, bulkIdsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'POST /memories' operation and returns the [Response]. + /// Parameters: + /// + /// * [MemoryCreateDto] memoryCreateDto (required): + Future createMemoryWithHttpInfo(MemoryCreateDto memoryCreateDto,) async { + // ignore: prefer_const_declarations + final path = r'/memories'; + + // ignore: prefer_final_locals + Object? postBody = memoryCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [MemoryCreateDto] memoryCreateDto (required): + Future createMemory(MemoryCreateDto memoryCreateDto,) async { + final response = await createMemoryWithHttpInfo(memoryCreateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemoryResponseDto',) as MemoryResponseDto; + + } + return null; + } + + /// Performs an HTTP 'DELETE /memories/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future deleteMemoryWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future deleteMemory(String id,) async { + final response = await deleteMemoryWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /memories/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future getMemoryWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future getMemory(String id,) async { + final response = await getMemoryWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemoryResponseDto',) as MemoryResponseDto; + + } + return null; + } + + /// Performs an HTTP 'DELETE /memories/{id}/assets' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future removeMemoryAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}/assets' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = bulkIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future?> removeMemoryAssets(String id, BulkIdsDto bulkIdsDto,) async { + final response = await removeMemoryAssetsWithHttpInfo(id, bulkIdsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'GET /memories' operation and returns the [Response]. + Future searchMemoriesWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/memories'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future?> searchMemories() async { + final response = await searchMemoriesWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'PUT /memories/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [MemoryUpdateDto] memoryUpdateDto (required): + Future updateMemoryWithHttpInfo(String id, MemoryUpdateDto memoryUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/memories/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = memoryUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [MemoryUpdateDto] memoryUpdateDto (required): + Future updateMemory(String id, MemoryUpdateDto memoryUpdateDto,) async { + final response = await updateMemoryWithHttpInfo(id, memoryUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MemoryResponseDto',) as MemoryResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 312153788..757f47568 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -304,6 +304,8 @@ class ApiClient { return FileReportFixDto.fromJson(value); case 'FileReportItemDto': return FileReportItemDto.fromJson(value); + case 'ImageFormat': + return ImageFormatTypeTransformer().decode(value); case 'JobCommand': return JobCommandTypeTransformer().decode(value); case 'JobCommandDto': @@ -334,8 +336,16 @@ class ApiClient { return MapMarkerResponseDto.fromJson(value); case 'MapTheme': return MapThemeTypeTransformer().decode(value); + case 'MemoryCreateDto': + return MemoryCreateDto.fromJson(value); case 'MemoryLaneResponseDto': return MemoryLaneResponseDto.fromJson(value); + case 'MemoryResponseDto': + return MemoryResponseDto.fromJson(value); + case 'MemoryType': + return MemoryTypeTypeTransformer().decode(value); + case 'MemoryUpdateDto': + return MemoryUpdateDto.fromJson(value); case 'MergePersonDto': return MergePersonDto.fromJson(value); case 'MetadataSearchDto': @@ -348,6 +358,8 @@ class ApiClient { return OAuthCallbackDto.fromJson(value); case 'OAuthConfigDto': return OAuthConfigDto.fromJson(value); + case 'OnThisDayDto': + return OnThisDayDto.fromJson(value); case 'PartnerResponseDto': return PartnerResponseDto.fromJson(value); case 'PathEntityType': @@ -432,6 +444,8 @@ class ApiClient { return SystemConfigDto.fromJson(value); case 'SystemConfigFFmpegDto': return SystemConfigFFmpegDto.fromJson(value); + case 'SystemConfigImageDto': + return SystemConfigImageDto.fromJson(value); case 'SystemConfigJobDto': return SystemConfigJobDto.fromJson(value); case 'SystemConfigLibraryDto': @@ -462,8 +476,6 @@ class ApiClient { return SystemConfigTemplateStorageOptionDto.fromJson(value); case 'SystemConfigThemeDto': return SystemConfigThemeDto.fromJson(value); - case 'SystemConfigThumbnailDto': - return SystemConfigThumbnailDto.fromJson(value); case 'SystemConfigTrashDto': return SystemConfigTrashDto.fromJson(value); case 'SystemConfigUserDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index d186845d9..7ad74d951 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -79,6 +79,9 @@ String parameterToString(dynamic value) { if (value is EntityType) { return EntityTypeTypeTransformer().encode(value).toString(); } + if (value is ImageFormat) { + return ImageFormatTypeTransformer().encode(value).toString(); + } if (value is JobCommand) { return JobCommandTypeTransformer().encode(value).toString(); } @@ -94,6 +97,9 @@ String parameterToString(dynamic value) { if (value is MapTheme) { return MapThemeTypeTransformer().encode(value).toString(); } + if (value is MemoryType) { + return MemoryTypeTypeTransformer().encode(value).toString(); + } if (value is ModelType) { return ModelTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/image_format.dart b/mobile/openapi/lib/model/image_format.dart new file mode 100644 index 000000000..570b6ca6e --- /dev/null +++ b/mobile/openapi/lib/model/image_format.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class ImageFormat { + /// Instantiate a new enum with the provided [value]. + const ImageFormat._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const jpeg = ImageFormat._(r'jpeg'); + static const webp = ImageFormat._(r'webp'); + + /// List of all possible values in this [enum][ImageFormat]. + static const values = [ + jpeg, + webp, + ]; + + static ImageFormat? fromJson(dynamic value) => ImageFormatTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ImageFormat.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ImageFormat] to String, +/// and [decode] dynamic data back to [ImageFormat]. +class ImageFormatTypeTransformer { + factory ImageFormatTypeTransformer() => _instance ??= const ImageFormatTypeTransformer._(); + + const ImageFormatTypeTransformer._(); + + String encode(ImageFormat data) => data.value; + + /// Decodes a [dynamic value][data] to a ImageFormat. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ImageFormat? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'jpeg': return ImageFormat.jpeg; + case r'webp': return ImageFormat.webp; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ImageFormatTypeTransformer] instance. + static ImageFormatTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart new file mode 100644 index 000000000..c48a07c5f --- /dev/null +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -0,0 +1,157 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MemoryCreateDto { + /// Returns a new [MemoryCreateDto] instance. + MemoryCreateDto({ + this.assetIds = const [], + required this.data, + this.isSaved, + required this.memoryAt, + this.seenAt, + required this.type, + }); + + List assetIds; + + OnThisDayDto data; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isSaved; + + DateTime memoryAt; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? seenAt; + + MemoryType type; + + @override + bool operator ==(Object other) => identical(this, other) || other is MemoryCreateDto && + _deepEquality.equals(other.assetIds, assetIds) && + other.data == data && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.seenAt == seenAt && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetIds.hashCode) + + (data.hashCode) + + (isSaved == null ? 0 : isSaved!.hashCode) + + (memoryAt.hashCode) + + (seenAt == null ? 0 : seenAt!.hashCode) + + (type.hashCode); + + @override + String toString() => 'MemoryCreateDto[assetIds=$assetIds, data=$data, isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt, type=$type]'; + + Map toJson() { + final json = {}; + json[r'assetIds'] = this.assetIds; + json[r'data'] = this.data; + if (this.isSaved != null) { + json[r'isSaved'] = this.isSaved; + } else { + // json[r'isSaved'] = null; + } + json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + if (this.seenAt != null) { + json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + } else { + // json[r'seenAt'] = null; + } + json[r'type'] = this.type; + return json; + } + + /// Returns a new [MemoryCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MemoryCreateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MemoryCreateDto( + assetIds: json[r'assetIds'] is Iterable + ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) + : const [], + data: OnThisDayDto.fromJson(json[r'data'])!, + isSaved: mapValueOfType(json, r'isSaved'), + memoryAt: mapDateTime(json, r'memoryAt', r'')!, + seenAt: mapDateTime(json, r'seenAt', r''), + type: MemoryType.fromJson(json[r'type'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MemoryCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MemoryCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MemoryCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'data', + 'memoryAt', + 'type', + }; +} + diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart new file mode 100644 index 000000000..8671e3c64 --- /dev/null +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -0,0 +1,267 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MemoryResponseDto { + /// Returns a new [MemoryResponseDto] instance. + MemoryResponseDto({ + this.assets = const [], + required this.createdAt, + required this.data, + this.deletedAt, + required this.id, + required this.isSaved, + required this.memoryAt, + required this.ownerId, + this.seenAt, + required this.type, + required this.updatedAt, + }); + + List assets; + + DateTime createdAt; + + OnThisDayDto data; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? deletedAt; + + String id; + + bool isSaved; + + DateTime memoryAt; + + String ownerId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? seenAt; + + MemoryResponseDtoTypeEnum type; + + DateTime updatedAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is MemoryResponseDto && + _deepEquality.equals(other.assets, assets) && + other.createdAt == createdAt && + other.data == data && + other.deletedAt == deletedAt && + other.id == id && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.ownerId == ownerId && + other.seenAt == seenAt && + other.type == type && + other.updatedAt == updatedAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assets.hashCode) + + (createdAt.hashCode) + + (data.hashCode) + + (deletedAt == null ? 0 : deletedAt!.hashCode) + + (id.hashCode) + + (isSaved.hashCode) + + (memoryAt.hashCode) + + (ownerId.hashCode) + + (seenAt == null ? 0 : seenAt!.hashCode) + + (type.hashCode) + + (updatedAt.hashCode); + + @override + String toString() => 'MemoryResponseDto[assets=$assets, createdAt=$createdAt, data=$data, deletedAt=$deletedAt, id=$id, isSaved=$isSaved, memoryAt=$memoryAt, ownerId=$ownerId, seenAt=$seenAt, type=$type, updatedAt=$updatedAt]'; + + Map toJson() { + final json = {}; + json[r'assets'] = this.assets; + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'data'] = this.data; + if (this.deletedAt != null) { + json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + } else { + // json[r'deletedAt'] = null; + } + json[r'id'] = this.id; + json[r'isSaved'] = this.isSaved; + json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'ownerId'] = this.ownerId; + if (this.seenAt != null) { + json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + } else { + // json[r'seenAt'] = null; + } + json[r'type'] = this.type; + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + return json; + } + + /// Returns a new [MemoryResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MemoryResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MemoryResponseDto( + assets: AssetResponseDto.listFromJson(json[r'assets']), + createdAt: mapDateTime(json, r'createdAt', r'')!, + data: OnThisDayDto.fromJson(json[r'data'])!, + deletedAt: mapDateTime(json, r'deletedAt', r''), + id: mapValueOfType(json, r'id')!, + isSaved: mapValueOfType(json, r'isSaved')!, + memoryAt: mapDateTime(json, r'memoryAt', r'')!, + ownerId: mapValueOfType(json, r'ownerId')!, + seenAt: mapDateTime(json, r'seenAt', r''), + type: MemoryResponseDtoTypeEnum.fromJson(json[r'type'])!, + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MemoryResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MemoryResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MemoryResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assets', + 'createdAt', + 'data', + 'id', + 'isSaved', + 'memoryAt', + 'ownerId', + 'type', + 'updatedAt', + }; +} + + +class MemoryResponseDtoTypeEnum { + /// Instantiate a new enum with the provided [value]. + const MemoryResponseDtoTypeEnum._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const onThisDay = MemoryResponseDtoTypeEnum._(r'on_this_day'); + + /// List of all possible values in this [enum][MemoryResponseDtoTypeEnum]. + static const values = [ + onThisDay, + ]; + + static MemoryResponseDtoTypeEnum? fromJson(dynamic value) => MemoryResponseDtoTypeEnumTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryResponseDtoTypeEnum.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MemoryResponseDtoTypeEnum] to String, +/// and [decode] dynamic data back to [MemoryResponseDtoTypeEnum]. +class MemoryResponseDtoTypeEnumTypeTransformer { + factory MemoryResponseDtoTypeEnumTypeTransformer() => _instance ??= const MemoryResponseDtoTypeEnumTypeTransformer._(); + + const MemoryResponseDtoTypeEnumTypeTransformer._(); + + String encode(MemoryResponseDtoTypeEnum data) => data.value; + + /// Decodes a [dynamic value][data] to a MemoryResponseDtoTypeEnum. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + MemoryResponseDtoTypeEnum? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'on_this_day': return MemoryResponseDtoTypeEnum.onThisDay; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MemoryResponseDtoTypeEnumTypeTransformer] instance. + static MemoryResponseDtoTypeEnumTypeTransformer? _instance; +} + + diff --git a/mobile/openapi/lib/model/memory_type.dart b/mobile/openapi/lib/model/memory_type.dart new file mode 100644 index 000000000..513b7c2d4 --- /dev/null +++ b/mobile/openapi/lib/model/memory_type.dart @@ -0,0 +1,82 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class MemoryType { + /// Instantiate a new enum with the provided [value]. + const MemoryType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const onThisDay = MemoryType._(r'on_this_day'); + + /// List of all possible values in this [enum][MemoryType]. + static const values = [ + onThisDay, + ]; + + static MemoryType? fromJson(dynamic value) => MemoryTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [MemoryType] to String, +/// and [decode] dynamic data back to [MemoryType]. +class MemoryTypeTypeTransformer { + factory MemoryTypeTypeTransformer() => _instance ??= const MemoryTypeTypeTransformer._(); + + const MemoryTypeTypeTransformer._(); + + String encode(MemoryType data) => data.value; + + /// Decodes a [dynamic value][data] to a MemoryType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + MemoryType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'on_this_day': return MemoryType.onThisDay; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [MemoryTypeTypeTransformer] instance. + static MemoryTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart new file mode 100644 index 000000000..adf42330d --- /dev/null +++ b/mobile/openapi/lib/model/memory_update_dto.dart @@ -0,0 +1,141 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class MemoryUpdateDto { + /// Returns a new [MemoryUpdateDto] instance. + MemoryUpdateDto({ + this.isSaved, + this.memoryAt, + this.seenAt, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isSaved; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? memoryAt; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? seenAt; + + @override + bool operator ==(Object other) => identical(this, other) || other is MemoryUpdateDto && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.seenAt == seenAt; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (isSaved == null ? 0 : isSaved!.hashCode) + + (memoryAt == null ? 0 : memoryAt!.hashCode) + + (seenAt == null ? 0 : seenAt!.hashCode); + + @override + String toString() => 'MemoryUpdateDto[isSaved=$isSaved, memoryAt=$memoryAt, seenAt=$seenAt]'; + + Map toJson() { + final json = {}; + if (this.isSaved != null) { + json[r'isSaved'] = this.isSaved; + } else { + // json[r'isSaved'] = null; + } + if (this.memoryAt != null) { + json[r'memoryAt'] = this.memoryAt!.toUtc().toIso8601String(); + } else { + // json[r'memoryAt'] = null; + } + if (this.seenAt != null) { + json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + } else { + // json[r'seenAt'] = null; + } + return json; + } + + /// Returns a new [MemoryUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static MemoryUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return MemoryUpdateDto( + isSaved: mapValueOfType(json, r'isSaved'), + memoryAt: mapDateTime(json, r'memoryAt', r''), + seenAt: mapDateTime(json, r'seenAt', r''), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = MemoryUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = MemoryUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of MemoryUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = MemoryUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 86a2856e6..3f770ed09 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -40,11 +40,13 @@ class MetadataSearchDto { this.originalPath, this.page, this.personIds = const [], + this.previewPath, this.resizePath, this.size, this.state, this.takenAfter, this.takenBefore, + this.thumbnailPath, this.trashedAfter, this.trashedBefore, this.type, @@ -268,6 +270,14 @@ class MetadataSearchDto { List personIds; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? previewPath; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -308,6 +318,14 @@ class MetadataSearchDto { /// DateTime? takenBefore; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? thumbnailPath; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -419,11 +437,13 @@ class MetadataSearchDto { other.originalPath == originalPath && other.page == page && _deepEquality.equals(other.personIds, personIds) && + other.previewPath == previewPath && other.resizePath == resizePath && other.size == size && other.state == state && other.takenAfter == takenAfter && other.takenBefore == takenBefore && + other.thumbnailPath == thumbnailPath && other.trashedAfter == trashedAfter && other.trashedBefore == trashedBefore && other.type == type && @@ -466,11 +486,13 @@ class MetadataSearchDto { (originalPath == null ? 0 : originalPath!.hashCode) + (page == null ? 0 : page!.hashCode) + (personIds.hashCode) + + (previewPath == null ? 0 : previewPath!.hashCode) + (resizePath == null ? 0 : resizePath!.hashCode) + (size == null ? 0 : size!.hashCode) + (state == null ? 0 : state!.hashCode) + (takenAfter == null ? 0 : takenAfter!.hashCode) + (takenBefore == null ? 0 : takenBefore!.hashCode) + + (thumbnailPath == null ? 0 : thumbnailPath!.hashCode) + (trashedAfter == null ? 0 : trashedAfter!.hashCode) + (trashedBefore == null ? 0 : trashedBefore!.hashCode) + (type == null ? 0 : type!.hashCode) + @@ -484,7 +506,7 @@ class MetadataSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, resizePath=$resizePath, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, webpPath=$webpPath, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, resizePath=$resizePath, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, webpPath=$webpPath, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; @@ -619,6 +641,11 @@ class MetadataSearchDto { // json[r'page'] = null; } json[r'personIds'] = this.personIds; + if (this.previewPath != null) { + json[r'previewPath'] = this.previewPath; + } else { + // json[r'previewPath'] = null; + } if (this.resizePath != null) { json[r'resizePath'] = this.resizePath; } else { @@ -644,6 +671,11 @@ class MetadataSearchDto { } else { // json[r'takenBefore'] = null; } + if (this.thumbnailPath != null) { + json[r'thumbnailPath'] = this.thumbnailPath; + } else { + // json[r'thumbnailPath'] = null; + } if (this.trashedAfter != null) { json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); } else { @@ -735,11 +767,13 @@ class MetadataSearchDto { personIds: json[r'personIds'] is Iterable ? (json[r'personIds'] as Iterable).cast().toList(growable: false) : const [], + previewPath: mapValueOfType(json, r'previewPath'), resizePath: mapValueOfType(json, r'resizePath'), size: num.parse('${json[r'size']}'), state: mapValueOfType(json, r'state'), takenAfter: mapDateTime(json, r'takenAfter', r''), takenBefore: mapDateTime(json, r'takenBefore', r''), + thumbnailPath: mapValueOfType(json, r'thumbnailPath'), trashedAfter: mapDateTime(json, r'trashedAfter', r''), trashedBefore: mapDateTime(json, r'trashedBefore', r''), type: AssetTypeEnum.fromJson(json[r'type']), diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart new file mode 100644 index 000000000..ef1471dfb --- /dev/null +++ b/mobile/openapi/lib/model/on_this_day_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class OnThisDayDto { + /// Returns a new [OnThisDayDto] instance. + OnThisDayDto({ + required this.year, + }); + + num year; + + @override + bool operator ==(Object other) => identical(this, other) || other is OnThisDayDto && + other.year == year; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (year.hashCode); + + @override + String toString() => 'OnThisDayDto[year=$year]'; + + Map toJson() { + final json = {}; + json[r'year'] = this.year; + return json; + } + + /// Returns a new [OnThisDayDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static OnThisDayDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return OnThisDayDto( + year: num.parse('${json[r'year']}'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = OnThisDayDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = OnThisDayDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of OnThisDayDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = OnThisDayDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'year', + }; +} + diff --git a/mobile/openapi/lib/model/path_type.dart b/mobile/openapi/lib/model/path_type.dart index ea722076d..11cdf41ea 100644 --- a/mobile/openapi/lib/model/path_type.dart +++ b/mobile/openapi/lib/model/path_type.dart @@ -24,8 +24,8 @@ class PathType { String toJson() => value; static const original = PathType._(r'original'); - static const jpegThumbnail = PathType._(r'jpeg_thumbnail'); - static const webpThumbnail = PathType._(r'webp_thumbnail'); + static const preview = PathType._(r'preview'); + static const thumbnail = PathType._(r'thumbnail'); static const encodedVideo = PathType._(r'encoded_video'); static const sidecar = PathType._(r'sidecar'); static const face = PathType._(r'face'); @@ -34,8 +34,8 @@ class PathType { /// List of all possible values in this [enum][PathType]. static const values = [ original, - jpegThumbnail, - webpThumbnail, + preview, + thumbnail, encodedVideo, sidecar, face, @@ -79,8 +79,8 @@ class PathTypeTypeTransformer { if (data != null) { switch (data) { case r'original': return PathType.original; - case r'jpeg_thumbnail': return PathType.jpegThumbnail; - case r'webp_thumbnail': return PathType.webpThumbnail; + case r'preview': return PathType.preview; + case r'thumbnail': return PathType.thumbnail; case r'encoded_video': return PathType.encodedVideo; case r'sidecar': return PathType.sidecar; case r'face': return PathType.face; diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 0b5f64fc2..f075d37c8 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -14,6 +14,7 @@ class SystemConfigDto { /// Returns a new [SystemConfigDto] instance. SystemConfigDto({ required this.ffmpeg, + required this.image, required this.job, required this.library_, required this.logging, @@ -26,13 +27,14 @@ class SystemConfigDto { required this.server, required this.storageTemplate, required this.theme, - required this.thumbnail, required this.trash, required this.user, }); SystemConfigFFmpegDto ffmpeg; + SystemConfigImageDto image; + SystemConfigJobDto job; SystemConfigLibraryDto library_; @@ -57,8 +59,6 @@ class SystemConfigDto { SystemConfigThemeDto theme; - SystemConfigThumbnailDto thumbnail; - SystemConfigTrashDto trash; SystemConfigUserDto user; @@ -66,6 +66,7 @@ class SystemConfigDto { @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && other.ffmpeg == ffmpeg && + other.image == image && other.job == job && other.library_ == library_ && other.logging == logging && @@ -78,7 +79,6 @@ class SystemConfigDto { other.server == server && other.storageTemplate == storageTemplate && other.theme == theme && - other.thumbnail == thumbnail && other.trash == trash && other.user == user; @@ -86,6 +86,7 @@ class SystemConfigDto { int get hashCode => // ignore: unnecessary_parenthesis (ffmpeg.hashCode) + + (image.hashCode) + (job.hashCode) + (library_.hashCode) + (logging.hashCode) + @@ -98,16 +99,16 @@ class SystemConfigDto { (server.hashCode) + (storageTemplate.hashCode) + (theme.hashCode) + - (thumbnail.hashCode) + (trash.hashCode) + (user.hashCode); @override - String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash, user=$user]'; + String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]'; Map toJson() { final json = {}; json[r'ffmpeg'] = this.ffmpeg; + json[r'image'] = this.image; json[r'job'] = this.job; json[r'library'] = this.library_; json[r'logging'] = this.logging; @@ -120,7 +121,6 @@ class SystemConfigDto { json[r'server'] = this.server; json[r'storageTemplate'] = this.storageTemplate; json[r'theme'] = this.theme; - json[r'thumbnail'] = this.thumbnail; json[r'trash'] = this.trash; json[r'user'] = this.user; return json; @@ -135,6 +135,7 @@ class SystemConfigDto { return SystemConfigDto( ffmpeg: SystemConfigFFmpegDto.fromJson(json[r'ffmpeg'])!, + image: SystemConfigImageDto.fromJson(json[r'image'])!, job: SystemConfigJobDto.fromJson(json[r'job'])!, library_: SystemConfigLibraryDto.fromJson(json[r'library'])!, logging: SystemConfigLoggingDto.fromJson(json[r'logging'])!, @@ -147,7 +148,6 @@ class SystemConfigDto { server: SystemConfigServerDto.fromJson(json[r'server'])!, storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, theme: SystemConfigThemeDto.fromJson(json[r'theme'])!, - thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!, trash: SystemConfigTrashDto.fromJson(json[r'trash'])!, user: SystemConfigUserDto.fromJson(json[r'user'])!, ); @@ -198,6 +198,7 @@ class SystemConfigDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'ffmpeg', + 'image', 'job', 'library', 'logging', @@ -210,7 +211,6 @@ class SystemConfigDto { 'server', 'storageTemplate', 'theme', - 'thumbnail', 'trash', 'user', }; diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart new file mode 100644 index 000000000..1c830861a --- /dev/null +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -0,0 +1,138 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SystemConfigImageDto { + /// Returns a new [SystemConfigImageDto] instance. + SystemConfigImageDto({ + required this.colorspace, + required this.previewFormat, + required this.previewSize, + required this.quality, + required this.thumbnailFormat, + required this.thumbnailSize, + }); + + Colorspace colorspace; + + ImageFormat previewFormat; + + int previewSize; + + int quality; + + ImageFormat thumbnailFormat; + + int thumbnailSize; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && + other.colorspace == colorspace && + other.previewFormat == previewFormat && + other.previewSize == previewSize && + other.quality == quality && + other.thumbnailFormat == thumbnailFormat && + other.thumbnailSize == thumbnailSize; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (colorspace.hashCode) + + (previewFormat.hashCode) + + (previewSize.hashCode) + + (quality.hashCode) + + (thumbnailFormat.hashCode) + + (thumbnailSize.hashCode); + + @override + String toString() => 'SystemConfigImageDto[colorspace=$colorspace, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; + + Map toJson() { + final json = {}; + json[r'colorspace'] = this.colorspace; + json[r'previewFormat'] = this.previewFormat; + json[r'previewSize'] = this.previewSize; + json[r'quality'] = this.quality; + json[r'thumbnailFormat'] = this.thumbnailFormat; + json[r'thumbnailSize'] = this.thumbnailSize; + return json; + } + + /// Returns a new [SystemConfigImageDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigImageDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return SystemConfigImageDto( + colorspace: Colorspace.fromJson(json[r'colorspace'])!, + previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!, + previewSize: mapValueOfType(json, r'previewSize')!, + quality: mapValueOfType(json, r'quality')!, + thumbnailFormat: ImageFormat.fromJson(json[r'thumbnailFormat'])!, + thumbnailSize: mapValueOfType(json, r'thumbnailSize')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigImageDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigImageDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigImageDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigImageDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'colorspace', + 'previewFormat', + 'previewSize', + 'quality', + 'thumbnailFormat', + 'thumbnailSize', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_thumbnail_dto.dart b/mobile/openapi/lib/model/system_config_thumbnail_dto.dart deleted file mode 100644 index f1d55d622..000000000 --- a/mobile/openapi/lib/model/system_config_thumbnail_dto.dart +++ /dev/null @@ -1,122 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class SystemConfigThumbnailDto { - /// Returns a new [SystemConfigThumbnailDto] instance. - SystemConfigThumbnailDto({ - required this.colorspace, - required this.jpegSize, - required this.quality, - required this.webpSize, - }); - - Colorspace colorspace; - - int jpegSize; - - int quality; - - int webpSize; - - @override - bool operator ==(Object other) => identical(this, other) || other is SystemConfigThumbnailDto && - other.colorspace == colorspace && - other.jpegSize == jpegSize && - other.quality == quality && - other.webpSize == webpSize; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (colorspace.hashCode) + - (jpegSize.hashCode) + - (quality.hashCode) + - (webpSize.hashCode); - - @override - String toString() => 'SystemConfigThumbnailDto[colorspace=$colorspace, jpegSize=$jpegSize, quality=$quality, webpSize=$webpSize]'; - - Map toJson() { - final json = {}; - json[r'colorspace'] = this.colorspace; - json[r'jpegSize'] = this.jpegSize; - json[r'quality'] = this.quality; - json[r'webpSize'] = this.webpSize; - return json; - } - - /// Returns a new [SystemConfigThumbnailDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static SystemConfigThumbnailDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - return SystemConfigThumbnailDto( - colorspace: Colorspace.fromJson(json[r'colorspace'])!, - jpegSize: mapValueOfType(json, r'jpegSize')!, - quality: mapValueOfType(json, r'quality')!, - webpSize: mapValueOfType(json, r'webpSize')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = SystemConfigThumbnailDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = SystemConfigThumbnailDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of SystemConfigThumbnailDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = SystemConfigThumbnailDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'colorspace', - 'jpegSize', - 'quality', - 'webpSize', - }; -} - diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 41d0ac8f5..0c1729e95 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -100,7 +100,7 @@ void main() { // TODO }); - //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isNotInAlbum, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, List personIds, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async + //Future> searchAssets({ String checksum, String city, String country, DateTime createdAfter, DateTime createdBefore, String deviceAssetId, String deviceId, String encodedVideoPath, String id, bool isArchived, bool isEncoded, bool isExternal, bool isFavorite, bool isMotion, bool isNotInAlbum, bool isOffline, bool isReadOnly, bool isVisible, String lensModel, String libraryId, String make, String model, AssetOrder order, String originalFileName, String originalPath, num page, List personIds, String previewPath, String resizePath, num size, String state, DateTime takenAfter, DateTime takenBefore, String thumbnailPath, DateTime trashedAfter, DateTime trashedBefore, AssetTypeEnum type, DateTime updatedAfter, DateTime updatedBefore, String webpPath, bool withArchived, bool withDeleted, bool withExif, bool withPeople, bool withStacked }) async test('test searchAssets', () async { // TODO }); diff --git a/mobile/openapi/test/image_format_test.dart b/mobile/openapi/test/image_format_test.dart new file mode 100644 index 000000000..2bb1512a6 --- /dev/null +++ b/mobile/openapi/test/image_format_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for ImageFormat +void main() { + + group('test ImageFormat', () { + + }); + +} diff --git a/mobile/openapi/test/memory_api_test.dart b/mobile/openapi/test/memory_api_test.dart new file mode 100644 index 000000000..1a930782e --- /dev/null +++ b/mobile/openapi/test/memory_api_test.dart @@ -0,0 +1,56 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + + +/// tests for MemoryApi +void main() { + // final instance = MemoryApi(); + + group('tests for MemoryApi', () { + //Future> addMemoryAssets(String id, BulkIdsDto bulkIdsDto) async + test('test addMemoryAssets', () async { + // TODO + }); + + //Future createMemory(MemoryCreateDto memoryCreateDto) async + test('test createMemory', () async { + // TODO + }); + + //Future deleteMemory(String id) async + test('test deleteMemory', () async { + // TODO + }); + + //Future getMemory(String id) async + test('test getMemory', () async { + // TODO + }); + + //Future> removeMemoryAssets(String id, BulkIdsDto bulkIdsDto) async + test('test removeMemoryAssets', () async { + // TODO + }); + + //Future> searchMemories() async + test('test searchMemories', () async { + // TODO + }); + + //Future updateMemory(String id, MemoryUpdateDto memoryUpdateDto) async + test('test updateMemory', () async { + // TODO + }); + + }); +} diff --git a/mobile/openapi/test/memory_create_dto_test.dart b/mobile/openapi/test/memory_create_dto_test.dart new file mode 100644 index 000000000..bfb6f09e1 --- /dev/null +++ b/mobile/openapi/test/memory_create_dto_test.dart @@ -0,0 +1,52 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for MemoryCreateDto +void main() { + // final instance = MemoryCreateDto(); + + group('test MemoryCreateDto', () { + // List assetIds (default value: const []) + test('to test the property `assetIds`', () async { + // TODO + }); + + // OnThisDayDto data + test('to test the property `data`', () async { + // TODO + }); + + // bool isSaved + test('to test the property `isSaved`', () async { + // TODO + }); + + // DateTime memoryAt + test('to test the property `memoryAt`', () async { + // TODO + }); + + // DateTime seenAt + test('to test the property `seenAt`', () async { + // TODO + }); + + // MemoryType type + test('to test the property `type`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/memory_response_dto_test.dart b/mobile/openapi/test/memory_response_dto_test.dart new file mode 100644 index 000000000..90b2e7834 --- /dev/null +++ b/mobile/openapi/test/memory_response_dto_test.dart @@ -0,0 +1,77 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for MemoryResponseDto +void main() { + // final instance = MemoryResponseDto(); + + group('test MemoryResponseDto', () { + // List assets (default value: const []) + test('to test the property `assets`', () async { + // TODO + }); + + // DateTime createdAt + test('to test the property `createdAt`', () async { + // TODO + }); + + // OnThisDayDto data + test('to test the property `data`', () async { + // TODO + }); + + // DateTime deletedAt + test('to test the property `deletedAt`', () async { + // TODO + }); + + // String id + test('to test the property `id`', () async { + // TODO + }); + + // bool isSaved + test('to test the property `isSaved`', () async { + // TODO + }); + + // DateTime memoryAt + test('to test the property `memoryAt`', () async { + // TODO + }); + + // String ownerId + test('to test the property `ownerId`', () async { + // TODO + }); + + // DateTime seenAt + test('to test the property `seenAt`', () async { + // TODO + }); + + // String type + test('to test the property `type`', () async { + // TODO + }); + + // DateTime updatedAt + test('to test the property `updatedAt`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/memory_type_test.dart b/mobile/openapi/test/memory_type_test.dart new file mode 100644 index 000000000..0a6589d9a --- /dev/null +++ b/mobile/openapi/test/memory_type_test.dart @@ -0,0 +1,21 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for MemoryType +void main() { + + group('test MemoryType', () { + + }); + +} diff --git a/mobile/openapi/test/memory_update_dto_test.dart b/mobile/openapi/test/memory_update_dto_test.dart new file mode 100644 index 000000000..173128e39 --- /dev/null +++ b/mobile/openapi/test/memory_update_dto_test.dart @@ -0,0 +1,37 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for MemoryUpdateDto +void main() { + // final instance = MemoryUpdateDto(); + + group('test MemoryUpdateDto', () { + // bool isSaved + test('to test the property `isSaved`', () async { + // TODO + }); + + // DateTime memoryAt + test('to test the property `memoryAt`', () async { + // TODO + }); + + // DateTime seenAt + test('to test the property `seenAt`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/metadata_search_dto_test.dart b/mobile/openapi/test/metadata_search_dto_test.dart index f817b7da7..62979da9c 100644 --- a/mobile/openapi/test/metadata_search_dto_test.dart +++ b/mobile/openapi/test/metadata_search_dto_test.dart @@ -151,6 +151,11 @@ void main() { // TODO }); + // String previewPath + test('to test the property `previewPath`', () async { + // TODO + }); + // String resizePath test('to test the property `resizePath`', () async { // TODO @@ -176,6 +181,11 @@ void main() { // TODO }); + // String thumbnailPath + test('to test the property `thumbnailPath`', () async { + // TODO + }); + // DateTime trashedAfter test('to test the property `trashedAfter`', () async { // TODO diff --git a/mobile/openapi/test/on_this_day_dto_test.dart b/mobile/openapi/test/on_this_day_dto_test.dart new file mode 100644 index 000000000..71379f8bb --- /dev/null +++ b/mobile/openapi/test/on_this_day_dto_test.dart @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for OnThisDayDto +void main() { + // final instance = OnThisDayDto(); + + group('test OnThisDayDto', () { + // num year + test('to test the property `year`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index b41d07e5f..e88ee17c4 100644 --- a/mobile/openapi/test/system_config_dto_test.dart +++ b/mobile/openapi/test/system_config_dto_test.dart @@ -21,6 +21,11 @@ void main() { // TODO }); + // SystemConfigImageDto image + test('to test the property `image`', () async { + // TODO + }); + // SystemConfigJobDto job test('to test the property `job`', () async { // TODO @@ -81,11 +86,6 @@ void main() { // TODO }); - // SystemConfigThumbnailDto thumbnail - test('to test the property `thumbnail`', () async { - // TODO - }); - // SystemConfigTrashDto trash test('to test the property `trash`', () async { // TODO diff --git a/mobile/openapi/test/system_config_thumbnail_dto_test.dart b/mobile/openapi/test/system_config_image_dto_test.dart similarity index 53% rename from mobile/openapi/test/system_config_thumbnail_dto_test.dart rename to mobile/openapi/test/system_config_image_dto_test.dart index 3cc66f467..aef907bbe 100644 --- a/mobile/openapi/test/system_config_thumbnail_dto_test.dart +++ b/mobile/openapi/test/system_config_image_dto_test.dart @@ -11,18 +11,23 @@ import 'package:openapi/api.dart'; import 'package:test/test.dart'; -// tests for SystemConfigThumbnailDto +// tests for SystemConfigImageDto void main() { - // final instance = SystemConfigThumbnailDto(); + // final instance = SystemConfigImageDto(); - group('test SystemConfigThumbnailDto', () { + group('test SystemConfigImageDto', () { // Colorspace colorspace test('to test the property `colorspace`', () async { // TODO }); - // int jpegSize - test('to test the property `jpegSize`', () async { + // ImageFormat previewFormat + test('to test the property `previewFormat`', () async { + // TODO + }); + + // int previewSize + test('to test the property `previewSize`', () async { // TODO }); @@ -31,8 +36,13 @@ void main() { // TODO }); - // int webpSize - test('to test the property `webpSize`', () async { + // ImageFormat thumbnailFormat + test('to test the property `thumbnailFormat`', () async { + // TODO + }); + + // int thumbnailSize + test('to test the property `thumbnailSize`', () async { // TODO }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index a2aae92c4..70e797f52 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2101,10 +2101,19 @@ } } }, + { + "name": "previewPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "resizePath", "required": false, "in": "query", + "deprecated": true, "schema": { "type": "string" } @@ -2143,6 +2152,14 @@ "type": "string" } }, + { + "name": "thumbnailPath", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + }, { "name": "trashedAfter", "required": false, @@ -2191,6 +2208,7 @@ "name": "webpPath", "required": false, "in": "query", + "deprecated": true, "schema": { "type": "string" } @@ -3417,6 +3435,314 @@ ] } }, + "/memories": { + "get": { + "operationId": "searchMemories", + "parameters": [], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/MemoryResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + }, + "post": { + "operationId": "createMemory", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryCreateDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + } + }, + "/memories/{id}": { + "delete": { + "operationId": "deleteMemory", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + }, + "get": { + "operationId": "getMemory", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + }, + "put": { + "operationId": "updateMemory", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MemoryResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + } + }, + "/memories/{id}/assets": { + "delete": { + "operationId": "removeMemoryAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + }, + "put": { + "operationId": "addMemoryAssets", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/BulkIdResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Memory" + ] + } + }, "/oauth/authorize": { "post": { "operationId": "startOAuth", @@ -8114,6 +8440,13 @@ ], "type": "object" }, + "ImageFormat": { + "enum": [ + "jpeg", + "webp" + ], + "type": "string" + }, "JobCommand": { "enum": [ "start", @@ -8426,6 +8759,40 @@ ], "type": "string" }, + "MemoryCreateDto": { + "properties": { + "assetIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "data": { + "$ref": "#/components/schemas/OnThisDayDto" + }, + "isSaved": { + "type": "boolean" + }, + "memoryAt": { + "format": "date-time", + "type": "string" + }, + "seenAt": { + "format": "date-time", + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/MemoryType" + } + }, + "required": [ + "data", + "memoryAt", + "type" + ], + "type": "object" + }, "MemoryLaneResponseDto": { "properties": { "assets": { @@ -8449,6 +8816,88 @@ ], "type": "object" }, + "MemoryResponseDto": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, + "data": { + "$ref": "#/components/schemas/OnThisDayDto" + }, + "deletedAt": { + "format": "date-time", + "type": "string" + }, + "id": { + "type": "string" + }, + "isSaved": { + "type": "boolean" + }, + "memoryAt": { + "format": "date-time", + "type": "string" + }, + "ownerId": { + "type": "string" + }, + "seenAt": { + "format": "date-time", + "type": "string" + }, + "type": { + "enum": [ + "on_this_day" + ], + "type": "string" + }, + "updatedAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "assets", + "createdAt", + "data", + "id", + "isSaved", + "memoryAt", + "ownerId", + "type", + "updatedAt" + ], + "type": "object" + }, + "MemoryType": { + "enum": [ + "on_this_day" + ], + "type": "string" + }, + "MemoryUpdateDto": { + "properties": { + "isSaved": { + "type": "boolean" + }, + "memoryAt": { + "format": "date-time", + "type": "string" + }, + "seenAt": { + "format": "date-time", + "type": "string" + } + }, + "type": "object" + }, "MergePersonDto": { "properties": { "ids": { @@ -8555,7 +9004,11 @@ }, "type": "array" }, + "previewPath": { + "type": "string" + }, "resizePath": { + "deprecated": true, "type": "string" }, "size": { @@ -8572,6 +9025,9 @@ "format": "date-time", "type": "string" }, + "thumbnailPath": { + "type": "string" + }, "trashedAfter": { "format": "date-time", "type": "string" @@ -8592,6 +9048,7 @@ "type": "string" }, "webpPath": { + "deprecated": true, "type": "string" }, "withArchived": { @@ -8653,6 +9110,17 @@ ], "type": "object" }, + "OnThisDayDto": { + "properties": { + "year": { + "type": "number" + } + }, + "required": [ + "year" + ], + "type": "object" + }, "PartnerResponseDto": { "properties": { "avatarColor": { @@ -8746,8 +9214,8 @@ "PathType": { "enum": [ "original", - "jpeg_thumbnail", - "webp_thumbnail", + "preview", + "thumbnail", "encoded_video", "sidecar", "face", @@ -9743,6 +10211,9 @@ "ffmpeg": { "$ref": "#/components/schemas/SystemConfigFFmpegDto" }, + "image": { + "$ref": "#/components/schemas/SystemConfigImageDto" + }, "job": { "$ref": "#/components/schemas/SystemConfigJobDto" }, @@ -9779,9 +10250,6 @@ "theme": { "$ref": "#/components/schemas/SystemConfigThemeDto" }, - "thumbnail": { - "$ref": "#/components/schemas/SystemConfigThumbnailDto" - }, "trash": { "$ref": "#/components/schemas/SystemConfigTrashDto" }, @@ -9791,6 +10259,7 @@ }, "required": [ "ffmpeg", + "image", "job", "library", "logging", @@ -9803,7 +10272,6 @@ "server", "storageTemplate", "theme", - "thumbnail", "trash", "user" ], @@ -9902,6 +10370,37 @@ ], "type": "object" }, + "SystemConfigImageDto": { + "properties": { + "colorspace": { + "$ref": "#/components/schemas/Colorspace" + }, + "previewFormat": { + "$ref": "#/components/schemas/ImageFormat" + }, + "previewSize": { + "type": "integer" + }, + "quality": { + "type": "integer" + }, + "thumbnailFormat": { + "$ref": "#/components/schemas/ImageFormat" + }, + "thumbnailSize": { + "type": "integer" + } + }, + "required": [ + "colorspace", + "previewFormat", + "previewSize", + "quality", + "thumbnailFormat", + "thumbnailSize" + ], + "type": "object" + }, "SystemConfigJobDto": { "properties": { "backgroundTask": { @@ -10251,29 +10750,6 @@ ], "type": "object" }, - "SystemConfigThumbnailDto": { - "properties": { - "colorspace": { - "$ref": "#/components/schemas/Colorspace" - }, - "jpegSize": { - "type": "integer" - }, - "quality": { - "type": "integer" - }, - "webpSize": { - "type": "integer" - } - }, - "required": [ - "colorspace", - "jpegSize", - "quality", - "webpSize" - ], - "type": "object" - }, "SystemConfigTrashDto": { "properties": { "days": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index b9b4978a9..bed1380d9 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -494,6 +494,35 @@ export type ValidateLibraryImportPathResponseDto = { export type ValidateLibraryResponseDto = { importPaths?: ValidateLibraryImportPathResponseDto[]; }; +export type OnThisDayDto = { + year: number; +}; +export type MemoryResponseDto = { + assets: AssetResponseDto[]; + createdAt: string; + data: OnThisDayDto; + deletedAt?: string; + id: string; + isSaved: boolean; + memoryAt: string; + ownerId: string; + seenAt?: string; + "type": Type2; + updatedAt: string; +}; +export type MemoryCreateDto = { + assetIds?: string[]; + data: OnThisDayDto; + isSaved?: boolean; + memoryAt: string; + seenAt?: string; + "type": MemoryType; +}; +export type MemoryUpdateDto = { + isSaved?: boolean; + memoryAt?: string; + seenAt?: string; +}; export type OAuthConfigDto = { redirectUri: string; }; @@ -640,11 +669,13 @@ export type MetadataSearchDto = { originalPath?: string; page?: number; personIds?: string[]; + previewPath?: string; resizePath?: string; size?: number; state?: string; takenAfter?: string; takenBefore?: string; + thumbnailPath?: string; trashedAfter?: string; trashedBefore?: string; "type"?: AssetTypeEnum; @@ -827,6 +858,14 @@ export type SystemConfigFFmpegDto = { transcode: TranscodePolicy; twoPass: boolean; }; +export type SystemConfigImageDto = { + colorspace: Colorspace; + previewFormat: ImageFormat; + previewSize: number; + quality: number; + thumbnailFormat: ImageFormat; + thumbnailSize: number; +}; export type JobSettingsDto = { concurrency: number; }; @@ -919,12 +958,6 @@ export type SystemConfigStorageTemplateDto = { export type SystemConfigThemeDto = { customCss: string; }; -export type SystemConfigThumbnailDto = { - colorspace: Colorspace; - jpegSize: number; - quality: number; - webpSize: number; -}; export type SystemConfigTrashDto = { days: number; enabled: boolean; @@ -934,6 +967,7 @@ export type SystemConfigUserDto = { }; export type SystemConfigDto = { ffmpeg: SystemConfigFFmpegDto; + image: SystemConfigImageDto; job: SystemConfigJobDto; library: SystemConfigLibraryDto; logging: SystemConfigLoggingDto; @@ -946,7 +980,6 @@ export type SystemConfigDto = { server: SystemConfigServerDto; storageTemplate: SystemConfigStorageTemplateDto; theme: SystemConfigThemeDto; - thumbnail: SystemConfigThumbnailDto; trash: SystemConfigTrashDto; user: SystemConfigUserDto; }; @@ -1497,7 +1530,7 @@ export function updateAsset({ id, updateAssetDto }: { body: updateAssetDto }))); } -export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, resizePath, size, state, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { +export function searchAssets({ checksum, city, country, createdAfter, createdBefore, deviceAssetId, deviceId, encodedVideoPath, id, isArchived, isEncoded, isExternal, isFavorite, isMotion, isNotInAlbum, isOffline, isReadOnly, isVisible, lensModel, libraryId, make, model, order, originalFileName, originalPath, page, personIds, previewPath, resizePath, size, state, takenAfter, takenBefore, thumbnailPath, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, webpPath, withArchived, withDeleted, withExif, withPeople, withStacked }: { checksum?: string; city?: string; country?: string; @@ -1525,11 +1558,13 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef originalPath?: string; page?: number; personIds?: string[]; + previewPath?: string; resizePath?: string; size?: number; state?: string; takenAfter?: string; takenBefore?: string; + thumbnailPath?: string; trashedAfter?: string; trashedBefore?: string; $type?: AssetTypeEnum; @@ -1573,11 +1608,13 @@ export function searchAssets({ checksum, city, country, createdAfter, createdBef originalPath, page, personIds, + previewPath, resizePath, size, state, takenAfter, takenBefore, + thumbnailPath, trashedAfter, trashedBefore, "type": $type, @@ -1900,6 +1937,83 @@ export function validate({ id, validateLibraryDto }: { body: validateLibraryDto }))); } +export function searchMemories(opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MemoryResponseDto[]; + }>("/memories", { + ...opts + })); +} +export function createMemory({ memoryCreateDto }: { + memoryCreateDto: MemoryCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: MemoryResponseDto; + }>("/memories", oazapfts.json({ + ...opts, + method: "POST", + body: memoryCreateDto + }))); +} +export function deleteMemory({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/memories/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getMemory({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MemoryResponseDto; + }>(`/memories/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updateMemory({ id, memoryUpdateDto }: { + id: string; + memoryUpdateDto: MemoryUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: MemoryResponseDto; + }>(`/memories/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: memoryUpdateDto + }))); +} +export function removeMemoryAssets({ id, bulkIdsDto }: { + id: string; + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: BulkIdResponseDto[]; + }>(`/memories/${encodeURIComponent(id)}/assets`, oazapfts.json({ + ...opts, + method: "DELETE", + body: bulkIdsDto + }))); +} +export function addMemoryAssets({ id, bulkIdsDto }: { + id: string; + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: BulkIdResponseDto[]; + }>(`/memories/${encodeURIComponent(id)}/assets`, oazapfts.json({ + ...opts, + method: "PUT", + body: bulkIdsDto + }))); +} export function startOAuth({ oAuthConfigDto }: { oAuthConfigDto: OAuthConfigDto; }, opts?: Oazapfts.RequestOpts) { @@ -2802,8 +2916,8 @@ export enum PathEntityType { } export enum PathType { Original = "original", - JpegThumbnail = "jpeg_thumbnail", - WebpThumbnail = "webp_thumbnail", + Preview = "preview", + Thumbnail = "thumbnail", EncodedVideo = "encoded_video", Sidecar = "sidecar", Face = "face", @@ -2834,6 +2948,12 @@ export enum LibraryType { Upload = "UPLOAD", External = "EXTERNAL" } +export enum Type2 { + OnThisDay = "on_this_day" +} +export enum MemoryType { + OnThisDay = "on_this_day" +} export enum SearchSuggestionType { Country = "country", State = "state", @@ -2885,6 +3005,14 @@ export enum TranscodePolicy { Required = "required", Disabled = "disabled" } +export enum Colorspace { + Srgb = "srgb", + P3 = "p3" +} +export enum ImageFormat { + Jpeg = "jpeg", + Webp = "webp" +} export enum LogLevel { Verbose = "verbose", Debug = "debug", @@ -2901,10 +3029,6 @@ export enum ModelType { FacialRecognition = "facial-recognition", Clip = "clip" } -export enum Colorspace { - Srgb = "srgb", - P3 = "p3" -} export enum MapTheme { Light = "light", Dark = "dark" diff --git a/server/Dockerfile b/server/Dockerfile index c1615d501..19f6c76eb 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -24,7 +24,7 @@ RUN npm prune --omit=dev --omit=optional COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img # web build -FROM node:iron-alpine3.18@sha256:876514790dabd49fae7d9c4dfbba027954bd91d8e7d36da76334466533bc6b0c as web +FROM node:iron-alpine3.18@sha256:fa5d3cf51725bd42d32e67917623038539dbe720dab082f590785c001eb4dfef as web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/server/README.md b/server/README.md new file mode 100644 index 000000000..61b6c2538 --- /dev/null +++ b/server/README.md @@ -0,0 +1,3 @@ +# Immich server project + +This project uses the [NestJS](https://nestjs.com/) web framework. Please refer to [the NestJS docs](https://docs.nestjs.com/) for information on getting started as a contributor to this project. diff --git a/server/package-lock.json b/server/package-lock.json index a17d82d46..e268cfbd2 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -995,9 +995,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", + "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -1286,9 +1286,9 @@ "dev": true }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz", - "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz", + "integrity": "sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==", "cpu": [ "arm64" ], @@ -1307,13 +1307,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.1" + "@img/sharp-libvips-darwin-arm64": "1.0.2" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz", - "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz", + "integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==", "cpu": [ "x64" ], @@ -1332,13 +1332,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.1" + "@img/sharp-libvips-darwin-x64": "1.0.2" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", "cpu": [ "arm64" ], @@ -1357,9 +1357,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz", - "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", + "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", "cpu": [ "x64" ], @@ -1378,9 +1378,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz", - "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", + "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", "cpu": [ "arm" ], @@ -1399,9 +1399,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz", - "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", + "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", "cpu": [ "arm64" ], @@ -1420,9 +1420,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz", - "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", + "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", "cpu": [ "s390x" ], @@ -1441,9 +1441,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz", - "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", + "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", "cpu": [ "x64" ], @@ -1462,9 +1462,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz", - "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", + "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", "cpu": [ "arm64" ], @@ -1483,9 +1483,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz", - "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", + "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", "cpu": [ "x64" ], @@ -1504,9 +1504,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz", - "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz", + "integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==", "cpu": [ "arm" ], @@ -1525,13 +1525,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.1" + "@img/sharp-libvips-linux-arm": "1.0.2" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz", - "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz", + "integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==", "cpu": [ "arm64" ], @@ -1550,13 +1550,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.1" + "@img/sharp-libvips-linux-arm64": "1.0.2" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz", - "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz", + "integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==", "cpu": [ "s390x" ], @@ -1575,13 +1575,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.1" + "@img/sharp-libvips-linux-s390x": "1.0.2" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz", - "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz", + "integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==", "cpu": [ "x64" ], @@ -1600,13 +1600,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.1" + "@img/sharp-libvips-linux-x64": "1.0.2" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz", - "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz", + "integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==", "cpu": [ "arm64" ], @@ -1625,13 +1625,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz", - "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz", + "integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==", "cpu": [ "x64" ], @@ -1650,19 +1650,19 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.1" + "@img/sharp-libvips-linuxmusl-x64": "1.0.2" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz", - "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz", + "integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==", "cpu": [ "wasm32" ], "optional": true, "dependencies": { - "@emnapi/runtime": "^0.45.0" + "@emnapi/runtime": "^1.1.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0", @@ -1675,9 +1675,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz", - "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz", + "integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==", "cpu": [ "ia32" ], @@ -1696,9 +1696,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz", - "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz", + "integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==", "cpu": [ "x64" ], @@ -2381,9 +2381,9 @@ ] }, "node_modules/@nestjs/bull-shared": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.0.tgz", - "integrity": "sha512-E1lAvVTCwbtBXySElkVrleXzr1bNuTCOLaQ1GmLSQGGlzXIvrXFXEIS1Dh1JCULICC25b7rGOfD3yL7uKRaMzw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.1.tgz", + "integrity": "sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==", "dependencies": { "tslib": "2.6.2" }, @@ -2393,11 +2393,11 @@ } }, "node_modules/@nestjs/bullmq": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.0.tgz", - "integrity": "sha512-e4QD3JilyOZAddyQ4ABj0rX7T2Rr0OVx4KwJKWTpaEqNQhBP4yVLZbSdEY+GFu1HEE8NkV92Q8BJJdCxlVphSw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.1.tgz", + "integrity": "sha512-afYx1wYCKtXEu1p0S1+qw2o7QaZWr/EQgF7Wkt3YL8RBIECy5S4C450gv/cRGd8EZjlt6bw8hGCLqR2Q5VjHpQ==", "dependencies": { - "@nestjs/bull-shared": "^10.1.0", + "@nestjs/bull-shared": "^10.1.1", "tslib": "2.6.2" }, "peerDependencies": { @@ -2463,6 +2463,52 @@ "balanced-match": "^1.0.0" } }, + "node_modules/@nestjs/cli/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nestjs/cli/node_modules/glob/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@nestjs/cli/node_modules/glob/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@nestjs/cli/node_modules/minimatch": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", @@ -2537,9 +2583,9 @@ } }, "node_modules/@nestjs/common": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.5.tgz", - "integrity": "sha512-XWxbDf2ey/jAyEa3/XpckgfzJZ9j3I05ZkEFx7cAlebFuVKeq5UDDb5Sq9O7hMmbH9xdQj3pYT19SSj01hKeug==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.7.tgz", + "integrity": "sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==", "dependencies": { "iterare": "1.2.1", "tslib": "2.6.2", @@ -2565,11 +2611,11 @@ } }, "node_modules/@nestjs/config": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.0.tgz", - "integrity": "sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.1.tgz", + "integrity": "sha512-tFZyLJKanSAu51ygQ6ZBSpx95pRcwS6qSpJDW6FFgRQzkOaOUXpL8qD8yMNoYoYxuJCxph+waiBaWKgFWxn3sw==", "dependencies": { - "dotenv": "16.4.1", + "dotenv": "16.4.5", "dotenv-expand": "10.0.0", "lodash": "4.17.21", "uuid": "9.0.1" @@ -2579,21 +2625,10 @@ "rxjs": "^7.1.0" } }, - "node_modules/@nestjs/config/node_modules/dotenv": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", - "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/motdotla/dotenv?sponsor=1" - } - }, "node_modules/@nestjs/core": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.5.tgz", - "integrity": "sha512-U7SrGD9/Mu4eUtxfZYiGdY38FcksEyJegs4dQZ8B19nnusw0aTocPEy4HVsmx0LLO4sG+fBLLYzCDDr9kFwXAQ==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.7.tgz", + "integrity": "sha512-hsdlnfiQ3kgqHL5k7js3CU0PV7hBJVi+LfFMgCkoagRxNMf67z0GFGeOV2jk5d65ssB19qdYsDa1MGVuEaoUpg==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", @@ -2659,13 +2694,13 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.5.tgz", - "integrity": "sha512-IhVomwLvdLlv4zCdQK2ROT/nInk1i8m4K48lAUHJV5UVktgVmg0WbQga2/9KywaTjNbx+eWhZXXFii+vtFRAOw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", + "integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", - "express": "4.18.3", + "express": "4.19.2", "multer": "1.4.4-lts.1", "tslib": "2.6.2" }, @@ -2679,9 +2714,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.5.tgz", - "integrity": "sha512-G2N3sTd9tZ7XQQ7RlrpaQdt1/IBztVHuKg686QmBTLVlRHZ1AMOmXouBk+q5SINT1XURiABa8tQh1Ydx0OEh9w==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", + "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", "dependencies": { "socket.io": "4.7.5", "tslib": "2.6.2" @@ -2732,9 +2767,9 @@ "dev": true }, "node_modules/@nestjs/swagger": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.0.tgz", - "integrity": "sha512-zLkfKZ+ioYsIZ3dfv7Bj8YHnZMNAGWFUmx2ZDuLp/fBE4P8BSjB7hldzDueFXsmwaPL90v7lgyd82P+s7KME1Q==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.1.tgz", + "integrity": "sha512-LUC4mr+5oAleEC/a2j8pNRh1S5xhKXJ1Gal5ZdRjt9XebQgbngXCdW7JTA9WOEcwGtFZN9EnKYdquzH971LZfw==", "dependencies": { "@microsoft/tsdoc": "^0.14.2", "@nestjs/mapped-types": "2.0.5", @@ -2764,9 +2799,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.5.tgz", - "integrity": "sha512-j30/lxH0BayeDTigapYtQn/XhMRR7CzlFsm3dHoWViWQv0qT1r2ffe3927BbBLX3N/ZzglE10OAqW06ADZV8dw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.7.tgz", + "integrity": "sha512-PmwZXyoCC/m3F3IFgpgD+SNN6cDPQa/vi3YQxFruvfX3cuHq+P6ZFvBB7hwaKKsLlhA0so42LsMm41oFBkdouw==", "dev": true, "dependencies": { "tslib": "2.6.2" @@ -2806,9 +2841,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.5.tgz", - "integrity": "sha512-6w383LUBFHoZ0eFODqEHN2NoIRUwbTd37Hc1KqtZZihhFUzscC/0LMAV20o9LdfS/Xjog5ShNTxvOHuzNBnE4A==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.7.tgz", + "integrity": "sha512-iYdsWiRNPUy0XzPoW44bx2MW1griuraTr5fNhoe2rUSNO0mEW1aeXp4v56KeZDLAss31WbeckC5P3N223Fys5g==", "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -4919,16 +4954,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz", - "integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/type-utils": "7.3.1", - "@typescript-eslint/utils": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -4954,15 +4989,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz", - "integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" }, "engines": { @@ -4982,13 +5017,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz", - "integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -4999,13 +5034,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz", - "integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/utils": "7.3.1", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -5026,9 +5061,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz", - "integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -5039,13 +5074,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz", - "integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -5091,17 +5126,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz", - "integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" }, "engines": { @@ -5116,12 +5151,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz", - "integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -7049,9 +7084,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "engines": { "node": ">=8" } @@ -7773,16 +7808,16 @@ } }, "node_modules/express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -7814,9 +7849,9 @@ } }, "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -8453,15 +8488,15 @@ } }, "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" @@ -8499,9 +8534,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -11077,11 +11112,11 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -11092,9 +11127,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "engines": { "node": "14 || >=16.14" } @@ -11231,9 +11266,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "engines": { "node": ">=12" }, @@ -12222,9 +12257,9 @@ "dev": true }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -12376,42 +12411,42 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/sharp": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", - "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.3.tgz", + "integrity": "sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", - "detect-libc": "^2.0.2", - "semver": "^7.5.4" + "detect-libc": "^2.0.3", + "semver": "^7.6.0" }, "engines": { - "libvips": ">=8.15.1", + "libvips": ">=8.15.2", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.2", - "@img/sharp-darwin-x64": "0.33.2", - "@img/sharp-libvips-darwin-arm64": "1.0.1", - "@img/sharp-libvips-darwin-x64": "1.0.1", - "@img/sharp-libvips-linux-arm": "1.0.1", - "@img/sharp-libvips-linux-arm64": "1.0.1", - "@img/sharp-libvips-linux-s390x": "1.0.1", - "@img/sharp-libvips-linux-x64": "1.0.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1", - "@img/sharp-libvips-linuxmusl-x64": "1.0.1", - "@img/sharp-linux-arm": "0.33.2", - "@img/sharp-linux-arm64": "0.33.2", - "@img/sharp-linux-s390x": "0.33.2", - "@img/sharp-linux-x64": "0.33.2", - "@img/sharp-linuxmusl-arm64": "0.33.2", - "@img/sharp-linuxmusl-x64": "0.33.2", - "@img/sharp-wasm32": "0.33.2", - "@img/sharp-win32-ia32": "0.33.2", - "@img/sharp-win32-x64": "0.33.2" + "@img/sharp-darwin-arm64": "0.33.3", + "@img/sharp-darwin-x64": "0.33.3", + "@img/sharp-libvips-darwin-arm64": "1.0.2", + "@img/sharp-libvips-darwin-x64": "1.0.2", + "@img/sharp-libvips-linux-arm": "1.0.2", + "@img/sharp-libvips-linux-arm64": "1.0.2", + "@img/sharp-libvips-linux-s390x": "1.0.2", + "@img/sharp-libvips-linux-x64": "1.0.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", + "@img/sharp-libvips-linuxmusl-x64": "1.0.2", + "@img/sharp-linux-arm": "0.33.3", + "@img/sharp-linux-arm64": "0.33.3", + "@img/sharp-linux-s390x": "0.33.3", + "@img/sharp-linux-x64": "0.33.3", + "@img/sharp-linuxmusl-arm64": "0.33.3", + "@img/sharp-linuxmusl-x64": "0.33.3", + "@img/sharp-wasm32": "0.33.3", + "@img/sharp-win32-ia32": "0.33.3", + "@img/sharp-win32-x64": "0.33.3" } }, "node_modules/shebang-command": { @@ -15161,9 +15196,9 @@ } }, "@emnapi/runtime": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", - "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", + "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", "optional": true, "requires": { "tslib": "^2.4.0" @@ -15393,144 +15428,144 @@ "dev": true }, "@img/sharp-darwin-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz", - "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.3.tgz", + "integrity": "sha512-FaNiGX1MrOuJ3hxuNzWgsT/mg5OHG/Izh59WW2mk1UwYHUwtfbhk5QNKYZgxf0pLOhx9ctGiGa2OykD71vOnSw==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-arm64": "1.0.1" + "@img/sharp-libvips-darwin-arm64": "1.0.2" } }, "@img/sharp-darwin-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz", - "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.3.tgz", + "integrity": "sha512-2QeSl7QDK9ru//YBT4sQkoq7L0EAJZA3rtV+v9p8xTKl4U1bUqTIaCnoC7Ctx2kCjQgwFXDasOtPTCT8eCTXvw==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-x64": "1.0.1" + "@img/sharp-libvips-darwin-x64": "1.0.2" } }, "@img/sharp-libvips-darwin-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz", - "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", "optional": true }, "@img/sharp-libvips-darwin-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz", - "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", + "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", "optional": true }, "@img/sharp-libvips-linux-arm": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz", - "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", + "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", "optional": true }, "@img/sharp-libvips-linux-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz", - "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", + "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", "optional": true }, "@img/sharp-libvips-linux-s390x": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz", - "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", + "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", "optional": true }, "@img/sharp-libvips-linux-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz", - "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", + "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", "optional": true }, "@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz", - "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", + "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", "optional": true }, "@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz", - "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", + "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", "optional": true }, "@img/sharp-linux-arm": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz", - "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.3.tgz", + "integrity": "sha512-Q7Ee3fFSC9P7vUSqVEF0zccJsZ8GiiCJYGWDdhEjdlOeS9/jdkyJ6sUSPj+bL8VuOYFSbofrW0t/86ceVhx32w==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm": "1.0.1" + "@img/sharp-libvips-linux-arm": "1.0.2" } }, "@img/sharp-linux-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz", - "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.3.tgz", + "integrity": "sha512-Zf+sF1jHZJKA6Gor9hoYG2ljr4wo9cY4twaxgFDvlG0Xz9V7sinsPp8pFd1XtlhTzYo0IhDbl3rK7P6MzHpnYA==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm64": "1.0.1" + "@img/sharp-libvips-linux-arm64": "1.0.2" } }, "@img/sharp-linux-s390x": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz", - "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.3.tgz", + "integrity": "sha512-vFk441DKRFepjhTEH20oBlFrHcLjPfI8B0pMIxGm3+yilKyYeHEVvrZhYFdqIseSclIqbQ3SnZMwEMWonY5XFA==", "optional": true, "requires": { - "@img/sharp-libvips-linux-s390x": "1.0.1" + "@img/sharp-libvips-linux-s390x": "1.0.2" } }, "@img/sharp-linux-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz", - "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.3.tgz", + "integrity": "sha512-Q4I++herIJxJi+qmbySd072oDPRkCg/SClLEIDh5IL9h1zjhqjv82H0Seupd+q2m0yOfD+/fJnjSoDFtKiHu2g==", "optional": true, "requires": { - "@img/sharp-libvips-linux-x64": "1.0.1" + "@img/sharp-libvips-linux-x64": "1.0.2" } }, "@img/sharp-linuxmusl-arm64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz", - "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.3.tgz", + "integrity": "sha512-qnDccehRDXadhM9PM5hLvcPRYqyFCBN31kq+ErBSZtZlsAc1U4Z85xf/RXv1qolkdu+ibw64fUDaRdktxTNP9A==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" } }, "@img/sharp-linuxmusl-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz", - "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.3.tgz", + "integrity": "sha512-Jhchim8kHWIU/GZ+9poHMWRcefeaxFIs9EBqf9KtcC14Ojk6qua7ghKiPs0sbeLbLj/2IGBtDcxHyjCdYWkk2w==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.1" + "@img/sharp-libvips-linuxmusl-x64": "1.0.2" } }, "@img/sharp-wasm32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz", - "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.3.tgz", + "integrity": "sha512-68zivsdJ0koE96stdUfM+gmyaK/NcoSZK5dV5CAjES0FUXS9lchYt8LAB5rTbM7nlWtxaU/2GON0HVN6/ZYJAQ==", "optional": true, "requires": { - "@emnapi/runtime": "^0.45.0" + "@emnapi/runtime": "^1.1.0" } }, "@img/sharp-win32-ia32": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz", - "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.3.tgz", + "integrity": "sha512-CyimAduT2whQD8ER4Ux7exKrtfoaUiVr7HG0zZvO0XTFn2idUWljjxv58GxNTkFb8/J9Ub9AqITGkJD6ZginxQ==", "optional": true }, "@img/sharp-win32-x64": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz", - "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.3.tgz", + "integrity": "sha512-viT4fUIDKnli3IfOephGnolMzhz5VaTvDRkYqtZxOMIoMQ4MrAziO7pT1nVnOt2FAm7qW5aa+CCc13aEY6Le0g==", "optional": true }, "@ioredis/commands": { @@ -16039,19 +16074,19 @@ "optional": true }, "@nestjs/bull-shared": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.0.tgz", - "integrity": "sha512-E1lAvVTCwbtBXySElkVrleXzr1bNuTCOLaQ1GmLSQGGlzXIvrXFXEIS1Dh1JCULICC25b7rGOfD3yL7uKRaMzw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.1.tgz", + "integrity": "sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==", "requires": { "tslib": "2.6.2" } }, "@nestjs/bullmq": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.0.tgz", - "integrity": "sha512-e4QD3JilyOZAddyQ4ABj0rX7T2Rr0OVx4KwJKWTpaEqNQhBP4yVLZbSdEY+GFu1HEE8NkV92Q8BJJdCxlVphSw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.1.tgz", + "integrity": "sha512-afYx1wYCKtXEu1p0S1+qw2o7QaZWr/EQgF7Wkt3YL8RBIECy5S4C450gv/cRGd8EZjlt6bw8hGCLqR2Q5VjHpQ==", "requires": { - "@nestjs/bull-shared": "^10.1.0", + "@nestjs/bull-shared": "^10.1.1", "tslib": "2.6.2" } }, @@ -16094,6 +16129,36 @@ "balanced-match": "^1.0.0" } }, + "glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "dependencies": { + "minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + } + } + }, "minimatch": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", @@ -16141,9 +16206,9 @@ } }, "@nestjs/common": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.5.tgz", - "integrity": "sha512-XWxbDf2ey/jAyEa3/XpckgfzJZ9j3I05ZkEFx7cAlebFuVKeq5UDDb5Sq9O7hMmbH9xdQj3pYT19SSj01hKeug==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.7.tgz", + "integrity": "sha512-gKFtFzcJznrwsRYjtNZoPAvSOPYdNgxbTYoAyLTpoy393cIKgLmJTHu6ReH8/qIB9AaZLdGaFLkx98W/tFWFUw==", "requires": { "iterare": "1.2.1", "tslib": "2.6.2", @@ -16151,27 +16216,20 @@ } }, "@nestjs/config": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.0.tgz", - "integrity": "sha512-BpYRn57shg7CH35KGT6h+hT7ZucB6Qn2B3NBNdvhD4ApU8huS5pX/Wc2e/aO5trIha606Bz2a9t9/vbiuTBTww==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.1.tgz", + "integrity": "sha512-tFZyLJKanSAu51ygQ6ZBSpx95pRcwS6qSpJDW6FFgRQzkOaOUXpL8qD8yMNoYoYxuJCxph+waiBaWKgFWxn3sw==", "requires": { - "dotenv": "16.4.1", + "dotenv": "16.4.5", "dotenv-expand": "10.0.0", "lodash": "4.17.21", "uuid": "9.0.1" - }, - "dependencies": { - "dotenv": { - "version": "16.4.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", - "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==" - } } }, "@nestjs/core": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.5.tgz", - "integrity": "sha512-U7SrGD9/Mu4eUtxfZYiGdY38FcksEyJegs4dQZ8B19nnusw0aTocPEy4HVsmx0LLO4sG+fBLLYzCDDr9kFwXAQ==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.7.tgz", + "integrity": "sha512-hsdlnfiQ3kgqHL5k7js3CU0PV7hBJVi+LfFMgCkoagRxNMf67z0GFGeOV2jk5d65ssB19qdYsDa1MGVuEaoUpg==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -16196,21 +16254,21 @@ "requires": {} }, "@nestjs/platform-express": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.5.tgz", - "integrity": "sha512-IhVomwLvdLlv4zCdQK2ROT/nInk1i8m4K48lAUHJV5UVktgVmg0WbQga2/9KywaTjNbx+eWhZXXFii+vtFRAOw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.7.tgz", + "integrity": "sha512-noNJ+PyIxQJLCKfuXz0tcQtlVAynfLIuKy62g70lEZ86UrIqSrZFqvWs/rFUgkbT6J8H7Rmv11hASOnX+7M2rA==", "requires": { "body-parser": "1.20.2", "cors": "2.8.5", - "express": "4.18.3", + "express": "4.19.2", "multer": "1.4.4-lts.1", "tslib": "2.6.2" } }, "@nestjs/platform-socket.io": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.5.tgz", - "integrity": "sha512-G2N3sTd9tZ7XQQ7RlrpaQdt1/IBztVHuKg686QmBTLVlRHZ1AMOmXouBk+q5SINT1XURiABa8tQh1Ydx0OEh9w==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.7.tgz", + "integrity": "sha512-T9VbVgEUnbid/RiywN9/8YQ8pAGDP++0nX73l4kIWeDWkz5DEh4aLB7O/JvLA3/xRHdjTZ4RiRZazwqSWi1Sog==", "requires": { "socket.io": "4.7.5", "tslib": "2.6.2" @@ -16247,9 +16305,9 @@ } }, "@nestjs/swagger": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.0.tgz", - "integrity": "sha512-zLkfKZ+ioYsIZ3dfv7Bj8YHnZMNAGWFUmx2ZDuLp/fBE4P8BSjB7hldzDueFXsmwaPL90v7lgyd82P+s7KME1Q==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.3.1.tgz", + "integrity": "sha512-LUC4mr+5oAleEC/a2j8pNRh1S5xhKXJ1Gal5ZdRjt9XebQgbngXCdW7JTA9WOEcwGtFZN9EnKYdquzH971LZfw==", "requires": { "@microsoft/tsdoc": "^0.14.2", "@nestjs/mapped-types": "2.0.5", @@ -16260,9 +16318,9 @@ } }, "@nestjs/testing": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.5.tgz", - "integrity": "sha512-j30/lxH0BayeDTigapYtQn/XhMRR7CzlFsm3dHoWViWQv0qT1r2ffe3927BbBLX3N/ZzglE10OAqW06ADZV8dw==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.7.tgz", + "integrity": "sha512-PmwZXyoCC/m3F3IFgpgD+SNN6cDPQa/vi3YQxFruvfX3cuHq+P6ZFvBB7hwaKKsLlhA0so42LsMm41oFBkdouw==", "dev": true, "requires": { "tslib": "2.6.2" @@ -16277,9 +16335,9 @@ } }, "@nestjs/websockets": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.5.tgz", - "integrity": "sha512-6w383LUBFHoZ0eFODqEHN2NoIRUwbTd37Hc1KqtZZihhFUzscC/0LMAV20o9LdfS/Xjog5ShNTxvOHuzNBnE4A==", + "version": "10.3.7", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.7.tgz", + "integrity": "sha512-iYdsWiRNPUy0XzPoW44bx2MW1griuraTr5fNhoe2rUSNO0mEW1aeXp4v56KeZDLAss31WbeckC5P3N223Fys5g==", "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -17919,16 +17977,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz", - "integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/type-utils": "7.3.1", - "@typescript-eslint/utils": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -17938,54 +17996,54 @@ } }, "@typescript-eslint/parser": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz", - "integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz", - "integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "requires": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" } }, "@typescript-eslint/type-utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz", - "integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/utils": "7.3.1", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz", - "integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz", - "integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "requires": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -18015,27 +18073,27 @@ } }, "@typescript-eslint/utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz", - "integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz", - "integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" } }, @@ -19467,9 +19525,9 @@ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, "detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==" + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" }, "detect-newline": { "version": "3.1.0", @@ -20001,16 +20059,16 @@ } }, "express": { - "version": "4.18.3", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.3.tgz", - "integrity": "sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -20039,9 +20097,9 @@ }, "dependencies": { "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" }, "debug": { "version": "2.6.9", @@ -20536,15 +20594,15 @@ "dev": true }, "glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "requires": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "dependencies": { "brace-expansion": { @@ -20556,9 +20614,9 @@ } }, "minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "requires": { "brace-expansion": "^2.0.1" } @@ -22528,18 +22586,18 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "requires": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "dependencies": { "lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==" + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==" } } }, @@ -22645,9 +22703,9 @@ "dev": true }, "picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==" }, "pirates": { "version": "4.0.6", @@ -23350,9 +23408,9 @@ } }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "requires": { "lru-cache": "^6.0.0" }, @@ -23487,32 +23545,32 @@ } }, "sharp": { - "version": "0.33.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz", - "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==", + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.3.tgz", + "integrity": "sha512-vHUeXJU1UvlO/BNwTpT0x/r53WkLUVxrmb5JTgW92fdFCFk0ispLMAeu/jPO2vjkXM1fYUi3K7/qcLF47pwM1A==", "requires": { - "@img/sharp-darwin-arm64": "0.33.2", - "@img/sharp-darwin-x64": "0.33.2", - "@img/sharp-libvips-darwin-arm64": "1.0.1", - "@img/sharp-libvips-darwin-x64": "1.0.1", - "@img/sharp-libvips-linux-arm": "1.0.1", - "@img/sharp-libvips-linux-arm64": "1.0.1", - "@img/sharp-libvips-linux-s390x": "1.0.1", - "@img/sharp-libvips-linux-x64": "1.0.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.1", - "@img/sharp-libvips-linuxmusl-x64": "1.0.1", - "@img/sharp-linux-arm": "0.33.2", - "@img/sharp-linux-arm64": "0.33.2", - "@img/sharp-linux-s390x": "0.33.2", - "@img/sharp-linux-x64": "0.33.2", - "@img/sharp-linuxmusl-arm64": "0.33.2", - "@img/sharp-linuxmusl-x64": "0.33.2", - "@img/sharp-wasm32": "0.33.2", - "@img/sharp-win32-ia32": "0.33.2", - "@img/sharp-win32-x64": "0.33.2", + "@img/sharp-darwin-arm64": "0.33.3", + "@img/sharp-darwin-x64": "0.33.3", + "@img/sharp-libvips-darwin-arm64": "1.0.2", + "@img/sharp-libvips-darwin-x64": "1.0.2", + "@img/sharp-libvips-linux-arm": "1.0.2", + "@img/sharp-libvips-linux-arm64": "1.0.2", + "@img/sharp-libvips-linux-s390x": "1.0.2", + "@img/sharp-libvips-linux-x64": "1.0.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", + "@img/sharp-libvips-linuxmusl-x64": "1.0.2", + "@img/sharp-linux-arm": "0.33.3", + "@img/sharp-linux-arm64": "0.33.3", + "@img/sharp-linux-s390x": "0.33.3", + "@img/sharp-linux-x64": "0.33.3", + "@img/sharp-linuxmusl-arm64": "0.33.3", + "@img/sharp-linuxmusl-x64": "0.33.3", + "@img/sharp-wasm32": "0.33.3", + "@img/sharp-win32-ia32": "0.33.3", + "@img/sharp-win32-x64": "0.33.3", "color": "^4.2.3", - "detect-libc": "^2.0.2", - "semver": "^7.5.4" + "detect-libc": "^2.0.3", + "semver": "^7.6.0" } }, "shebang-command": { diff --git a/server/package.json b/server/package.json index afa5bcb29..e5780eb68 100644 --- a/server/package.json +++ b/server/package.json @@ -167,6 +167,6 @@ "globalSetup": "/test/global-setup.js" }, "volta": { - "node": "20.11.1" + "node": "20.12.0" } } diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 00cf7bbab..ce51aa4c0 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -10,6 +10,7 @@ import { DownloadController } from 'src/controllers/download.controller'; import { FaceController } from 'src/controllers/face.controller'; import { JobController } from 'src/controllers/job.controller'; import { LibraryController } from 'src/controllers/library.controller'; +import { MemoryController } from 'src/controllers/memory.controller'; import { OAuthController } from 'src/controllers/oauth.controller'; import { PartnerController } from 'src/controllers/partner.controller'; import { PersonController } from 'src/controllers/person.controller'; @@ -36,6 +37,7 @@ export const controllers = [ FaceController, JobController, LibraryController, + MemoryController, OAuthController, PartnerController, SearchController, diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts new file mode 100644 index 000000000..771d70594 --- /dev/null +++ b/server/src/controllers/memory.controller.ts @@ -0,0 +1,64 @@ +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { MemoryService } from 'src/services/memory.service'; +import { UUIDParamDto } from 'src/validation'; + +@ApiTags('Memory') +@Controller('memories') +@Authenticated() +export class MemoryController { + constructor(private service: MemoryService) {} + + @Get() + searchMemories(@Auth() auth: AuthDto): Promise { + return this.service.search(auth); + } + + @Post() + createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise { + return this.service.create(auth, dto); + } + + @Get(':id') + getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); + } + + @Put(':id') + updateMemory( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: MemoryUpdateDto, + ): Promise { + return this.service.update(auth, id, dto); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.remove(auth, id); + } + + @Put(':id/assets') + addMemoryAssets( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: BulkIdsDto, + ): Promise { + return this.service.addAssets(auth, id, dto); + } + + @Delete(':id/assets') + @HttpCode(HttpStatus.OK) + removeMemoryAssets( + @Auth() auth: AuthDto, + @Body() dto: BulkIdsDto, + @Param() { id }: UUIDParamDto, + ): Promise { + return this.service.removeAssets(auth, id, dto); + } +} diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts index 8d021031e..72644870d 100644 --- a/server/src/cores/access.core.ts +++ b/server/src/cores/access.core.ts @@ -33,6 +33,10 @@ export enum Permission { TIMELINE_READ = 'timeline.read', TIMELINE_DOWNLOAD = 'timeline.download', + MEMORY_READ = 'memory.read', + MEMORY_WRITE = 'memory.write', + MEMORY_DELETE = 'memory.delete', + PERSON_READ = 'person.read', PERSON_WRITE = 'person.write', PERSON_MERGE = 'person.merge', @@ -259,6 +263,18 @@ export class AccessCore { return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); } + case Permission.MEMORY_READ: { + return this.repository.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_WRITE: { + return this.repository.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_DELETE: { + return this.repository.memory.checkOwnerAccess(auth.user.id, ids); + } + case Permission.PERSON_READ: { return await this.repository.person.checkOwnerAccess(auth.user.id, ids); } diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index ee9f12e51..035f90c91 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -4,6 +4,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { ImageFormat } from 'src/entities/system-config.entity'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; @@ -34,7 +35,8 @@ export interface MoveRequest { }; } -type GeneratedAssetPath = AssetPathType.JPEG_THUMBNAIL | AssetPathType.WEBP_THUMBNAIL | AssetPathType.ENCODED_VIDEO; +export type GeneratedImageType = AssetPathType.PREVIEW | AssetPathType.THUMBNAIL; +export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDEO; let instance: StorageCore | null; @@ -94,12 +96,8 @@ export class StorageCore { return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`); } - static getLargeThumbnailPath(asset: AssetEntity) { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.jpeg`); - } - - static getSmallThumbnailPath(asset: AssetEntity) { - return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.webp`); + static getImagePath(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) { + return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`); } static getEncodedVideoPath(asset: AssetEntity) { @@ -128,34 +126,23 @@ export class StorageCore { return path.startsWith(THUMBNAIL_DIR) || path.startsWith(ENCODED_VIDEO_DIR); } - async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) { - const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset; - switch (pathType) { - case AssetPathType.JPEG_THUMBNAIL: { - return this.moveFile({ - entityId, - pathType, - oldPath: resizePath, - newPath: StorageCore.getLargeThumbnailPath(asset), - }); - } - case AssetPathType.WEBP_THUMBNAIL: { - return this.moveFile({ - entityId, - pathType, - oldPath: webpPath, - newPath: StorageCore.getSmallThumbnailPath(asset), - }); - } - case AssetPathType.ENCODED_VIDEO: { - return this.moveFile({ - entityId, - pathType, - oldPath: encodedVideoPath, - newPath: StorageCore.getEncodedVideoPath(asset), - }); - } - } + async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) { + const { id: entityId, previewPath, thumbnailPath } = asset; + return this.moveFile({ + entityId, + pathType, + oldPath: pathType === AssetPathType.PREVIEW ? previewPath : thumbnailPath, + newPath: StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, format), + }); + } + + async moveAssetVideo(asset: AssetEntity) { + return this.moveFile({ + entityId: asset.id, + pathType: AssetPathType.ENCODED_VIDEO, + oldPath: asset.encodedVideoPath, + newPath: StorageCore.getEncodedVideoPath(asset), + }); } async movePersonFile(person: PersonEntity, pathType: PersonPathType) { @@ -294,11 +281,11 @@ export class StorageCore { case AssetPathType.ORIGINAL: { return this.assetRepository.update({ id, originalPath: newPath }); } - case AssetPathType.JPEG_THUMBNAIL: { - return this.assetRepository.update({ id, resizePath: newPath }); + case AssetPathType.PREVIEW: { + return this.assetRepository.update({ id, previewPath: newPath }); } - case AssetPathType.WEBP_THUMBNAIL: { - return this.assetRepository.update({ id, webpPath: newPath }); + case AssetPathType.THUMBNAIL: { + return this.assetRepository.update({ id, thumbnailPath: newPath }); } case AssetPathType.ENCODED_VIDEO: { return this.assetRepository.update({ id, encodedVideoPath: newPath }); diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 01bfacc9b..3a1ea47bb 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -10,6 +10,7 @@ import { AudioCodec, CQMode, Colorspace, + ImageFormat, LogLevel, SystemConfig, SystemConfigEntity, @@ -112,9 +113,11 @@ export const defaults = Object.freeze({ hashVerificationEnabled: true, template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, - thumbnail: { - webpSize: 250, - jpegSize: 1440, + image: { + thumbnailFormat: ImageFormat.WEBP, + thumbnailSize: 250, + previewFormat: ImageFormat.JPEG, + previewSize: 1440, quality: 80, colorspace: Colorspace.P3, }, diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index cf16a99a2..bdda36d15 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -82,7 +82,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As type: entity.type, thumbhash: entity.thumbhash?.toString('base64') ?? null, localDateTime: entity.localDateTime, - resized: !!entity.resizePath, + resized: !!entity.previewPath, duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, @@ -100,7 +100,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As type: entity.type, originalPath: entity.originalPath, originalFileName: entity.originalFileName, - resized: !!entity.resizePath, + resized: !!entity.previewPath, thumbhash: entity.thumbhash?.toString('base64') ?? null, fileCreatedAt: entity.fileCreatedAt, fileModifiedAt: entity.fileModifiedAt, diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts new file mode 100644 index 000000000..ecd62785f --- /dev/null +++ b/server/src/dtos/memory.dto.ts @@ -0,0 +1,84 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { MemoryEntity, MemoryType } from 'src/entities/memory.entity'; +import { ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; + +class MemoryBaseDto { + @ValidateBoolean({ optional: true }) + isSaved?: boolean; + + @ValidateDate({ optional: true }) + seenAt?: Date; +} + +class OnThisDayDto { + @IsInt() + @IsPositive() + year!: number; +} + +type MemoryData = OnThisDayDto; + +export class MemoryUpdateDto extends MemoryBaseDto { + @ValidateDate({ optional: true }) + memoryAt?: Date; +} + +export class MemoryCreateDto extends MemoryBaseDto { + @IsEnum(MemoryType) + @ApiProperty({ enum: MemoryType, enumName: 'MemoryType' }) + type!: MemoryType; + + @IsObject() + @ValidateNested() + @Type((options) => { + switch (options?.object.type) { + case MemoryType.ON_THIS_DAY: { + return OnThisDayDto; + } + + default: { + return Object; + } + } + }) + data!: MemoryData; + + @ValidateDate() + memoryAt!: Date; + + @ValidateUUID({ optional: true, each: true }) + assetIds?: string[]; +} + +export class MemoryResponseDto { + id!: string; + createdAt!: Date; + updatedAt!: Date; + deletedAt?: Date; + memoryAt!: Date; + seenAt?: Date; + ownerId!: string; + type!: MemoryType; + data!: MemoryData; + isSaved!: boolean; + assets!: AssetResponseDto[]; +} + +export const mapMemory = (entity: MemoryEntity): MemoryResponseDto => { + return { + id: entity.id, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + deletedAt: entity.deletedAt, + memoryAt: entity.memoryAt, + seenAt: entity.seenAt, + ownerId: entity.ownerId, + type: entity.type, + data: entity.data, + isSaved: entity.isSaved, + assets: entity.assets.map((asset) => mapAsset(asset)), + }; +}; diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 799baddee..d96ce0d98 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -163,13 +163,25 @@ export class MetadataSearchDto extends BaseSearchDto { @IsString() @IsNotEmpty() @Optional() + @ApiProperty({ deprecated: true }) resizePath?: string; @IsString() @IsNotEmpty() @Optional() + @ApiProperty({ deprecated: true }) webpPath?: string; + @IsString() + @IsNotEmpty() + @Optional() + previewPath?: string; + + @IsString() + @IsNotEmpty() + @Optional() + thumbnailPath?: string; + @IsString() @IsNotEmpty() @Optional() diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 740f1672e..9f80e8d6a 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -22,6 +22,7 @@ import { AudioCodec, CQMode, Colorspace, + ImageFormat, LogLevel, SystemConfig, ToneMapping, @@ -385,18 +386,26 @@ export class SystemConfigThemeDto { customCss!: string; } -class SystemConfigThumbnailDto { - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - webpSize!: number; +class SystemConfigImageDto { + @IsEnum(ImageFormat) + @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + thumbnailFormat!: ImageFormat; @IsInt() @Min(1) @Type(() => Number) @ApiProperty({ type: 'integer' }) - jpegSize!: number; + thumbnailSize!: number; + + @IsEnum(ImageFormat) + @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) + previewFormat!: ImageFormat; + + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + previewSize!: number; @IsInt() @Min(1) @@ -480,10 +489,10 @@ export class SystemConfigDto implements SystemConfig { @IsObject() job!: SystemConfigJobDto; - @Type(() => SystemConfigThumbnailDto) + @Type(() => SystemConfigImageDto) @ValidateNested() @IsObject() - thumbnail!: SystemConfigThumbnailDto; + image!: SystemConfigImageDto; @Type(() => SystemConfigTrashDto) @ValidateNested() diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index 916636115..c977560be 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -67,10 +67,10 @@ export class AssetEntity { originalPath!: string; @Column({ type: 'varchar', nullable: true }) - resizePath!: string | null; + previewPath!: string | null; @Column({ type: 'varchar', nullable: true, default: '' }) - webpPath!: string | null; + thumbnailPath!: string | null; @Column({ type: 'bytea', nullable: true }) thumbhash!: Buffer | null; diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts index 4b568cd9c..761b47693 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -9,6 +9,7 @@ import { AuditEntity } from 'src/entities/audit.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { LibraryEntity } from 'src/entities/library.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; import { MoveEntity } from 'src/entities/move.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; @@ -32,6 +33,7 @@ export const entities = [ AuditEntity, ExifEntity, GeodataPlacesEntity, + MemoryEntity, MoveEntity, PartnerEntity, PersonEntity, diff --git a/server/src/entities/memory.entity.ts b/server/src/entities/memory.entity.ts new file mode 100644 index 000000000..d7dcff4b8 --- /dev/null +++ b/server/src/entities/memory.entity.ts @@ -0,0 +1,67 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { + Column, + CreateDateColumn, + DeleteDateColumn, + Entity, + JoinTable, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +export enum MemoryType { + /** pictures taken on this day X years ago */ + ON_THIS_DAY = 'on_this_day', +} + +export type OnThisDayData = { year: number }; + +export interface MemoryData { + [MemoryType.ON_THIS_DAY]: OnThisDayData; +} + +@Entity('memories') +export class MemoryEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; + + @DeleteDateColumn({ type: 'timestamptz' }) + deletedAt?: Date; + + @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) + owner!: UserEntity; + + @Column() + ownerId!: string; + + @Column() + type!: T; + + @Column({ type: 'jsonb' }) + data!: MemoryData[T]; + + /** unless set to true, will be automatically deleted in the future */ + @Column({ default: false }) + isSaved!: boolean; + + /** memories are sorted in ascending order by this value */ + @Column({ type: 'timestamptz' }) + memoryAt!: Date; + + /** when the user last viewed the memory */ + @Column({ type: 'timestamptz', nullable: true }) + seenAt?: Date; + + @ManyToMany(() => AssetEntity) + @JoinTable() + assets!: AssetEntity[]; +} diff --git a/server/src/entities/move.entity.ts b/server/src/entities/move.entity.ts index de20cb973..f3dad6b28 100644 --- a/server/src/entities/move.entity.ts +++ b/server/src/entities/move.entity.ts @@ -24,8 +24,8 @@ export class MoveEntity { export enum AssetPathType { ORIGINAL = 'original', - JPEG_THUMBNAIL = 'jpeg_thumbnail', - WEBP_THUMBNAIL = 'webp_thumbnail', + PREVIEW = 'preview', + THUMBNAIL = 'thumbnail', ENCODED_VIDEO = 'encoded_video', SIDECAR = 'sidecar', } diff --git a/server/src/entities/system-config.entity.ts b/server/src/entities/system-config.entity.ts index 98b882a36..e07b6d4a2 100644 --- a/server/src/entities/system-config.entity.ts +++ b/server/src/entities/system-config.entity.ts @@ -165,6 +165,11 @@ export enum Colorspace { P3 = 'p3', } +export enum ImageFormat { + JPEG = 'jpeg', + WEBP = 'webp', +} + export enum LogLevel { VERBOSE = 'verbose', DEBUG = 'debug', @@ -249,9 +254,11 @@ export interface SystemConfig { hashVerificationEnabled: boolean; template: string; }; - thumbnail: { - webpSize: number; - jpegSize: number; + image: { + thumbnailFormat: ImageFormat; + thumbnailSize: number; + previewFormat: ImageFormat; + previewSize: number; quality: number; colorspace: Colorspace; }; diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts index 7924a29dd..8b9bdcc4b 100644 --- a/server/src/interfaces/access.interface.ts +++ b/server/src/interfaces/access.interface.ts @@ -32,6 +32,10 @@ export interface IAccessRepository { checkPartnerAccess(userId: string, partnerIds: Set): Promise>; }; + memory: { + checkOwnerAccess(userId: string, memoryIds: Set): Promise>; + }; + person: { checkFaceOwnerAccess(userId: string, assetFaceId: Set): Promise>; checkOwnerAccess(userId: string, personIds: Set): Promise>; diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index d6e5ee1d8..2eca6c167 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -33,9 +33,9 @@ export enum JobName { // thumbnails QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', - GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail', - GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail', - GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail', + GENERATE_PREVIEW = 'generate-preview', + GENERATE_THUMBNAIL = 'generate-thumbnail', + GENERATE_THUMBHASH = 'generate-thumbhash', GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', // metadata @@ -165,9 +165,9 @@ export type JobItem = // Thumbnails | { name: JobName.QUEUE_GENERATE_THUMBNAILS; data: IBaseJob } - | { name: JobName.GENERATE_JPEG_THUMBNAIL; data: IEntityJob } - | { name: JobName.GENERATE_WEBP_THUMBNAIL; data: IEntityJob } - | { name: JobName.GENERATE_THUMBHASH_THUMBNAIL; data: IEntityJob } + | { name: JobName.GENERATE_PREVIEW; data: IEntityJob } + | { name: JobName.GENERATE_THUMBNAIL; data: IEntityJob } + | { name: JobName.GENERATE_THUMBHASH; data: IEntityJob } // User | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob } diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index 0e34227d3..5e51e94a5 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -1,11 +1,11 @@ import { Writable } from 'node:stream'; -import { TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity'; +import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity'; export const IMediaRepository = 'IMediaRepository'; export interface ResizeOptions { size: number; - format: 'webp' | 'jpeg'; + format: ImageFormat; colorspace: string; quality: number; } diff --git a/server/src/interfaces/memory.interface.ts b/server/src/interfaces/memory.interface.ts new file mode 100644 index 000000000..505e1662c --- /dev/null +++ b/server/src/interfaces/memory.interface.ts @@ -0,0 +1,14 @@ +import { MemoryEntity } from 'src/entities/memory.entity'; + +export const IMemoryRepository = 'IMemoryRepository'; + +export interface IMemoryRepository { + search(ownerId: string): Promise; + get(id: string): Promise; + create(memory: Partial): Promise; + update(memory: Partial): Promise; + delete(id: string): Promise; + getAssetIds(id: string, assetIds: string[]): Promise>; + addAssetIds(id: string, assetIds: string[]): Promise; + removeAssetIds(id: string, assetIds: string[]): Promise; +} diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index 1287202ad..771b23e9c 100644 --- a/server/src/interfaces/search.interface.ts +++ b/server/src/interfaces/search.interface.ts @@ -117,8 +117,8 @@ export interface SearchPathOptions { encodedVideoPath?: string; originalFileName?: string; originalPath?: string; - resizePath?: string; - webpPath?: string; + previewPath?: string; + thumbnailPath?: string; } export interface SearchExifOptions { diff --git a/server/src/migrations/1711257900274-RenameWebpJpegPaths.ts b/server/src/migrations/1711257900274-RenameWebpJpegPaths.ts new file mode 100644 index 000000000..ab6f2a4e9 --- /dev/null +++ b/server/src/migrations/1711257900274-RenameWebpJpegPaths.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameWebpJpegPaths1711257900274 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.renameColumn('assets', 'webpPath', 'thumbnailPath'); + await queryRunner.renameColumn('assets', 'resizePath', 'previewPath'); + await queryRunner.query(` + UPDATE system_config + SET key = 'image.previewSize' + WHERE key = 'thumbnail.jpegSize'`); + await queryRunner.query( + `UPDATE system_config + SET key = 'image.thumbnailSize' + WHERE key = 'thumbnail.webpSize'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'image.quality' + WHERE key = 'thumbnail.quality'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'image.colorspace' + WHERE key = 'thumbnail.colorspace'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.renameColumn('assets', 'thumbnailPath', 'webpPath'); + await queryRunner.renameColumn('assets', 'previewPath', 'resizePath'); + await queryRunner.query(` + UPDATE system_config + SET key = 'thumbnail.jpegSize' + WHERE key = 'image.previewSize'`); + await queryRunner.query( + `UPDATE system_config + SET key = 'thumbnail.webpSize' + WHERE key = 'image.thumbnailSize'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'thumbnail.quality' + WHERE key = 'image.quality'`, + ); + await queryRunner.query( + `UPDATE system_config + SET key = 'thumbnail.colorspace' + WHERE key = 'image.colorspace'`, + ); + } +} diff --git a/server/src/migrations/1711637874206-AddMemoryTable.ts b/server/src/migrations/1711637874206-AddMemoryTable.ts new file mode 100644 index 000000000..6309cb508 --- /dev/null +++ b/server/src/migrations/1711637874206-AddMemoryTable.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddMemoryTable1711637874206 implements MigrationInterface { + name = 'AddMemoryTable1711637874206' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "memories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "ownerId" uuid NOT NULL, "type" character varying NOT NULL, "data" jsonb NOT NULL, "isSaved" boolean NOT NULL DEFAULT false, "memoryAt" TIMESTAMP WITH TIME ZONE NOT NULL, "seenAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "PK_aaa0692d9496fe827b0568612f8" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE TABLE "memories_assets_assets" ("memoriesId" uuid NOT NULL, "assetsId" uuid NOT NULL, CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" PRIMARY KEY ("memoriesId", "assetsId"))`); + await queryRunner.query(`CREATE INDEX "IDX_984e5c9ab1f04d34538cd32334" ON "memories_assets_assets" ("memoriesId") `); + await queryRunner.query(`CREATE INDEX "IDX_6942ecf52d75d4273de19d2c16" ON "memories_assets_assets" ("assetsId") `); + await queryRunner.query(`ALTER TABLE "memories" ADD CONSTRAINT "FK_575842846f0c28fa5da46c99b19" FOREIGN KEY ("ownerId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" FOREIGN KEY ("memoriesId") REFERENCES "memories"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f"`); + await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e"`); + await queryRunner.query(`ALTER TABLE "memories" DROP CONSTRAINT "FK_575842846f0c28fa5da46c99b19"`); + await queryRunner.query(`DROP INDEX "public"."IDX_6942ecf52d75d4273de19d2c16"`); + await queryRunner.query(`DROP INDEX "public"."IDX_984e5c9ab1f04d34538cd32334"`); + await queryRunner.query(`DROP TABLE "memories_assets_assets"`); + await queryRunner.query(`DROP TABLE "memories"`); + } + +} diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index a0c4e1927..0e1cab6d0 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -196,6 +196,20 @@ WHERE ) AND ("LibraryEntity"."deletedAt" IS NULL) +-- AccessRepository.memory.checkOwnerAccess +SELECT + "MemoryEntity"."id" AS "MemoryEntity_id" +FROM + "memories" "MemoryEntity" +WHERE + ( + ( + ("MemoryEntity"."id" IN ($1)) + AND ("MemoryEntity"."ownerId" = $2) + ) + ) + AND ("MemoryEntity"."deletedAt" IS NULL) + -- AccessRepository.person.checkOwnerAccess SELECT "PersonEntity"."id" AS "PersonEntity_id" diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index fab0f5376..6f0e8cd5e 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -9,8 +9,8 @@ SELECT "entity"."deviceId" AS "entity_deviceId", "entity"."type" AS "entity_type", "entity"."originalPath" AS "entity_originalPath", - "entity"."resizePath" AS "entity_resizePath", - "entity"."webpPath" AS "entity_webpPath", + "entity"."previewPath" AS "entity_previewPath", + "entity"."thumbnailPath" AS "entity_thumbnailPath", "entity"."thumbhash" AS "entity_thumbhash", "entity"."encodedVideoPath" AS "entity_encodedVideoPath", "entity"."createdAt" AS "entity_createdAt", @@ -67,7 +67,7 @@ WHERE "entity"."ownerId" IN ($1) AND "entity"."isVisible" = true AND "entity"."isArchived" = false - AND "entity"."resizePath" IS NOT NULL + AND "entity"."previewPath" IS NOT NULL AND EXTRACT( DAY FROM @@ -92,8 +92,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -128,8 +128,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -213,8 +213,8 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", - "bd93d5747511a4dad4923546c51365bf1a803774"."resizePath" AS "bd93d5747511a4dad4923546c51365bf1a803774_resizePath", - "bd93d5747511a4dad4923546c51365bf1a803774"."webpPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_webpPath", + "bd93d5747511a4dad4923546c51365bf1a803774"."previewPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_previewPath", + "bd93d5747511a4dad4923546c51365bf1a803774"."thumbnailPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbnailPath", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", "bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt", @@ -294,8 +294,8 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -391,8 +391,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -437,8 +437,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -481,8 +481,8 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -570,8 +570,8 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -629,8 +629,8 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."resizePath" AS "stackedAssets_resizePath", - "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", diff --git a/server/src/queries/memory.repository.sql b/server/src/queries/memory.repository.sql new file mode 100644 index 000000000..aa3df240c --- /dev/null +++ b/server/src/queries/memory.repository.sql @@ -0,0 +1,18 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- MemoryRepository.getAssetIds +SELECT + "memories_assets"."assetsId" AS "assetId" +FROM + "memories_assets_assets" "memories_assets" +WHERE + "memories_assets"."memoriesId" = $1 + AND "memories_assets"."assetsId" IN ($2) + +-- MemoryRepository.removeAssetIds +DELETE FROM "memories_assets_assets" +WHERE + ( + "memoriesId" = $1 + AND "assetsId" IN ($2) + ) diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index b6a513ff9..1cde746d8 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -152,8 +152,8 @@ FROM "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", - "AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", + "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", + "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", @@ -250,8 +250,8 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."resizePath" AS "AssetEntity_resizePath", - "AssetEntity"."webpPath" AS "AssetEntity_webpPath", + "AssetEntity"."previewPath" AS "AssetEntity_previewPath", + "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -380,8 +380,8 @@ SELECT "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath", - "AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath", + "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", + "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index e985a1a6d..3e83d7238 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -14,8 +14,8 @@ FROM "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -45,8 +45,8 @@ FROM "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."resizePath" AS "stackedAssets_resizePath", - "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -110,8 +110,8 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -141,8 +141,8 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."resizePath" AS "stackedAssets_resizePath", - "stackedAssets"."webpPath" AS "stackedAssets_webpPath", + "stackedAssets"."previewPath" AS "stackedAssets_previewPath", + "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -320,8 +320,8 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."resizePath" AS "asset_resizePath", - "asset"."webpPath" AS "asset_webpPath", + "asset"."previewPath" AS "asset_previewPath", + "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 27531cfc9..78581b8ba 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -28,8 +28,8 @@ FROM "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", - "SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", + "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", + "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", @@ -95,8 +95,8 @@ FROM "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."resizePath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_resizePath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."webpPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_webpPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."previewPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_previewPath", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbnailPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbnailPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt", @@ -218,8 +218,8 @@ SELECT "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."resizePath" AS "SharedLinkEntity__SharedLinkEntity_assets_resizePath", - "SharedLinkEntity__SharedLinkEntity_assets"."webpPath" AS "SharedLinkEntity__SharedLinkEntity_assets_webpPath", + "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", + "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 37b5be0e8..fd74eb2ec 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -5,6 +5,7 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { LibraryEntity } from 'src/entities/library.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; @@ -19,6 +20,7 @@ type IAssetAccess = IAccessRepository['asset']; type IAuthDeviceAccess = IAccessRepository['authDevice']; type ILibraryAccess = IAccessRepository['library']; type ITimelineAccess = IAccessRepository['timeline']; +type IMemoryAccess = IAccessRepository['memory']; type IPersonAccess = IAccessRepository['person']; type IPartnerAccess = IAccessRepository['partner']; @@ -345,6 +347,28 @@ class TimelineAccess implements ITimelineAccess { } } +class MemoryAccess implements IMemoryAccess { + constructor(private memoryRepository: Repository) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkOwnerAccess(userId: string, memoryIds: Set): Promise> { + if (memoryIds.size === 0) { + return new Set(); + } + + return this.memoryRepository + .find({ + select: { id: true }, + where: { + id: In([...memoryIds]), + ownerId: userId, + }, + }) + .then((memories) => new Set(memories.map((memory) => memory.id))); + } +} + class PersonAccess implements IPersonAccess { constructor( private assetFaceRepository: Repository, @@ -416,6 +440,7 @@ export class AccessRepository implements IAccessRepository { asset: IAssetAccess; authDevice: IAuthDeviceAccess; library: ILibraryAccess; + memory: IMemoryAccess; person: IPersonAccess; partner: IPartnerAccess; timeline: ITimelineAccess; @@ -425,6 +450,7 @@ export class AccessRepository implements IAccessRepository { @InjectRepository(AssetEntity) assetRepository: Repository, @InjectRepository(AlbumEntity) albumRepository: Repository, @InjectRepository(LibraryEntity) libraryRepository: Repository, + @InjectRepository(MemoryEntity) memoryRepository: Repository, @InjectRepository(PartnerEntity) partnerRepository: Repository, @InjectRepository(PersonEntity) personRepository: Repository, @InjectRepository(AssetFaceEntity) assetFaceRepository: Repository, @@ -436,6 +462,7 @@ export class AccessRepository implements IAccessRepository { this.asset = new AssetAccess(albumRepository, assetRepository, partnerRepository, sharedLinkRepository); this.authDevice = new AuthDeviceAccess(tokenRepository); this.library = new LibraryAccess(libraryRepository); + this.memory = new MemoryAccess(memoryRepository); this.person = new PersonAccess(assetFaceRepository, personRepository); this.partner = new PartnerAccess(partnerRepository); this.timeline = new TimelineAccess(partnerRepository); diff --git a/server/src/repositories/asset-v1.repository.ts b/server/src/repositories/asset-v1.repository.ts index 6f53d820c..229e700fd 100644 --- a/server/src/repositories/asset-v1.repository.ts +++ b/server/src/repositories/asset-v1.repository.ts @@ -66,7 +66,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { getDetectedObjectsByUserId(userId: string): Promise { return this.assetRepository.query( ` - SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId" + SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."previewPath" AS "resizePath", a."deviceAssetId", a."deviceId" FROM assets a LEFT JOIN smart_info si ON a.id = si."assetId" WHERE a."ownerId" = $1 @@ -80,7 +80,7 @@ export class AssetRepositoryV1 implements IAssetRepositoryV1 { getLocationsByUserId(userId: string): Promise { return this.assetRepository.query( ` - SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId" + SELECT DISTINCT ON (e.city) a.id, e.city, a."previewPath" AS "resizePath", a."deviceAssetId", a."deviceId" FROM assets a LEFT JOIN exif e ON a.id = e."assetId" WHERE a."ownerId" = $1 diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 70a986453..a5de078f7 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -83,7 +83,7 @@ export class AssetRepository implements IAssetRepository { `entity.ownerId IN (:...ownerIds) AND entity.isVisible = true AND entity.isArchived = false - AND entity.resizePath IS NOT NULL + AND entity.previewPath IS NOT NULL AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`, { @@ -302,10 +302,10 @@ export class AssetRepository implements IAssetRepository { switch (property) { case WithoutProperty.THUMBNAIL: { where = [ - { resizePath: IsNull(), isVisible: true }, - { resizePath: '', isVisible: true }, - { webpPath: IsNull(), isVisible: true }, - { webpPath: '', isVisible: true }, + { previewPath: IsNull(), isVisible: true }, + { previewPath: '', isVisible: true }, + { thumbnailPath: IsNull(), isVisible: true }, + { thumbnailPath: '', isVisible: true }, { thumbhash: IsNull(), isVisible: true }, ]; break; @@ -339,7 +339,7 @@ export class AssetRepository implements IAssetRepository { }; where = { isVisible: true, - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), smartSearch: { embedding: IsNull(), }, @@ -352,7 +352,7 @@ export class AssetRepository implements IAssetRepository { smartInfo: true, }; where = { - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), isVisible: true, smartInfo: { tags: IsNull(), @@ -367,7 +367,7 @@ export class AssetRepository implements IAssetRepository { jobStatus: true, }; where = { - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), isVisible: true, faces: { assetId: IsNull(), @@ -385,7 +385,7 @@ export class AssetRepository implements IAssetRepository { faces: true, }; where = { - resizePath: Not(IsNull()), + previewPath: Not(IsNull()), isVisible: true, faces: { assetId: Not(IsNull()), diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 4ed114216..336d5df0f 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -13,6 +13,7 @@ import { IJobRepository } from 'src/interfaces/job.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMemoryRepository } from 'src/interfaces/memory.interface'; import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IMetricRepository } from 'src/interfaces/metric.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; @@ -42,6 +43,7 @@ import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; import { MachineLearningRepository } from 'src/repositories/machine-learning.repository'; import { MediaRepository } from 'src/repositories/media.repository'; +import { MemoryRepository } from 'src/repositories/memory.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MetricRepository } from 'src/repositories/metric.repository'; import { MoveRepository } from 'src/repositories/move.repository'; @@ -72,6 +74,7 @@ export const repositories = [ { provide: ILibraryRepository, useClass: LibraryRepository }, { provide: IKeyRepository, useClass: ApiKeyRepository }, { provide: IMachineLearningRepository, useClass: MachineLearningRepository }, + { provide: IMemoryRepository, useClass: MemoryRepository }, { provide: IMetadataRepository, useClass: MetadataRepository }, { provide: IMetricRepository, useClass: MetricRepository }, { provide: IMoveRepository, useClass: MoveRepository }, diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index 0c580b6fe..a865baaeb 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -35,9 +35,9 @@ export const JOBS_TO_QUEUE: Record = { // thumbnails [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_PREVIEW]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_THUMBHASH]: QueueName.THUMBNAIL_GENERATION, [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, // metadata diff --git a/server/src/repositories/memory.repository.ts b/server/src/repositories/memory.repository.ts new file mode 100644 index 000000000..ae8346d00 --- /dev/null +++ b/server/src/repositories/memory.repository.ts @@ -0,0 +1,104 @@ +import { Injectable } from '@nestjs/common'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; +import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { Instrumentation } from 'src/utils/instrumentation'; +import { DataSource, In, Repository } from 'typeorm'; + +@Instrumentation() +@Injectable() +export class MemoryRepository implements IMemoryRepository { + constructor( + @InjectRepository(AssetEntity) private assetRepository: Repository, + @InjectRepository(MemoryEntity) private repository: Repository, + @InjectDataSource() private dataSource: DataSource, + ) {} + + search(ownerId: string): Promise { + return this.repository.find({ + where: { + ownerId, + }, + order: { + memoryAt: 'DESC', + }, + }); + } + + get(id: string): Promise { + return this.repository.findOne({ + where: { + id, + }, + relations: { + assets: true, + }, + }); + } + + create(memory: Partial): Promise { + return this.save(memory); + } + + update(memory: Partial): Promise { + return this.save(memory); + } + + async delete(id: string): Promise { + await this.repository.delete({ id }); + } + + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @ChunkedSet({ paramIndex: 1 }) + async getAssetIds(id: string, assetIds: string[]): Promise> { + if (assetIds.length === 0) { + return new Set(); + } + + const results = await this.dataSource + .createQueryBuilder() + .select('memories_assets.assetsId', 'assetId') + .from('memories_assets_assets', 'memories_assets') + .where('"memories_assets"."memoriesId" = :memoryId', { memoryId: id }) + .andWhere('memories_assets.assetsId IN (:...assetIds)', { assetIds }) + .getRawMany(); + + return new Set(results.map((row) => row['assetId'])); + } + + @GenerateSql({ params: [{ albumId: DummyValue.UUID, assetIds: [DummyValue.UUID] }] }) + async addAssetIds(id: string, assetIds: string[]): Promise { + await this.dataSource + .createQueryBuilder() + .insert() + .into('memories_assets_assets', ['memoriesId', 'assetsId']) + .values(assetIds.map((assetId) => ({ memoriesId: id, assetsId: assetId }))) + .execute(); + } + + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @Chunked({ paramIndex: 1 }) + async removeAssetIds(id: string, assetIds: string[]): Promise { + await this.dataSource + .createQueryBuilder() + .delete() + .from('memories_assets_assets') + .where({ + memoriesId: id, + assetsId: In(assetIds), + }) + .execute(); + } + + private async save(memory: Partial): Promise { + const { id } = await this.repository.save(memory); + return this.repository.findOneOrFail({ + where: { id }, + relations: { + assets: true, + }, + }); + } +} diff --git a/server/src/services/asset-v1.service.spec.ts b/server/src/services/asset-v1.service.spec.ts index 898fb5a99..735ac8322 100644 --- a/server/src/services/asset-v1.service.spec.ts +++ b/server/src/services/asset-v1.service.spec.ts @@ -44,13 +44,13 @@ const _getAsset_1 = () => { asset_1.deviceId = 'device_id_1'; asset_1.type = AssetType.VIDEO; asset_1.originalPath = 'fake_path/asset_1.jpeg'; - asset_1.resizePath = ''; + asset_1.previewPath = ''; asset_1.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.updatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.isFavorite = false; asset_1.isArchived = false; - asset_1.webpPath = ''; + asset_1.thumbnailPath = ''; asset_1.encodedVideoPath = ''; asset_1.duration = '0:00:00.000000'; asset_1.exifInfo = new ExifEntity(); diff --git a/server/src/services/asset-v1.service.ts b/server/src/services/asset-v1.service.ts index a24ddbd69..97aa99d91 100644 --- a/server/src/services/asset-v1.service.ts +++ b/server/src/services/asset-v1.service.ts @@ -247,16 +247,16 @@ export class AssetServiceV1 { private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) { switch (format) { case GetAssetThumbnailFormatEnum.WEBP: { - if (asset.webpPath) { - return asset.webpPath; + if (asset.thumbnailPath) { + return asset.thumbnailPath; } this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`); } case GetAssetThumbnailFormatEnum.JPEG: { - if (!asset.resizePath) { + if (!asset.previewPath) { throw new NotFoundException(`No thumbnail found for asset ${asset.id}`); } - return asset.resizePath; + return asset.previewPath; } } } @@ -268,12 +268,12 @@ export class AssetServiceV1 { * Serve file viewer on the web */ if (dto.isWeb && mimeType != 'image/gif') { - if (!asset.resizePath) { + if (!asset.previewPath) { this.logger.error('Error serving IMAGE asset for web'); throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile'); } - return asset.resizePath; + return asset.previewPath; } /** @@ -283,15 +283,15 @@ export class AssetServiceV1 { return asset.originalPath; } - if (asset.webpPath && asset.webpPath.length > 0) { - return asset.webpPath; + if (asset.thumbnailPath && asset.thumbnailPath.length > 0) { + return asset.thumbnailPath; } - if (!asset.resizePath) { - throw new Error('resizePath not set'); + if (!asset.previewPath) { + throw new Error('previewPath not set'); } - return asset.resizePath; + return asset.previewPath; } private async getLibraryId(auth: AuthDto, libraryId?: string) { diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 683e2f5ae..b90c194aa 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -661,8 +661,8 @@ describe(AssetService.name, () => { name: JobName.DELETE_FILES, data: { files: [ - assetWithFace.webpPath, - assetWithFace.resizePath, + assetWithFace.thumbnailPath, + assetWithFace.previewPath, assetWithFace.encodedVideoPath, assetWithFace.sidecarPath, assetWithFace.originalPath, @@ -745,8 +745,8 @@ describe(AssetService.name, () => { name: JobName.DELETE_FILES, data: { files: [ - assetStub.external.webpPath, - assetStub.external.resizePath, + assetStub.external.thumbnailPath, + assetStub.external.previewPath, assetStub.external.encodedVideoPath, assetStub.external.sidecarPath, ], @@ -828,9 +828,7 @@ describe(AssetService.name, () => { it('should run the refresh thumbnails job', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }), - expect(jobMock.queueAll).toHaveBeenCalledWith([ - { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } }, - ]); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }]); }); it('should run the transcode video', async () => { diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 556883afd..135377e0b 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -399,7 +399,7 @@ export class AssetService { await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } }); } - const files = [asset.webpPath, asset.resizePath, asset.encodedVideoPath, asset.sidecarPath]; + const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath, asset.sidecarPath]; if (!fromExternal) { files.push(asset.originalPath); } @@ -472,7 +472,7 @@ export class AssetService { } case AssetJobName.REGENERATE_THUMBNAIL: { - jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id } }); + jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id } }); break; } diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index ff5e0d9c7..d40167429 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -95,13 +95,13 @@ export class AuditService { break; } - case AssetPathType.JPEG_THUMBNAIL: { - await this.assetRepository.update({ id, resizePath: pathValue }); + case AssetPathType.PREVIEW: { + await this.assetRepository.update({ id, previewPath: pathValue }); break; } - case AssetPathType.WEBP_THUMBNAIL: { - await this.assetRepository.update({ id, webpPath: pathValue }); + case AssetPathType.THUMBNAIL: { + await this.assetRepository.update({ id, thumbnailPath: pathValue }); break; } @@ -174,8 +174,8 @@ export class AuditService { const orphans: FileReportItemDto[] = []; for await (const assets of pagination) { assetCount += assets.length; - for (const { id, originalPath, resizePath, encodedVideoPath, webpPath, isExternal, checksum } of assets) { - for (const file of [originalPath, resizePath, encodedVideoPath, webpPath]) { + for (const { id, originalPath, previewPath, encodedVideoPath, thumbnailPath, isExternal, checksum } of assets) { + for (const file of [originalPath, previewPath, encodedVideoPath, thumbnailPath]) { track(file); } @@ -191,14 +191,14 @@ export class AuditService { ) { orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); } - if (resizePath && !hasFile(thumbFiles, resizePath)) { - orphans.push({ ...entity, pathType: AssetPathType.JPEG_THUMBNAIL, pathValue: resizePath }); + if (previewPath && !hasFile(thumbFiles, previewPath)) { + orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewPath }); } - if (webpPath && !hasFile(thumbFiles, webpPath)) { - orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: webpPath }); + if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) { + orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailPath }); } if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { - orphans.push({ ...entity, pathType: AssetPathType.WEBP_THUMBNAIL, pathValue: encodedVideoPath }); + orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath }); } } } diff --git a/server/src/services/index.ts b/server/src/services/index.ts index 5a97d16fe..3c903c927 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -11,6 +11,7 @@ import { DownloadService } from 'src/services/download.service'; import { JobService } from 'src/services/job.service'; import { LibraryService } from 'src/services/library.service'; import { MediaService } from 'src/services/media.service'; +import { MemoryService } from 'src/services/memory.service'; import { MetadataService } from 'src/services/metadata.service'; import { MicroservicesService } from 'src/services/microservices.service'; import { PartnerService } from 'src/services/partner.service'; @@ -42,6 +43,7 @@ export const services = [ JobService, LibraryService, MediaService, + MemoryService, MetadataService, PartnerService, PersonService, diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index aa5739878..ac0e502ae 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -279,7 +279,7 @@ describe(JobService.name, () => { }, { item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1', source: 'upload' } }, - jobs: [JobName.GENERATE_JPEG_THUMBNAIL], + jobs: [JobName.GENERATE_PREVIEW], }, { item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1' } }, @@ -290,24 +290,24 @@ describe(JobService.name, () => { jobs: [], }, { - item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1' } }, - jobs: [JobName.GENERATE_WEBP_THUMBNAIL, JobName.GENERATE_THUMBHASH_THUMBNAIL], + item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }, + jobs: [JobName.GENERATE_THUMBNAIL, JobName.GENERATE_THUMBHASH], }, { - item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-1', source: 'upload' } }, + item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1', source: 'upload' } }, jobs: [ - JobName.GENERATE_WEBP_THUMBNAIL, - JobName.GENERATE_THUMBHASH_THUMBNAIL, + JobName.GENERATE_THUMBNAIL, + JobName.GENERATE_THUMBHASH, JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION, ], }, { - item: { name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: 'asset-live-image', source: 'upload' } }, + item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-live-image', source: 'upload' } }, jobs: [ - JobName.GENERATE_WEBP_THUMBNAIL, - JobName.GENERATE_THUMBHASH_THUMBNAIL, + JobName.GENERATE_THUMBNAIL, + JobName.GENERATE_THUMBHASH, JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION, @@ -329,7 +329,7 @@ describe(JobService.name, () => { for (const { item, jobs } of tests) { it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => { - if (item.name === JobName.GENERATE_JPEG_THUMBNAIL && item.data.source === 'upload') { + if (item.name === JobName.GENERATE_PREVIEW && item.data.source === 'upload') { if (item.data.id === 'asset-live-image') { assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]); } else { diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index c03b7c7bc..3f9cd8a22 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -245,7 +245,7 @@ export class JobService { case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { if (item.data.source === 'upload') { - await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: item.data }); + await this.jobRepository.queue({ name: JobName.GENERATE_PREVIEW, data: item.data }); } break; } @@ -259,10 +259,10 @@ export class JobService { break; } - case JobName.GENERATE_JPEG_THUMBNAIL: { + case JobName.GENERATE_PREVIEW: { const jobs: JobItem[] = [ - { name: JobName.GENERATE_WEBP_THUMBNAIL, data: item.data }, - { name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: item.data }, + { name: JobName.GENERATE_THUMBNAIL, data: item.data }, + { name: JobName.GENERATE_THUMBHASH, data: item.data }, ]; if (item.data.source === 'upload') { @@ -282,7 +282,7 @@ export class JobService { break; } - case JobName.GENERATE_WEBP_THUMBNAIL: { + case JobName.GENERATE_THUMBNAIL: { if (item.data.source !== 'upload') { break; } diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 4397730ab..3a650430e 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -4,6 +4,7 @@ import { ExifEntity } from 'src/entities/exif.entity'; import { AudioCodec, Colorspace, + ImageFormat, SystemConfigKey, ToneMapping, TranscodeHWAccel, @@ -78,7 +79,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).not.toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_JPEG_THUMBNAIL, + name: JobName.GENERATE_PREVIEW, data: { id: assetStub.image.id }, }, ]); @@ -136,7 +137,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_JPEG_THUMBNAIL, + name: JobName.GENERATE_PREVIEW, data: { id: assetStub.image.id }, }, ]); @@ -160,7 +161,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_WEBP_THUMBNAIL, + name: JobName.GENERATE_THUMBNAIL, data: { id: assetStub.image.id }, }, ]); @@ -184,7 +185,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBHASH_THUMBNAIL, + name: JobName.GENERATE_THUMBHASH, data: { id: assetStub.image.id }, }, ]); @@ -193,10 +194,10 @@ describe(MediaService.name, () => { }); }); - describe('handleGenerateJpegThumbnail', () => { + describe('handleGeneratePreview', () => { it('should skip thumbnail generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(mediaMock.resize).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); @@ -204,25 +205,29 @@ describe(MediaService.name, () => { it('should skip video thumbnail generation if no video stream', async () => { mediaMock.probe.mockResolvedValue(probeStub.noVideoStreams); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(mediaMock.resize).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); it('should generate a thumbnail for an image', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.jpeg', { - size: 1440, - format: 'jpeg', - quality: 80, - colorspace: Colorspace.SRGB, - }); + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + { + size: 1440, + format: ImageFormat.JPEG, + quality: 80, + colorspace: Colorspace.SRGB, + }, + ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -230,30 +235,34 @@ describe(MediaService.name, () => { assetMock.getByIds.mockResolvedValue([ { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, ]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.jpeg', { - size: 1440, - format: 'jpeg', - quality: 80, - colorspace: Colorspace.P3, - }); + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + { + size: 1440, + format: ImageFormat.JPEG, + quality: 80, + colorspace: Colorspace.P3, + }, + ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); it('should generate a thumbnail for a video', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); + await sut.handleGeneratePreview({ id: assetStub.video.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id.jpeg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', { inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'], outputOptions: [ @@ -266,19 +275,19 @@ describe(MediaService.name, () => { ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); it('should tonemap thumbnail for hdr video', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); + await sut.handleGeneratePreview({ id: assetStub.video.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id.jpeg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', { inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'], outputOptions: [ @@ -291,7 +300,7 @@ describe(MediaService.name, () => { ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - resizePath: 'upload/thumbs/user-id/as/se/asset-id.jpeg', + previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -302,11 +311,11 @@ describe(MediaService.name, () => { { key: SystemConfigKey.FFMPEG_MAX_BITRATE, value: '5000k' }, ]); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.video.id }); + await sut.handleGeneratePreview({ id: assetStub.video.id }); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', - 'upload/thumbs/user-id/as/se/asset-id.jpeg', + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', { inputOptions: ['-ss 00:00:00', '-sws_flags accurate_rnd+bitexact+full_chroma_int'], outputOptions: [ @@ -321,31 +330,35 @@ describe(MediaService.name, () => { it('should run successfully', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGenerateJpegThumbnail({ id: assetStub.image.id }); + await sut.handleGeneratePreview({ id: assetStub.image.id }); }); }); - describe('handleGenerateWebpThumbnail', () => { + describe('handleGenerateThumbnail', () => { it('should skip thumbnail generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); expect(mediaMock.resize).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); it('should generate a thumbnail', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.webp', { - format: 'webp', - size: 250, - quality: 80, - colorspace: Colorspace.SRGB, - }); + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.SRGB, + }, + ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp', + thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', }); }); }); @@ -354,31 +367,35 @@ describe(MediaService.name, () => { assetMock.getByIds.mockResolvedValue([ { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, ]); - await sut.handleGenerateWebpThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnail({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/as/se/asset-id.webp', { - format: 'webp', - size: 250, - quality: 80, - colorspace: Colorspace.P3, - }); + expect(mediaMock.resize).toHaveBeenCalledWith( + '/original/path.jpg', + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + colorspace: Colorspace.P3, + }, + ); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', - webpPath: 'upload/thumbs/user-id/as/se/asset-id.webp', + thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', }); }); describe('handleGenerateThumbhashThumbnail', () => { it('should skip thumbhash generation if asset not found', async () => { assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateThumbhashThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbhash({ id: assetStub.image.id }); expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); }); it('should skip thumbhash generation if resize path is missing', async () => { assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]); - await sut.handleGenerateThumbhashThumbnail({ id: assetStub.noResizePath.id }); + await sut.handleGenerateThumbhash({ id: assetStub.noResizePath.id }); expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); }); @@ -387,7 +404,7 @@ describe(MediaService.name, () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer); - await sut.handleGenerateThumbhashThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbhash({ id: assetStub.image.id }); expect(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 26aa2dce9..c56fd26e6 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; -import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetEntity, AssetType } from 'src/entities/asset.entity'; @@ -7,6 +7,7 @@ import { AssetPathType } from 'src/entities/move.entity'; import { AudioCodec, Colorspace, + ImageFormat, TranscodeHWAccel, TranscodePolicy, TranscodeTarget, @@ -81,15 +82,15 @@ export class MediaService { const jobs: JobItem[] = []; for (const asset of assets) { - if (!asset.resizePath || force) { - jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id: asset.id } }); + if (!asset.previewPath || force) { + jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } }); continue; } - if (!asset.webpPath) { - jobs.push({ name: JobName.GENERATE_WEBP_THUMBNAIL, data: { id: asset.id } }); + if (!asset.thumbnailPath) { + jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } }); } if (!asset.thumbhash) { - jobs.push({ name: JobName.GENERATE_THUMBHASH_THUMBNAIL, data: { id: asset.id } }); + jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } }); } } @@ -152,41 +153,41 @@ export class MediaService { } async handleAssetMigration({ id }: IEntityJob): Promise { + const { image } = await this.configCore.getConfig(); const [asset] = await this.assetRepository.getByIds([id]); if (!asset) { return JobStatus.FAILED; } - await this.storageCore.moveAssetFile(asset, AssetPathType.JPEG_THUMBNAIL); - await this.storageCore.moveAssetFile(asset, AssetPathType.WEBP_THUMBNAIL); - await this.storageCore.moveAssetFile(asset, AssetPathType.ENCODED_VIDEO); + await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat); + await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); + await this.storageCore.moveAssetVideo(asset); return JobStatus.SUCCESS; } - async handleGenerateJpegThumbnail({ id }: IEntityJob): Promise { + async handleGeneratePreview({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset) { return JobStatus.FAILED; } - const resizePath = await this.generateThumbnail(asset, 'jpeg'); - await this.assetRepository.update({ id: asset.id, resizePath }); + const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, ImageFormat.JPEG); + await this.assetRepository.update({ id: asset.id, previewPath }); return JobStatus.SUCCESS; } - private async generateThumbnail(asset: AssetEntity, format: 'jpeg' | 'webp') { - const { thumbnail, ffmpeg } = await this.configCore.getConfig(); - const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize; - const path = - format === 'jpeg' ? StorageCore.getLargeThumbnailPath(asset) : StorageCore.getSmallThumbnailPath(asset); + private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) { + const { image, ffmpeg } = await this.configCore.getConfig(); + const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize; + const path = StorageCore.getImagePath(asset, type, format); this.storageCore.ensureFolders(path); switch (asset.type) { case AssetType.IMAGE: { - const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : thumbnail.colorspace; - const thumbnailOptions = { format, size, colorspace, quality: thumbnail.quality }; - await this.mediaRepository.resize(asset.originalPath, path, thumbnailOptions); + const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; + const imageOptions = { format, size, colorspace, quality: image.quality }; + await this.mediaRepository.resize(asset.originalPath, path, imageOptions); break; } @@ -214,24 +215,24 @@ export class MediaService { return path; } - async handleGenerateWebpThumbnail({ id }: IEntityJob): Promise { + async handleGenerateThumbnail({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); if (!asset) { return JobStatus.FAILED; } - const webpPath = await this.generateThumbnail(asset, 'webp'); - await this.assetRepository.update({ id: asset.id, webpPath }); + const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, ImageFormat.WEBP); + await this.assetRepository.update({ id: asset.id, thumbnailPath }); return JobStatus.SUCCESS; } - async handleGenerateThumbhashThumbnail({ id }: IEntityJob): Promise { + async handleGenerateThumbhash({ id }: IEntityJob): Promise { const [asset] = await this.assetRepository.getByIds([id]); - if (!asset?.resizePath) { + if (!asset?.previewPath) { return JobStatus.FAILED; } - const thumbhash = await this.mediaRepository.generateThumbhash(asset.resizePath); + const thumbhash = await this.mediaRepository.generateThumbhash(asset.previewPath); await this.assetRepository.update({ id: asset.id, thumbhash }); return JobStatus.SUCCESS; diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts new file mode 100644 index 000000000..5f045ffde --- /dev/null +++ b/server/src/services/memory.service.spec.ts @@ -0,0 +1,214 @@ +import { BadRequestException } from '@nestjs/common'; +import { MemoryType } from 'src/entities/memory.entity'; +import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { MemoryService } from 'src/services/memory.service'; +import { authStub } from 'test/fixtures/auth.stub'; +import { memoryStub } from 'test/fixtures/memory.stub'; +import { userStub } from 'test/fixtures/user.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newMemoryRepositoryMock } from 'test/repositories/memory.repository.mock'; + +describe(MemoryService.name, () => { + let accessMock: IAccessRepositoryMock; + let memoryMock: jest.Mocked; + let sut: MemoryService; + + beforeEach(() => { + accessMock = newAccessRepositoryMock(); + memoryMock = newMemoryRepositoryMock(); + + sut = new MemoryService(accessMock, memoryMock); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe('search', () => { + it('should search memories', async () => { + memoryMock.search.mockResolvedValue([memoryStub.memory1, memoryStub.empty]); + await expect(sut.search(authStub.admin)).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 'memory1', assets: expect.any(Array) }), + expect.objectContaining({ id: 'memoryEmpty', assets: [] }), + ]), + ); + }); + + it('should map ', async () => { + await expect(sut.search(authStub.admin)).resolves.toEqual([]); + }); + }); + + describe('get', () => { + it('should throw an error when no access', async () => { + await expect(sut.get(authStub.admin, 'not-found')).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should throw an error when the memory is not found', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['race-condition'])); + await expect(sut.get(authStub.admin, 'race-condition')).rejects.toBeInstanceOf(BadRequestException); + }); + + it('should get a memory by id', async () => { + memoryMock.get.mockResolvedValue(memoryStub.memory1); + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + await expect(sut.get(authStub.admin, 'memory1')).resolves.toMatchObject({ id: 'memory1' }); + expect(memoryMock.get).toHaveBeenCalledWith('memory1'); + expect(accessMock.memory.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['memory1'])); + }); + }); + + describe('create', () => { + it('should skip assets the user does not have access to', async () => { + memoryMock.create.mockResolvedValue(memoryStub.empty); + await expect( + sut.create(authStub.admin, { + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + assetIds: ['not-mine'], + memoryAt: new Date(2024), + }), + ).resolves.toMatchObject({ assets: [] }); + expect(memoryMock.create).toHaveBeenCalledWith(expect.objectContaining({ assets: [] })); + }); + + it('should create a memory', async () => { + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1'])); + memoryMock.create.mockResolvedValue(memoryStub.memory1); + await expect( + sut.create(authStub.admin, { + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + assetIds: ['asset1'], + memoryAt: new Date(2024), + }), + ).resolves.toBeDefined(); + expect(memoryMock.create).toHaveBeenCalledWith( + expect.objectContaining({ + ownerId: userStub.admin.id, + assets: [{ id: 'asset1' }], + }), + ); + }); + + it('should create a memory without assets', async () => { + memoryMock.create.mockResolvedValue(memoryStub.memory1); + await expect( + sut.create(authStub.admin, { + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + memoryAt: new Date(2024), + }), + ).resolves.toBeDefined(); + }); + }); + + describe('update', () => { + it('should require access', async () => { + await expect(sut.update(authStub.admin, 'not-found', { isSaved: true })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(memoryMock.update).not.toHaveBeenCalled(); + }); + + it('should update a memory', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + memoryMock.update.mockResolvedValue(memoryStub.memory1); + await expect(sut.update(authStub.admin, 'memory1', { isSaved: true })).resolves.toBeDefined(); + expect(memoryMock.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'memory1', + isSaved: true, + }), + ); + }); + }); + + describe('remove', () => { + it('should require access', async () => { + await expect(sut.remove(authStub.admin, 'not-found')).rejects.toBeInstanceOf(BadRequestException); + expect(memoryMock.delete).not.toHaveBeenCalled(); + }); + + it('should delete a memory', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + await expect(sut.remove(authStub.admin, 'memory1')).resolves.toBeUndefined(); + expect(memoryMock.delete).toHaveBeenCalledWith('memory1'); + }); + }); + + describe('addAssets', () => { + it('should require memory access', async () => { + await expect(sut.addAssets(authStub.admin, 'not-found', { ids: ['asset1'] })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(memoryMock.addAssetIds).not.toHaveBeenCalled(); + }); + + it('should require asset access', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + memoryMock.get.mockResolvedValue(memoryStub.memory1); + await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['not-found'] })).resolves.toEqual([ + { error: 'no_permission', id: 'not-found', success: false }, + ]); + expect(memoryMock.addAssetIds).not.toHaveBeenCalled(); + }); + + it('should skip assets already in the memory', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + memoryMock.get.mockResolvedValue(memoryStub.memory1); + memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1'])); + await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([ + { error: 'duplicate', id: 'asset1', success: false }, + ]); + expect(memoryMock.addAssetIds).not.toHaveBeenCalled(); + }); + + it('should add assets', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1'])); + memoryMock.get.mockResolvedValue(memoryStub.memory1); + await expect(sut.addAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([ + { id: 'asset1', success: true }, + ]); + expect(memoryMock.addAssetIds).toHaveBeenCalledWith('memory1', ['asset1']); + }); + }); + + describe('removeAssets', () => { + it('should require memory access', async () => { + await expect(sut.removeAssets(authStub.admin, 'not-found', { ids: ['asset1'] })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(memoryMock.removeAssetIds).not.toHaveBeenCalled(); + }); + + it('should skip assets not in the memory', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['not-found'] })).resolves.toEqual([ + { error: 'not_found', id: 'not-found', success: false }, + ]); + expect(memoryMock.removeAssetIds).not.toHaveBeenCalled(); + }); + + it('should require asset access', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1'])); + await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([ + { error: 'no_permission', id: 'asset1', success: false }, + ]); + expect(memoryMock.removeAssetIds).not.toHaveBeenCalled(); + }); + + it('should remove assets', async () => { + accessMock.memory.checkOwnerAccess.mockResolvedValue(new Set(['memory1'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1'])); + memoryMock.getAssetIds.mockResolvedValue(new Set(['asset1'])); + await expect(sut.removeAssets(authStub.admin, 'memory1', { ids: ['asset1'] })).resolves.toEqual([ + { id: 'asset1', success: true }, + ]); + expect(memoryMock.removeAssetIds).toHaveBeenCalledWith('memory1', ['asset1']); + }); + }); +}); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts new file mode 100644 index 000000000..a73eb3ec0 --- /dev/null +++ b/server/src/services/memory.service.ts @@ -0,0 +1,105 @@ +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { AccessCore, Permission } from 'src/cores/access.core'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { addAssets, removeAssets } from 'src/utils/asset.util'; + +@Injectable() +export class MemoryService { + private access: AccessCore; + + constructor( + @Inject(IAccessRepository) private accessRepository: IAccessRepository, + @Inject(IMemoryRepository) private repository: IMemoryRepository, + ) { + this.access = AccessCore.create(accessRepository); + } + + async search(auth: AuthDto) { + const memories = await this.repository.search(auth.user.id); + return memories.map((memory) => mapMemory(memory)); + } + + async get(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + const memory = await this.findOrFail(id); + return mapMemory(memory); + } + + async create(auth: AuthDto, dto: MemoryCreateDto) { + // TODO validate type/data combination + + const assetIds = dto.assetIds || []; + const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, assetIds); + const memory = await this.repository.create({ + ownerId: auth.user.id, + type: dto.type, + data: dto.data, + isSaved: dto.isSaved, + memoryAt: dto.memoryAt, + seenAt: dto.seenAt, + assets: [...allowedAssetIds].map((id) => ({ id }) as AssetEntity), + }); + + return mapMemory(memory); + } + + async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id); + + const memory = await this.repository.update({ + id, + isSaved: dto.isSaved, + memoryAt: dto.memoryAt, + seenAt: dto.seenAt, + }); + + return mapMemory(memory); + } + + async remove(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_DELETE, id); + await this.repository.delete(id); + } + + async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + + const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const results = await addAssets(auth, repos, { id, assetIds: dto.ids }); + + const hasSuccess = results.find(({ success }) => success); + if (hasSuccess) { + await this.repository.update({ id, updatedAt: new Date() }); + } + + return results; + } + + async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { + await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id); + + const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const permissions = [Permission.ASSET_SHARE]; + const results = await removeAssets(auth, repos, { id, assetIds: dto.ids, permissions }); + + const hasSuccess = results.find(({ success }) => success); + if (hasSuccess) { + await this.repository.update({ id, updatedAt: new Date() }); + } + + return results; + } + + private async findOrFail(id: string) { + const memory = await this.repository.get(id); + if (!memory) { + throw new BadRequestException('Memory not found'); + } + return memory; + } +} diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts index 07ef3f35f..f420b3e50 100644 --- a/server/src/services/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -53,9 +53,9 @@ export class MicroservicesService { [JobName.MIGRATE_ASSET]: (data) => this.mediaService.handleAssetMigration(data), [JobName.MIGRATE_PERSON]: (data) => this.personService.handlePersonMigration(data), [JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data), - [JobName.GENERATE_JPEG_THUMBNAIL]: (data) => this.mediaService.handleGenerateJpegThumbnail(data), - [JobName.GENERATE_WEBP_THUMBNAIL]: (data) => this.mediaService.handleGenerateWebpThumbnail(data), - [JobName.GENERATE_THUMBHASH_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbhashThumbnail(data), + [JobName.GENERATE_PREVIEW]: (data) => this.mediaService.handleGeneratePreview(data), + [JobName.GENERATE_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbnail(data), + [JobName.GENERATE_THUMBHASH]: (data) => this.mediaService.handleGenerateThumbhash(data), [JobName.QUEUE_VIDEO_CONVERSION]: (data) => this.mediaService.handleQueueVideoConversion(data), [JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data), [JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataService.handleQueueMetadataExtraction(data), diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 10e42e1b6..501154c1d 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -645,7 +645,7 @@ describe(PersonService.name, () => { expect(machineLearningMock.detectFaces).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', { - imagePath: assetStub.image.resizePath, + imagePath: assetStub.image.previewPath, }, { enabled: true, diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 504716a55..d2bc81b0e 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -23,6 +23,7 @@ import { } from 'src/dtos/person.dto'; import { PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { ImageFormat } from 'src/entities/system-config.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @@ -315,17 +316,17 @@ export class PersonService { }, }; const [asset] = await this.assetRepository.getByIds([id], relations); - if (!asset || !asset.resizePath || asset.faces?.length > 0) { + if (!asset || !asset.previewPath || asset.faces?.length > 0) { return JobStatus.FAILED; } const faces = await this.machineLearningRepository.detectFaces( machineLearning.url, - { imagePath: asset.resizePath }, + { imagePath: asset.previewPath }, machineLearning.facialRecognition, ); - this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`); + this.logger.debug(`${faces.length} faces detected in ${asset.previewPath}`); this.logger.verbose(faces.map((face) => ({ ...face, embedding: `vector(${face.embedding.length})` }))); if (faces.length > 0) { @@ -470,7 +471,7 @@ export class PersonService { } async handleGeneratePersonThumbnail(data: IEntityJob): Promise { - const { machineLearning, thumbnail } = await this.configCore.getConfig(); + const { machineLearning, image } = await this.configCore.getConfig(); if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) { return JobStatus.SKIPPED; } @@ -496,7 +497,7 @@ export class PersonService { } = face; const [asset] = await this.assetRepository.getByIds([assetId]); - if (!asset?.resizePath) { + if (!asset?.previewPath) { return JobStatus.FAILED; } this.logger.verbose(`Cropping face for person: ${person.id}`); @@ -527,12 +528,12 @@ export class PersonService { height: newHalfSize * 2, }; - const croppedOutput = await this.mediaRepository.crop(asset.resizePath, cropOptions); + const croppedOutput = await this.mediaRepository.crop(asset.previewPath, cropOptions); const thumbnailOptions = { - format: 'jpeg', + format: ImageFormat.JPEG, size: FACE_THUMBNAIL_SIZE, - colorspace: thumbnail.colorspace, - quality: thumbnail.quality, + colorspace: image.colorspace, + quality: image.quality, } as const; await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 03fa154a3..9422dac86 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -76,6 +76,9 @@ export class SearchService { checksum = Buffer.from(dto.checksum, encoding); } + dto.previewPath ??= dto.resizePath; + dto.thumbnailPath ??= dto.webpPath; + const page = dto.page ?? 1; const size = dto.size || 250; const enumToOrder = { [AssetOrder.ASC]: 'ASC', [AssetOrder.DESC]: 'DESC' } as const; diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 968aeac5f..2e1dfbafc 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -18,7 +18,7 @@ import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.r const asset = { id: 'asset-1', - resizePath: 'path/to/resize.ext', + previewPath: 'path/to/resize.ext', } as AssetEntity; describe(SmartInfoService.name, () => { @@ -94,7 +94,7 @@ describe(SmartInfoService.name, () => { }); it('should skip assets without a resize path', async () => { - const asset = { resizePath: '' } as AssetEntity; + const asset = { previewPath: '' } as AssetEntity; assetMock.getByIds.mockResolvedValue([asset]); await sut.handleEncodeClip({ id: asset.id }); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 183c45b80..f9d36c238 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -83,13 +83,13 @@ export class SmartInfoService { return JobStatus.FAILED; } - if (!asset.resizePath) { + if (!asset.previewPath) { return JobStatus.FAILED; } const clipEmbedding = await this.machineLearning.encodeImage( machineLearning.url, - { imagePath: asset.resizePath }, + { imagePath: asset.previewPath }, machineLearning.clip, ); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index b2079f606..4bb5dd0a1 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -4,6 +4,7 @@ import { AudioCodec, CQMode, Colorspace, + ImageFormat, LogLevel, SystemConfig, SystemConfigEntity, @@ -119,9 +120,11 @@ const updatedConfig = Object.freeze({ hashVerificationEnabled: true, template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, - thumbnail: { - webpSize: 250, - jpegSize: 1440, + image: { + thumbnailFormat: ImageFormat.WEBP, + thumbnailSize: 250, + previewFormat: ImageFormat.JPEG, + previewSize: 1440, quality: 80, colorspace: Colorspace.P3, }, diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 7ec677756..be0eb8fa6 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -58,7 +58,7 @@ export function searchAssetBuilder( builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); } - const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'resizePath', 'webpPath']); + const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'previewPath', 'thumbnailPath']); builder.andWhere(_.omitBy(path, _.isUndefined)); if (options.originalFileName) { diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index fc94a363a..0b2ff82a3 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -26,10 +26,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_123.jpg', - resizePath: null, + previewPath: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -62,10 +62,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_456.jpg', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -102,10 +102,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -139,10 +139,10 @@ export const assetStub = { ownerId: 'admin-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - resizePath: '/uploads/admin-id/thumbs/path.jpg', + previewPath: '/uploads/admin-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/admin-id/webp/path.ext', + thumbnailPath: '/uploads/admin-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -184,10 +184,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -224,10 +224,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -264,10 +264,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -304,10 +304,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -344,10 +344,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2015-02-23T05:06:29.716Z'), @@ -385,10 +385,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - webpPath: null, + thumbnailPath: null, thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -456,10 +456,10 @@ export const assetStub = { deviceId: 'device-id', checksum: Buffer.from('file hash', 'utf8'), originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', sidecarPath: null, type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-22T05:06:29.716Z'), @@ -499,11 +499,11 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -535,11 +535,11 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -572,11 +572,11 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: null, + thumbnailPath: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -610,10 +610,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - resizePath: '/uploads/user-id/thumbs/path.ext', + previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - webpPath: null, + thumbnailPath: null, thumbhash: null, encodedVideoPath: '/encoded/video/path.mp4', createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -648,10 +648,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -687,10 +687,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - resizePath: '/uploads/user-id/thumbs/path.jpg', + previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - webpPath: '/uploads/user-id/webp/path.ext', + thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), diff --git a/server/test/fixtures/memory.stub.ts b/server/test/fixtures/memory.stub.ts new file mode 100644 index 000000000..bb84a8f1d --- /dev/null +++ b/server/test/fixtures/memory.stub.ts @@ -0,0 +1,30 @@ +import { MemoryEntity, MemoryType } from 'src/entities/memory.entity'; +import { assetStub } from 'test/fixtures/asset.stub'; +import { userStub } from 'test/fixtures/user.stub'; + +export const memoryStub = { + empty: { + id: 'memoryEmpty', + createdAt: new Date(), + updatedAt: new Date(), + memoryAt: new Date(2024), + ownerId: userStub.admin.id, + owner: userStub.admin, + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + isSaved: false, + assets: [], + }, + memory1: { + id: 'memory1', + createdAt: new Date(), + updatedAt: new Date(), + memoryAt: new Date(2024), + ownerId: userStub.admin.id, + owner: userStub.admin, + type: MemoryType.ON_THIS_DAY, + data: { year: 2024 }, + isSaved: false, + assets: [assetStub.image1], + }, +}; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 34e3da515..ccd76c328 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -199,7 +199,7 @@ export const sharedLinkStub = { deviceId: 'device_id_1', type: AssetType.VIDEO, originalPath: 'fake_path/jpeg', - resizePath: '', + previewPath: '', checksum: Buffer.from('file hash', 'utf8'), fileModifiedAt: today, fileCreatedAt: today, @@ -219,7 +219,7 @@ export const sharedLinkStub = { objects: ['a', 'b', 'c'], asset: null as any, }, - webpPath: '', + thumbnailPath: '', thumbhash: null, encodedVideoPath: '', duration: null, diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index a61451277..fe7de7c83 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -8,6 +8,7 @@ export interface IAccessRepositoryMock { authDevice: jest.Mocked; library: jest.Mocked; timeline: jest.Mocked; + memory: jest.Mocked; person: jest.Mocked; partner: jest.Mocked; } @@ -49,6 +50,10 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), }, + memory: { + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), + }, + person: { checkFaceOwnerAccess: jest.fn().mockResolvedValue(new Set()), checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), diff --git a/server/test/repositories/memory.repository.mock.ts b/server/test/repositories/memory.repository.mock.ts new file mode 100644 index 000000000..85b17a198 --- /dev/null +++ b/server/test/repositories/memory.repository.mock.ts @@ -0,0 +1,14 @@ +import { IMemoryRepository } from 'src/interfaces/memory.interface'; + +export const newMemoryRepositoryMock = (): jest.Mocked => { + return { + search: jest.fn().mockResolvedValue([]), + get: jest.fn(), + create: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + getAssetIds: jest.fn().mockResolvedValue(new Set()), + addAssetIds: jest.fn(), + removeAssetIds: jest.fn(), + }; +}; diff --git a/web/Dockerfile b/web/Dockerfile index 422ad9746..c59b6c716 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:iron-alpine3.18@sha256:876514790dabd49fae7d9c4dfbba027954bd91d8e7d36da76334466533bc6b0c +FROM node:iron-alpine3.18@sha256:fa5d3cf51725bd42d32e67917623038539dbe720dab082f590785c001eb4dfef RUN apk add --no-cache tini USER node diff --git a/web/README.md b/web/README.md new file mode 100644 index 000000000..e9693ceb0 --- /dev/null +++ b/web/README.md @@ -0,0 +1,5 @@ +# Immich web project + +This project uses the [SvelteKit](https://kit.svelte.dev/) web framework. Please refer to [the SvelteKit docs](https://kit.svelte.dev/docs) for information on getting started as a contributor to this project. In particular, it will help you navigate the project's code if you understand the basics of [SvelteKit routing](https://kit.svelte.dev/docs/routing). + +When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [../server](the server project). diff --git a/web/package-lock.json b/web/package-lock.json index 5fb9aa66b..a00c329b3 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1825,9 +1825,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.4.tgz", - "integrity": "sha512-eDxK2d4EGzk99QsZNoPXe7jlzA5EGqfcCpUwZ912bhnalsZ2ZsG5wGRthkydupVjYyqdmzEanVKFhLxU2vkPSQ==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.5.tgz", + "integrity": "sha512-ULe3PB00q4+wYRL+IS5FDPsCEVnhEITofm7b9Yz8malcH3r1SAnW/JJ6T13hIMeu8QNRIuVQWo+P4+2VklbnLQ==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -2264,16 +2264,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.3.1.tgz", - "integrity": "sha512-STEDMVQGww5lhCuNXVSQfbfuNII5E08QWkvAw5Qwf+bj2WT+JkG1uc+5/vXA3AOYMDHVOSpL+9rcbEUiHIm2dw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", + "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/type-utils": "7.3.1", - "@typescript-eslint/utils": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/type-utils": "7.4.0", + "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -2332,15 +2332,15 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.3.1.tgz", - "integrity": "sha512-Rq49+pq7viTRCH48XAbTA+wdLRrB/3sRq4Lpk0oGDm0VmnjBrAOVXH/Laalmwsv2VpekiEfVFwJYVk6/e8uvQw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", + "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4" }, "engines": { @@ -2360,13 +2360,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.3.1.tgz", - "integrity": "sha512-fVS6fPxldsKY2nFvyT7IP78UO1/I2huG+AYu5AMjCT9wtl6JFiDnsv4uad4jQ0GTFzcUV5HShVeN96/17bTBag==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", + "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1" + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2377,13 +2377,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.3.1.tgz", - "integrity": "sha512-iFhaysxFsMDQlzJn+vr3OrxN8NmdQkHks4WaqD4QBnt5hsq234wcYdyQ9uquzJJIDAj5W4wQne3yEsYA6OmXGw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", + "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.3.1", - "@typescript-eslint/utils": "7.3.1", + "@typescript-eslint/typescript-estree": "7.4.0", + "@typescript-eslint/utils": "7.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -2404,9 +2404,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.3.1.tgz", - "integrity": "sha512-2tUf3uWggBDl4S4183nivWQ2HqceOZh1U4hhu4p1tPiIJoRRXrab7Y+Y0p+dozYwZVvLPRI6r5wKe9kToF9FIw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", + "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2417,13 +2417,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.3.1.tgz", - "integrity": "sha512-tLpuqM46LVkduWP7JO7yVoWshpJuJzxDOPYIVWUUZbW+4dBpgGeUdl/fQkhuV0A8eGnphYw3pp8d2EnvPOfxmQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", + "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/visitor-keys": "7.3.1", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/visitor-keys": "7.4.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2502,17 +2502,17 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.3.1.tgz", - "integrity": "sha512-jIERm/6bYQ9HkynYlNZvXpzmXWZGhMbrOvq3jJzOSOlKXsVjrrolzWBjDW6/TvT5Q3WqaN4EkmcfdQwi9tDjBQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", + "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.3.1", - "@typescript-eslint/types": "7.3.1", - "@typescript-eslint/typescript-estree": "7.3.1", + "@typescript-eslint/scope-manager": "7.4.0", + "@typescript-eslint/types": "7.4.0", + "@typescript-eslint/typescript-estree": "7.4.0", "semver": "^7.5.4" }, "engines": { @@ -2560,12 +2560,12 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.3.1.tgz", - "integrity": "sha512-9RMXwQF8knsZvfv9tdi+4D/j7dMG28X/wMJ8Jj6eOHyHWwDW4ngQJcqEczSsqIKKjFiLFr40Mnr7a5ulDD3vmw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", + "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.3.1", + "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2803,9 +2803,9 @@ "dev": true }, "node_modules/@zoom-image/core": { - "version": "0.33.1", - "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.33.1.tgz", - "integrity": "sha512-RHzehi287J7SpPV3bHjHcTYMus5u2DlarvcCQJjy1O+Nh4PT8DCg+jiyhCd0SDkClWbppyrqtoDbSbogJpo85g==", + "version": "0.33.2", + "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.33.2.tgz", + "integrity": "sha512-Nqg/JrvtaScXJ16L7dcPQjun3F0enajULtokYWy13VtETqqBOBqKDa3feTJH7JXrYvEs/w6e4AU56UvzGG1wXA==", "dependencies": { "@namnode/store": "^0.1.0" }, @@ -2815,11 +2815,11 @@ } }, "node_modules/@zoom-image/svelte": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.7.tgz", - "integrity": "sha512-CyrP4NloVrA/w6Dzucdw0bNxZLmJ7dTFQ9dMu7KsIPuIf9KYGKPbWF/IBDPQGmxnDK+BB+0EGFW8411JyAP94g==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.8.tgz", + "integrity": "sha512-z+xCyMHIeTmSYZYdDde/EAz08odlBWMv6jmHOcz95DMt3sJ/+vlVtUEMCzZfuK2KqV8v59EKZHAeutMzTx3QPg==", "dependencies": { - "@zoom-image/core": "0.33.1" + "@zoom-image/core": "0.33.2" }, "funding": { "type": "github", @@ -5677,9 +5677,9 @@ } }, "node_modules/jiti": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.20.0.tgz", - "integrity": "sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==", + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", "dev": true, "bin": { "jiti": "bin/jiti.js" @@ -8223,9 +8223,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -8236,7 +8236,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -8295,19 +8295,25 @@ } }, "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", "dev": true, "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", "dev": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } @@ -8671,13 +8677,13 @@ } }, "node_modules/vite": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.3.tgz", - "integrity": "sha512-+i1oagbvkVIhEy9TnEV+fgXsng13nZM90JQbrcPrf6DvW2mXARlz+DK7DLiDP+qeKoD1FCVx/1SpFL1CLq9Mhw==", + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz", + "integrity": "sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==", "dev": true, "dependencies": { "esbuild": "^0.20.1", - "postcss": "^8.4.36", + "postcss": "^8.4.38", "rollup": "^4.13.0" }, "bin": { diff --git a/web/src/app.css b/web/src/app.css index c361d890c..9db440e46 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -29,6 +29,7 @@ src: url('$lib/assets/fonts/overpass/Overpass.ttf') format('truetype-variations'); font-weight: 1 999; font-style: normal; + ascent-override: 100%; } @font-face { @@ -36,6 +37,7 @@ src: url('$lib/assets/fonts/overpass/OverpassMono.ttf') format('truetype-variations'); font-weight: 1 999; font-style: monospace; + ascent-override: 100%; } @font-face { diff --git a/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte similarity index 75% rename from web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte rename to web/src/lib/components/admin-page/settings/image/image-settings.svelte index 8e2936b55..dcf59936d 100644 --- a/web/src/lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -25,10 +25,10 @@
(config.thumbnail.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)} - isEdited={config.thumbnail.colorspace !== savedConfig.thumbnail.colorspace} + checked={config.image.colorspace === Colorspace.P3} + on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)} + isEdited={config.image.colorspace !== savedConfig.image.colorspace} />
dispatch('reset', { ...detail, configKeys: ['thumbnail'] })} - on:save={() => dispatch('save', { thumbnail: config.thumbnail })} + on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['image'] })} + on:save={() => dispatch('save', { image: config.image })} showResetToDefault={!isEqual(savedConfig, defaultConfig)} {disabled} /> diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index 5f60d2b40..b96974e0e 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -66,13 +66,7 @@ {#if !selectedRemoveUser} - dispatch('close')}> - - -

Options

-
-
- +
diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index b7c0b53b6..d7b7cc514 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -13,7 +13,6 @@ import { createEventDispatcher, onMount } from 'svelte'; import Button from '../elements/buttons/button.svelte'; import BaseModal from '../shared-components/base-modal.svelte'; - import ImmichLogo from '../shared-components/immich-logo.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte'; export let album: AlbumResponseDto; @@ -55,14 +54,7 @@ }; - dispatch('close')}> - - - -

Invite to album

-
-
- + {#if selectedUsers.length > 0}

To

diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 1772b8641..abcb248f1 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -161,6 +161,16 @@ }} /> {/if} + + {#if !isOwner && showDownloadButton} + dispatch('download')} + title="Download" + /> + {/if} + {#if showDetailButton} {/if} + {#if isOwner} import Icon from '$lib/components/elements/icon.svelte'; + import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; + import FocusTrap from '$lib/components/shared-components/focus-trap.svelte'; import { AssetAction, ProjectionType } from '$lib/constants'; import { updateNumberOfComments } from '$lib/stores/activity.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; @@ -9,7 +11,7 @@ import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; import { user } from '$lib/stores/user.store'; - import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils'; + import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils'; import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { shortcuts } from '$lib/utils/shortcut'; @@ -30,7 +32,6 @@ type ActivityResponseDto, type AlbumResponseDto, type AssetResponseDto, - type SharedLinkResponseDto, } from '@immich/sdk'; import { mdiChevronLeft, mdiChevronRight, mdiImageBrokenVariant } from '@mdi/js'; import { createEventDispatcher, onDestroy, onMount } from 'svelte'; @@ -49,14 +50,11 @@ import PhotoViewer from './photo-viewer.svelte'; import SlideshowBar from './slideshow-bar.svelte'; import VideoViewer from './video-viewer.svelte'; - import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; - import FocusTrap from '$lib/components/shared-components/focus-trap.svelte'; export let assetStore: AssetStore | null = null; export let asset: AssetResponseDto; export let preloadAssets: AssetResponseDto[] = []; export let showNavigation = true; - export let sharedLink: SharedLinkResponseDto | undefined = undefined; $: isTrashEnabled = $featureFlags.trash; export let withStacked = false; export let isShared = false; @@ -64,7 +62,7 @@ let reactions: ActivityResponseDto[] = []; - const { setAssetId } = assetViewingStore; + const { setAsset } = assetViewingStore; const { restartProgress: restartSlideshowProgress, stopProgress: stopSlideshowProgress, @@ -86,6 +84,7 @@ let addToSharedAlbum = true; let shouldPlayMotionPhoto = false; let isShowProfileImageCrop = false; + let sharedLink = getSharedLink(); let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline; let shouldShowDetailButton = asset.hasMetadata; let shouldShowShareModal = !asset.isTrashed; @@ -194,7 +193,7 @@ slideshowStateUnsubscribe = slideshowState.subscribe((value) => { if (value === SlideshowState.PlaySlideshow) { slideshowHistory.reset(); - slideshowHistory.queue(asset.id); + slideshowHistory.queue(asset); handlePromiseError(handlePlaySlideshow()); } else if (value === SlideshowState.StopSlideshow) { handlePromiseError(handleStopSlideshow()); @@ -204,7 +203,7 @@ shuffleSlideshowUnsubscribe = slideshowNavigation.subscribe((value) => { if (value === SlideshowNavigation.Shuffle) { slideshowHistory.reset(); - slideshowHistory.queue(asset.id); + slideshowHistory.queue(asset); } }); @@ -264,7 +263,6 @@ }; const handleCloseViewer = () => { - $isShowDetail = false; closeViewer(); }; @@ -280,9 +278,9 @@ return; } - slideshowHistory.queue(asset.id); + slideshowHistory.queue(asset); - await setAssetId(asset.id); + setAsset(asset); $restartSlideshowProgress = true; }; @@ -301,9 +299,7 @@ if ($slideshowState === SlideshowState.PlaySlideshow && assetStore) { const hasNext = - order === 'previous' - ? await assetStore.getPreviousAssetId(asset.id) - : await assetStore.getNextAssetId(asset.id); + order === 'previous' ? await assetStore.getPreviousAsset(asset.id) : await assetStore.getNextAsset(asset.id); if (hasNext) { $restartSlideshowProgress = true; } else { @@ -443,8 +439,8 @@ let assetViewerHtmlElement: HTMLElement; - const slideshowHistory = new SlideshowHistory((assetId: string) => { - handlePromiseError(setAssetId(assetId)); + const slideshowHistory = new SlideshowHistory((asset) => { + setAsset(asset); $restartSlideshowProgress = true; }); @@ -737,7 +733,6 @@ on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)} on:album={({ detail }) => handleAddToAlbum(detail)} on:close={() => (isShowAlbumPicker = false)} - on:escape={() => (isShowAlbumPicker = false)} /> {/if} @@ -751,19 +746,11 @@ {/if} {#if isShowProfileImageCrop} - (isShowProfileImageCrop = false)} - on:escape={() => (isShowProfileImageCrop = false)} - /> + (isShowProfileImageCrop = false)} /> {/if} {#if isShowShareModal} - (isShowShareModal = false)} - on:escape={() => (isShowShareModal = false)} - /> + (isShowShareModal = false)} /> {/if}
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 318007280..565bca401 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -200,7 +200,7 @@ disabled={!isOwner || isSharedLink()} bind:this={textArea} class="max-h-[500px] - w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary" + w-full resize-none border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary immich-scrollbar" placeholder={isOwner ? 'Add a description' : ''} on:focusout={handleFocusOut} on:input={() => autoGrowHeight(textArea)} diff --git a/web/src/lib/components/photos-page/actions/create-shared-link.svelte b/web/src/lib/components/photos-page/actions/create-shared-link.svelte index b3e68d303..ef9121ced 100644 --- a/web/src/lib/components/photos-page/actions/create-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/create-shared-link.svelte @@ -2,26 +2,14 @@ import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; import { mdiShareVariantOutline } from '@mdi/js'; - import { createEventDispatcher } from 'svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte'; let showModal = false; - const dispatch = createEventDispatcher<{ - escape: void; - }>(); const { getAssets } = getAssetControlContext(); - const escape = () => { - dispatch('escape'); - showModal = false; - }; (showModal = true)} /> {#if showModal} - id)} - on:close={() => (showModal = false)} - on:escape={escape} - /> + id)} on:close={() => (showModal = false)} /> {/if} diff --git a/web/src/lib/components/photos-page/actions/stack-action.svelte b/web/src/lib/components/photos-page/actions/stack-action.svelte index 1df6cce30..a857da1dd 100644 --- a/web/src/lib/components/photos-page/actions/stack-action.svelte +++ b/web/src/lib/components/photos-page/actions/stack-action.svelte @@ -1,13 +1,8 @@ diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index e3af25984..a42f8dc78 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -80,17 +80,13 @@ }); } - const assetClickHandler = async ( - asset: AssetResponseDto, - assetsInDateGroup: AssetResponseDto[], - groupTitle: string, - ) => { + const assetClickHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => { if (isSelectionMode || $isMultiSelectState) { assetSelectHandler(asset, assetsInDateGroup, groupTitle); return; } - await assetViewingStore.setAssetId(asset.id); + assetViewingStore.setAsset(asset); }; const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets }); diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 53847c581..9aca51fa0 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -18,6 +18,7 @@ import Scrollbar from '../shared-components/scrollbar/scrollbar.svelte'; import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; import AssetDateGroup from './asset-date-group.svelte'; + import { stackAssets } from '$lib/utils/asset-utils'; import DeleteAssetDialog from './delete-asset-dialog.svelte'; import { handlePromiseError } from '$lib/utils'; import { selectAllAssets } from '$lib/utils/asset-utils'; @@ -85,6 +86,13 @@ handlePromiseError(trashOrDelete(true)); }; + const onStackAssets = async () => { + await stackAssets(Array.from($selectedAssets), (ids) => { + assetStore.removeAssets(ids); + dispatch('escape'); + }); + }; + $: shortcutList = (() => { if ($isSearchEnabled || $showAssetViewer) { return []; @@ -102,6 +110,7 @@ { shortcut: { key: 'Delete' }, onShortcut: onDelete }, { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete }, { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() }, + { shortcut: { key: 's' }, onShortcut: () => onStackAssets() }, ); } @@ -128,26 +137,22 @@ } const handlePrevious = async () => { - const previousAsset = await assetStore.getPreviousAssetId($viewingAsset.id); + const previousAsset = await assetStore.getPreviousAsset($viewingAsset.id); if (previousAsset) { - const preloadId = await assetStore.getPreviousAssetId(previousAsset); - preloadId - ? await assetViewingStore.setAssetId(previousAsset, [preloadId]) - : await assetViewingStore.setAssetId(previousAsset); + const preloadAsset = await assetStore.getPreviousAsset(previousAsset.id); + assetViewingStore.setAsset(previousAsset, preloadAsset ? [preloadAsset] : []); } return !!previousAsset; }; const handleNext = async () => { - const nextAsset = await assetStore.getNextAssetId($viewingAsset.id); + const nextAsset = await assetStore.getNextAsset($viewingAsset.id); if (nextAsset) { - const preloadId = await assetStore.getNextAssetId(nextAsset); - preloadId - ? await assetViewingStore.setAssetId(nextAsset, [preloadId]) - : await assetViewingStore.setAssetId(nextAsset); + const preloadAsset = await assetStore.getNextAsset(nextAsset.id); + assetViewingStore.setAsset(nextAsset, preloadAsset ? [preloadAsset] : []); } return !!nextAsset; diff --git a/web/src/lib/components/shared-components/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection-modal.svelte index 9e3caf00e..f86ede716 100644 --- a/web/src/lib/components/shared-components/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection-modal.svelte @@ -43,17 +43,16 @@ const handleNew = () => { dispatch('newAlbum', search.length > 0 ? search : ''); }; + + const getTitle = () => { + if (shared) { + return 'Add to shared album'; + } + return 'Add to album'; + }; - - - -

- Add to {shared ? 'Shared ' : ''}Album -

-
-
- +
{#if loading} {#each { length: 3 } as _} @@ -69,11 +68,9 @@
{/each} {:else} -
diff --git a/web/src/lib/components/shared-components/base-modal.svelte b/web/src/lib/components/shared-components/base-modal.svelte index c5bf5380a..c09653d6e 100644 --- a/web/src/lib/components/shared-components/base-modal.svelte +++ b/web/src/lib/components/shared-components/base-modal.svelte @@ -7,12 +7,26 @@ import { clickOutside } from '$lib/utils/click-outside'; import { mdiClose } from '@mdi/js'; import FocusTrap from '$lib/components/shared-components/focus-trap.svelte'; + import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; + import Icon from '$lib/components/elements/icon.svelte'; const dispatch = createEventDispatcher<{ - escape: void; close: void; }>(); + /** + * Unique identifier for the modal. + */ + export let id: string; + export let title: string; export let zIndex = 9999; + /** + * If true, the logo will be displayed next to the modal title. + */ + export let showLogo = false; + /** + * Optional icon to display next to the modal title, if `showLogo` is false. + */ + export let icon: string | undefined = undefined; onMount(() => { if (browser) { @@ -36,7 +50,8 @@
dispatch('close'), - onEscape: () => dispatch('escape'), + onEscape: () => dispatch('close'), }} - class="max-h-[800px] min-h-[200px] w-[450px] overflow-y-auto rounded-lg bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar" + class="max-h-[800px] min-h-[200px] w-[450px] overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar" tabindex="-1" >
-
- -

Modal Title

-
+
+ {#if showLogo} + + {:else if icon} + + {/if} +

+ {title} +

dispatch('close')} icon={mdiClose} size={'20'} title="Close" /> diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 801cb9f3e..9e05af625 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -143,20 +143,16 @@ handleError(error, 'Failed to edit shared link'); } }; + + const getTitle = () => { + if (editingLink) { + return 'Edit link'; + } + return 'Create link to share'; + }; - dispatch('close')} on:escape={() => dispatch('escape')}> - - - - {#if editingLink} -

Edit link

- {:else} -

Create link to share

- {/if} -
-
- +
{#if shareType === SharedLinkType.Album} {#if !editingLink} @@ -241,11 +237,11 @@ {#if !sharedLink} {#if editingLink}
- +
{:else}
- +
{/if} {:else} diff --git a/web/src/lib/components/shared-components/profile-image-cropper.svelte b/web/src/lib/components/shared-components/profile-image-cropper.svelte index 36cb6152f..de583e0a0 100644 --- a/web/src/lib/components/shared-components/profile-image-cropper.svelte +++ b/web/src/lib/components/shared-components/profile-image-cropper.svelte @@ -71,12 +71,7 @@ }; - - - -

Set profile picture

-
-
+
- dispatch('close')}> - - - -

Add partner

-
-
- +
{#if availableUsers.length > 0} {#each availableUsers as user} diff --git a/web/src/lib/stores/asset-viewing.store.ts b/web/src/lib/stores/asset-viewing.store.ts index 0416d4903..bb3214995 100644 --- a/web/src/lib/stores/asset-viewing.store.ts +++ b/web/src/lib/stores/asset-viewing.store.ts @@ -7,24 +7,17 @@ function createAssetViewingStore() { const preloadAssets = writable([]); const viewState = writable(false); - const setAssetId = async (id: string, preloadIds?: string[]) => { - const data = await getAssetInfo({ id, key: getKey() }); - - if (preloadIds) { - const preloadList = []; - for (const preloadId of preloadIds) { - if (preloadId) { - const preloadAsset = await getAssetInfo({ id: preloadId, key: getKey() }); - preloadList.push(preloadAsset); - } - } - preloadAssets.set(preloadList); - } - - viewingAssetStoreState.set(data); + const setAsset = (asset: AssetResponseDto, assetsToPreload: AssetResponseDto[] = []) => { + preloadAssets.set(assetsToPreload); + viewingAssetStoreState.set(asset); viewState.set(true); }; + const setAssetId = async (id: string) => { + const asset = await getAssetInfo({ id, key: getKey() }); + setAsset(asset); + }; + const showAssetViewer = (show: boolean) => { viewState.set(show); }; @@ -40,6 +33,7 @@ function createAssetViewingStore() { subscribe: viewState.subscribe, set: viewState.set, }, + setAsset, setAssetId, showAssetViewer, }; diff --git a/web/src/lib/stores/asset.store.spec.ts b/web/src/lib/stores/asset.store.spec.ts index 55f3fe1ff..e2f702fba 100644 --- a/web/src/lib/stores/asset.store.spec.ts +++ b/web/src/lib/stores/asset.store.spec.ts @@ -286,7 +286,7 @@ describe('AssetStore', () => { }); }); - describe('getPreviousAssetId', () => { + describe('getPreviousAsset', () => { let assetStore: AssetStore; const bucketAssets: Record = { '2024-03-01T00:00:00.000Z': assetFactory.buildList(1), @@ -307,15 +307,15 @@ describe('AssetStore', () => { }); it('returns null for invalid assetId', async () => { - expect(() => assetStore.getPreviousAssetId('invalid')).not.toThrow(); - expect(await assetStore.getPreviousAssetId('invalid')).toBeNull(); + expect(() => assetStore.getPreviousAsset('invalid')).not.toThrow(); + expect(await assetStore.getPreviousAsset('invalid')).toBeNull(); }); it('returns previous assetId', async () => { await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible); const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z'); - expect(await assetStore.getPreviousAssetId(bucket!.assets[1].id)).toEqual(bucket!.assets[0].id); + expect(await assetStore.getPreviousAsset(bucket!.assets[1].id)).toEqual(bucket!.assets[0]); }); it('returns previous assetId spanning multiple buckets', async () => { @@ -324,7 +324,7 @@ describe('AssetStore', () => { const bucket = assetStore.getBucketByDate('2024-02-01T00:00:00.000Z'); const previousBucket = assetStore.getBucketByDate('2024-03-01T00:00:00.000Z'); - expect(await assetStore.getPreviousAssetId(bucket!.assets[0].id)).toEqual(previousBucket!.assets[0].id); + expect(await assetStore.getPreviousAsset(bucket!.assets[0].id)).toEqual(previousBucket!.assets[0]); }); it('loads previous bucket', async () => { @@ -333,7 +333,7 @@ describe('AssetStore', () => { const loadBucketSpy = vi.spyOn(assetStore, 'loadBucket'); const bucket = assetStore.getBucketByDate('2024-02-01T00:00:00.000Z'); const previousBucket = assetStore.getBucketByDate('2024-03-01T00:00:00.000Z'); - expect(await assetStore.getPreviousAssetId(bucket!.assets[0].id)).toEqual(previousBucket!.assets[0].id); + expect(await assetStore.getPreviousAsset(bucket!.assets[0].id)).toEqual(previousBucket!.assets[0]); expect(loadBucketSpy).toBeCalledTimes(1); }); @@ -344,12 +344,12 @@ describe('AssetStore', () => { const [assetOne, assetTwo, assetThree] = assetStore.assets; assetStore.removeAssets([assetTwo.id]); - expect(await assetStore.getPreviousAssetId(assetThree.id)).toEqual(assetOne.id); + expect(await assetStore.getPreviousAsset(assetThree.id)).toEqual(assetOne); }); it('returns null when no more assets', async () => { await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible); - expect(await assetStore.getPreviousAssetId(assetStore.assets[0].id)).toBeNull(); + expect(await assetStore.getPreviousAsset(assetStore.assets[0].id)).toBeNull(); }); }); diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 4cc92ab86..019a70401 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -451,7 +451,7 @@ export class AssetStore { this.emit(true); } - async getPreviousAssetId(assetId: string): Promise { + async getPreviousAsset(assetId: string): Promise { const info = this.getBucketInfoForAssetId(assetId); if (!info) { return null; @@ -460,7 +460,7 @@ export class AssetStore { const { bucket, assetIndex, bucketIndex } = info; if (assetIndex !== 0) { - return bucket.assets[assetIndex - 1].id; + return bucket.assets[assetIndex - 1]; } if (bucketIndex === 0) { @@ -469,10 +469,10 @@ export class AssetStore { const previousBucket = this.buckets[bucketIndex - 1]; await this.loadBucket(previousBucket.bucketDate, BucketPosition.Unknown); - return previousBucket.assets.at(-1)?.id || null; + return previousBucket.assets.at(-1) || null; } - async getNextAssetId(assetId: string): Promise { + async getNextAsset(assetId: string): Promise { const info = this.getBucketInfoForAssetId(assetId); if (!info) { return null; @@ -481,7 +481,7 @@ export class AssetStore { const { bucket, assetIndex, bucketIndex } = info; if (assetIndex !== bucket.assets.length - 1) { - return bucket.assets[assetIndex + 1].id; + return bucket.assets[assetIndex + 1]; } if (bucketIndex === this.buckets.length - 1) { @@ -490,7 +490,7 @@ export class AssetStore { const nextBucket = this.buckets[bucketIndex + 1]; await this.loadBucket(nextBucket.bucketDate, BucketPosition.Unknown); - return nextBucket.assets[0]?.id || null; + return nextBucket.assets[0] || null; } triggerUpdate() { diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 299f04505..e6c21b70a 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -10,6 +10,7 @@ import { linkOAuthAccount, startOAuth, unlinkOAuthAccount, + type SharedLinkResponseDto, type UserResponseDto, } from '@immich/sdk'; import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js'; @@ -129,14 +130,12 @@ export const getJobName = (jobName: JobName) => { }; let _key: string | undefined; +let _sharedLink: SharedLinkResponseDto | undefined; -export const setKey = (key: string) => { - _key = key; -}; - -export const getKey = (): string | undefined => { - return _key; -}; +export const setKey = (key: string) => (_key = key); +export const getKey = (): string | undefined => _key; +export const setSharedLink = (sharedLink: SharedLinkResponseDto) => (_sharedLink = sharedLink); +export const getSharedLink = (): SharedLinkResponseDto | undefined => _sharedLink; export const isSharedLink = () => { return !!_key; diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 532479912..a4255b75d 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -11,6 +11,7 @@ import { createAlbum, defaults, getDownloadInfo, + updateAssets, type AssetResponseDto, type AssetTypeEnum, type DownloadInfoDto, @@ -270,6 +271,44 @@ export const getSelectedAssets = (assets: Set, user: UserRespo return ids; }; +export async function stackAssets(assets: Array, onStack: (ds: string[]) => void) { + try { + const parent = assets.at(0); + if (!parent) { + return; + } + + const children = assets.slice(1); + const ids = children.map(({ id }) => id); + + if (children.length > 0) { + await updateAssets({ assetBulkUpdateDto: { ids, stackParentId: parent.id } }); + } + + let childrenCount = parent.stackCount || 1; + for (const asset of children) { + asset.stackParentId = parent.id; + // Add grand-children's count to new parent + childrenCount += asset.stackCount || 1; + // Reset children stack info + asset.stackCount = null; + asset.stack = []; + } + + parent.stackCount = childrenCount; + + notificationController.show({ + message: `Stacked ${ids.length + 1} assets`, + type: NotificationType.Info, + timeout: 1500, + }); + + onStack(ids); + } catch (error) { + handleError(error, `Unable to stack`); + } +} + export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { if (get(isSelectingAllAssets)) { // Selection is already ongoing diff --git a/web/src/lib/utils/slideshow-history.ts b/web/src/lib/utils/slideshow-history.ts index 8b34359d0..b9d5fdea7 100644 --- a/web/src/lib/utils/slideshow-history.ts +++ b/web/src/lib/utils/slideshow-history.ts @@ -1,16 +1,18 @@ +import type { AssetResponseDto } from '@immich/sdk'; + export class SlideshowHistory { - private history: string[] = []; + private history: AssetResponseDto[] = []; private index = 0; - constructor(private onChange: (assetId: string) => void) {} + constructor(private onChange: (asset: AssetResponseDto) => void) {} reset() { this.history = []; this.index = 0; } - queue(assetId: string) { - this.history.push(assetId); + queue(asset: AssetResponseDto) { + this.history.push(asset); // If we were at the end of the slideshow history, move the index to the new end if (this.index === this.history.length - 2) { diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index d4b665bc0..5f76012dd 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -82,7 +82,7 @@ export let data: PageData; - let { isViewing: showAssetViewer, setAssetId } = assetViewingStore; + let { isViewing: showAssetViewer, setAsset } = assetViewingStore; let { slideshowState, slideshowNavigation } = slideshowStore; $: album = data.album; @@ -231,7 +231,7 @@ const asset = $slideshowNavigation === SlideshowNavigation.Shuffle ? await assetStore.getRandomAsset() : assetStore.assets[0]; if (asset) { - await setAssetId(asset.id); + setAsset(asset); $slideshowState = SlideshowState.PlaySlideshow; } }; diff --git a/web/src/routes/(user)/photos/+page.svelte b/web/src/routes/(user)/photos/+page.svelte index ffc30d656..00409cdc1 100644 --- a/web/src/routes/(user)/photos/+page.svelte +++ b/web/src/routes/(user)/photos/+page.svelte @@ -53,7 +53,7 @@ assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()} > - (handleEscapeKey = true)} /> + diff --git a/web/src/routes/(user)/share/[key]/+page.svelte b/web/src/routes/(user)/share/[key]/+page.svelte index 1b87063da..28ebbc6c8 100644 --- a/web/src/routes/(user)/share/[key]/+page.svelte +++ b/web/src/routes/(user)/share/[key]/+page.svelte @@ -9,6 +9,7 @@ import { handleError } from '$lib/utils/handle-error'; import { getMySharedLink, SharedLinkType } from '@immich/sdk'; import type { PageData } from './$types'; + import { setSharedLink } from '$lib/utils'; export let data: PageData; let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = data; @@ -19,6 +20,7 @@ const handlePasswordSubmit = async () => { try { sharedLink = await getMySharedLink({ password, key }); + setSharedLink(sharedLink); passwordRequired = false; isOwned = $user ? $user.id === sharedLink.userId : false; title = (sharedLink.album ? sharedLink.album.albumName : 'Public Share') + ' - Immich'; diff --git a/web/src/routes/(user)/share/[key]/+page.ts b/web/src/routes/(user)/share/[key]/+page.ts index 380c9d002..89821a447 100644 --- a/web/src/routes/(user)/share/[key]/+page.ts +++ b/web/src/routes/(user)/share/[key]/+page.ts @@ -1,4 +1,4 @@ -import { getAssetThumbnailUrl } from '$lib/utils'; +import { getAssetThumbnailUrl, setSharedLink } from '$lib/utils'; import { authenticate } from '$lib/utils/auth'; import { ThumbnailFormat, getMySharedLink, isHttpError } from '@immich/sdk'; import type { PageLoad } from './$types'; @@ -9,6 +9,7 @@ export const load = (async ({ params }) => { try { const sharedLink = await getMySharedLink({ key }); + setSharedLink(sharedLink); const assetCount = sharedLink.assets.length; const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id; diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index d63f9544a..cb80a9e0d 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -13,7 +13,7 @@ import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; import ThemeSettings from '$lib/components/admin-page/settings/theme/theme-settings.svelte'; - import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte'; + import ImageSettings from '$lib/components/admin-page/settings/image/image-settings.svelte'; import TrashSettings from '$lib/components/admin-page/settings/trash-settings/trash-settings.svelte'; import UserSettings from '$lib/components/admin-page/settings/user-settings/user-settings.svelte'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; @@ -43,7 +43,7 @@ | typeof ServerSettings | typeof StorageTemplateSettings | typeof ThemeSettings - | typeof ThumbnailSettings + | typeof ImageSettings | typeof TrashSettings | typeof NewVersionCheckSettings | typeof FFmpegSettings @@ -64,6 +64,12 @@ subtitle: string; key: string; }> = [ + { + item: ImageSettings, + title: 'Image Settings', + subtitle: 'Manage the quality and resolution of generated images', + key: 'image', + }, { item: JobSettings, title: 'Job Settings', @@ -124,12 +130,6 @@ subtitle: 'Manage customization of the Immich web interface', key: 'theme', }, - { - item: ThumbnailSettings, - title: 'Thumbnail Settings', - subtitle: 'Manage the resolution of thumbnail sizes', - key: 'thumbnail', - }, { item: TrashSettings, title: 'Trash Settings',