Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ec1f51cdf | |||
| 507793235c | |||
| cd26b6260b | |||
| b08ddf4c61 | |||
| 352abd6188 | |||
| 24c27eb366 | |||
| 882d9bee04 |
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.9.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
steps:
|
||||
- name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.9.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.8.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
name: Fix formatting
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
fix-formatting:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.label.name == 'fix:formatting' }}
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: 'Checkout'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: './server/.nvmrc'
|
||||
|
||||
- name: Fix formatting
|
||||
run: make install-all && make format-all
|
||||
|
||||
- name: Commit and push
|
||||
uses: EndBug/add-and-commit@v9
|
||||
with:
|
||||
default_author: github_actions
|
||||
message: 'chore: fix formatting'
|
||||
|
||||
- name: Remove label
|
||||
uses: actions/github-script@v7
|
||||
if: always()
|
||||
with:
|
||||
script: |
|
||||
github.rest.issues.removeLabel({
|
||||
issue_number: context.payload.pull_request.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
name: 'fix:formatting'
|
||||
})
|
||||
|
||||
Generated
+8
-8
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.28",
|
||||
"version": "2.2.26",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.28",
|
||||
"version": "2.2.26",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.3.2",
|
||||
@@ -24,7 +24,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
@@ -52,14 +52,14 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -1378,9 +1378,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.2.tgz",
|
||||
"integrity": "sha512-OOHK4sjXqkL7yQ7VEEHcf6+0jSvKjWqwnaCtY7AKD/VLEvRHMsxxu7eI8ErnjxHS8VwmekD4PeVCpu4qZEZSxg==",
|
||||
"version": "20.16.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz",
|
||||
"integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.28",
|
||||
"version": "2.2.26",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
@@ -20,7 +20,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
|
||||
@@ -40,26 +40,6 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
#### Compatibility with Let's Encrypt
|
||||
|
||||
In the event that your nginx configuration includes a section for Let's Encrypt, it's likely that you have a segment similar to the following:
|
||||
|
||||
```nginx
|
||||
location ~ /.well-known {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
This particular `location` directive can inadvertently prevent mobile clients from reaching the `/.well-known/immich` path, which is crucial for discovery. Usual error message for this case is: "Your app major version is not compatible with the server". To remedy this, you should introduce an additional location block specifically for this path, ensuring that requests are correctly proxied to the Immich server:
|
||||
|
||||
```nginx
|
||||
location = /.well-known/immich {
|
||||
proxy_pass http://<backend_url>:2283;
|
||||
}
|
||||
```
|
||||
|
||||
By doing so, you'll maintain the functionality of Let's Encrypt while allowing mobile clients to access the necessary Immich path without obstruction.
|
||||
|
||||
### Caddy example config
|
||||
|
||||
As an alternative to nginx, you can also use [Caddy](https://caddyserver.com/) as a reverse proxy (with automatic HTTPS configuration). Below is an example config.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 MiB |
@@ -149,22 +149,6 @@ If you get an error here, please rename the other external library to something
|
||||
|
||||
Within seconds, the assets from the old-pics and videos folders should show up in the main timeline.
|
||||
|
||||
### Folder view
|
||||
|
||||
:::info
|
||||
This feature also exists for assets uploaded other than through external libraries.
|
||||
:::tip
|
||||
You can use the storage template migration feature for the best experience with uploaded assets in this view.
|
||||
:::
|
||||
|
||||
You can browse your photos and videos by folder like in a file explorer.
|
||||
|
||||
Enable this feature from the Users Settings > Features > Folders.
|
||||
|
||||
The UI is currently only available for the web; mobile will come in a subsequent release.
|
||||
|
||||
<img src={require('./img/folder-view.png').default} width="75%" title='Folder-view' />
|
||||
|
||||
### Set Custom Scan Interval
|
||||
|
||||
:::note
|
||||
|
||||
@@ -27,39 +27,3 @@ The beta release channel allows users to test upcoming changes before they are o
|
||||
:::info
|
||||
You can enable automatic backup on supported devices. For more information see [Automatic Backup](/docs/features/automatic-backup.md).
|
||||
:::
|
||||
|
||||
## Album Sync
|
||||
|
||||
You can sync or mirror an album from your phone to the Immich server on your account. For example, if you select Recents, Camera and Videos album for backup, the corresponding album with the same name will be created on the server. Once the assets from those albums are uploaded, they will be put into the target albums automatically.
|
||||
|
||||
### Album Synchronization Highlights
|
||||
|
||||
- **One-Way Sync:** Synchronization is one-way, from the device to the server.
|
||||
|
||||
- **Name Matching:** If an album on the server has the same name as the album on the device, images from the device will be merged with the existing images in the server album.
|
||||
|
||||
- **Shared Albums:** If the matching album on the server is shared, the new photos merged into the album will also be shared.
|
||||
|
||||
- **Album Structure:** When an album is created for the first time, its structure is based on the initial state. Future updates made on the phone (such as deleting or repositioning photos) will not be reflected in Immich.
|
||||
|
||||
- **User-Specific Sync:** Album synchronization is unique to each server user and does not sync between different users or partners.
|
||||
|
||||
- **Mobile-Only Feature:** Album synchronization is currently only available on mobile. For similar options on a computer, refer to [Libraries](/docs/features/libraries) for further details.
|
||||
|
||||
### Synchronizing albums from the past
|
||||
|
||||
Albums can be synchronized to the server even if they did not exist on the server before. In order to apply this setting you have to:
|
||||
Enter the cloud on the top right -> cog wheel on the top right -> select the sync option under Sync albums.
|
||||
|
||||
:::info Sync albums delete/move photos
|
||||
If you delete/move photos in the local album on your device, it will not be reflected in the album on the server **even if** you click Sync albums
|
||||
It will only reflect files you add.
|
||||
:::
|
||||
|
||||
If the same asset is in more than one album it will only sync to the first album it's in, after that it won't sync again even if the user clicks sync albums manually.
|
||||
To overcome this limitation, the files must be removed from the blacklist by
|
||||
App settings -> Advanced -> Duplicate Assets -> Clear
|
||||
|
||||
:::info
|
||||
Cleaning duplicate assets from the list will cause all the previously uploaded duplicate files to be re-uploaded, the files will not actually be uploaded and will be rejected on the server side (due to duplication) but will be synchronized to the album and at the end will be added to the black list again at the end of the synchronization.
|
||||
:::
|
||||
|
||||
@@ -25,10 +25,10 @@ The metrics in immich are grouped into API (endpoint calls and response times),
|
||||
|
||||
### Configuration
|
||||
|
||||
Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_TELEMETRY_INCLUDE=all` environmental variable to your `.env` file. Note that only the server container currently use this variable.
|
||||
Immich will not expose an endpoint for metrics by default. To enable this endpoint, you can add the `IMMICH_METRICS=true` environmental variable to your `.env` file. Note that only the server and microservices containers currently use this variable.
|
||||
|
||||
:::tip
|
||||
`IMMICH_TELEMETRY_INCLUDE=all` enables all metrics. For a more granular configuration you can enumerate the telemetry metrics that should be included as a comma separated list (e.g. `IMMICH_TELEMETRY_INCLUDE=repo,api`). Alternatively, you can also exclude specific metrics with `IMMICH_TELEMETRY_EXCLUDE`. For more information refer to the [environment section](/docs/install/environment-variables.md#prometheus).
|
||||
`IMMICH_METRICS` enables all metrics, but there are also [environmental variables](/docs/install/environment-variables.md#prometheus) to toggle specific metric groups. If you'd like to only expose certain kinds of metrics, you can set only those environmental variables to `true`. Explicitly setting the environmental variable for a metric group overrides `IMMICH_METRICS` for that group. For example, setting `IMMICH_METRICS=true` and `IMMICH_API_METRICS=false` will enable all metrics except API metrics.
|
||||
:::
|
||||
|
||||
The next step is to configure a new or existing Prometheus instance to scrape this endpoint. The following steps assume that you do not have an existing Prometheus instance, but the steps will be similar either way.
|
||||
|
||||
@@ -183,10 +183,15 @@ Other machine learning parameters can be tuned from the admin UI.
|
||||
|
||||
## Prometheus
|
||||
|
||||
| Variable | Description | Default | Containers | Workers |
|
||||
| :------------------------- | :-------------------------------------------------------------------------------------------------------------------- | :-----: | :--------- | :----------------- |
|
||||
| `IMMICH_TELEMETRY_INCLUDE` | Collect these telemetries. List of `host`, `api`, `io`, `repo`, `job`. Note: You can also specify `all` to enable all | | server | api, microservices |
|
||||
| `IMMICH_TELEMETRY_EXCLUDE` | Do not collect these telemetries. List of `host`, `api`, `io`, `repo`, `job` | | server | api, microservices |
|
||||
| Variable | Description | Default | Containers | Workers |
|
||||
| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :--------- | :----------------- |
|
||||
| `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`]) | | server | api, microservices |
|
||||
| `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server | api, microservices |
|
||||
| `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server | api, microservices |
|
||||
| `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server | api, microservices |
|
||||
| `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server | api, microservices |
|
||||
|
||||
\*1: Overridden for a metric group when its corresponding environmental variable is set.
|
||||
|
||||
## Docker Secrets
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
|
||||
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
|
||||
|
||||
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
|
||||
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata`). If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.
|
||||
|
||||
<img
|
||||
src={require('./img/unraid05.webp').default}
|
||||
|
||||
Vendored
-8
@@ -1,12 +1,4 @@
|
||||
[
|
||||
{
|
||||
"label": "v1.119.1",
|
||||
"url": "https://v1.119.1.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.119.0",
|
||||
"url": "https://v1.119.0.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.118.2",
|
||||
"url": "https://v1.118.2.archive.immich.app"
|
||||
|
||||
Generated
+10
-10
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-e2e",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
@@ -15,7 +15,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"../cli": {
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.28",
|
||||
"version": "2.2.26",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
@@ -64,7 +64,7 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
@@ -92,14 +92,14 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"dev": true,
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -1614,9 +1614,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.2.tgz",
|
||||
"integrity": "sha512-OOHK4sjXqkL7yQ7VEEHcf6+0jSvKjWqwnaCtY7AKD/VLEvRHMsxxu7eI8ErnjxHS8VwmekD4PeVCpu4qZEZSxg==",
|
||||
"version": "20.16.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz",
|
||||
"integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -25,7 +25,7 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
|
||||
@@ -1141,6 +1141,7 @@
|
||||
"show_albums": "Show albums",
|
||||
"show_all_people": "Show all people",
|
||||
"show_and_hide_people": "Show & hide people",
|
||||
"show_archived_or_unarchived_assets": "{with, select, true {With} other {Without}} archived assets",
|
||||
"show_file_location": "Show file location",
|
||||
"show_gallery": "Show gallery",
|
||||
"show_hidden_people": "Show hidden people",
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiocache"
|
||||
@@ -3776,4 +3776,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "f95dddfd343a4b2f4d19ffee71ce6b2f5137e5514a60765424164259c4dc1044"
|
||||
content-hash = "f4594d26ee661fb239c7b5750a4c79e5e049480182928af816ccf5e34e8b641f"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.119.1"
|
||||
version = "1.118.2"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
@@ -18,6 +18,7 @@ pydantic-settings = "^2.5.2"
|
||||
aiocache = ">=0.12.1,<1.0"
|
||||
rich = ">=13.4.2"
|
||||
ftfy = ">=6.1.1"
|
||||
setuptools = "^70.0.0"
|
||||
python-multipart = ">=0.0.6,<1.0"
|
||||
orjson = ">=3.9.5"
|
||||
gunicorn = ">=21.1.0"
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 164,
|
||||
"android.injected.version.name" => "1.119.1",
|
||||
"android.injected.version.code" => 163,
|
||||
"android.injected.version.name" => "1.118.2",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -401,7 +401,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 181;
|
||||
CURRENT_PROJECT_VERSION = 179;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -543,7 +543,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 181;
|
||||
CURRENT_PROJECT_VERSION = 179;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -571,7 +571,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 181;
|
||||
CURRENT_PROJECT_VERSION = 179;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -58,11 +58,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.119.0</string>
|
||||
<string>1.117.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>181</string>
|
||||
<string>179</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Release"
|
||||
lane :release do
|
||||
increment_version_number(
|
||||
version_number: "1.119.1"
|
||||
version_number: "1.118.2"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
Generated
+1
-1
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.119.1
|
||||
- API version: 1.118.2
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
|
||||
Generated
+11
-3
@@ -184,7 +184,9 @@ class PeopleApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getPersonStatisticsWithHttpInfo(String id,) async {
|
||||
///
|
||||
/// * [bool] withArchived:
|
||||
Future<Response> getPersonStatisticsWithHttpInfo(String id, { bool? withArchived, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/people/{id}/statistics'
|
||||
.replaceAll('{id}', id);
|
||||
@@ -196,6 +198,10 @@ class PeopleApi {
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (withArchived != null) {
|
||||
queryParams.addAll(_queryParams('', 'withArchived', withArchived));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
@@ -213,8 +219,10 @@ class PeopleApi {
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<PersonStatisticsResponseDto?> getPersonStatistics(String id,) async {
|
||||
final response = await getPersonStatisticsWithHttpInfo(id,);
|
||||
///
|
||||
/// * [bool] withArchived:
|
||||
Future<PersonStatisticsResponseDto?> getPersonStatistics(String id, { bool? withArchived, }) async {
|
||||
final response = await getPersonStatisticsWithHttpInfo(id, withArchived: withArchived, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
||||
+21
-3
@@ -18,6 +18,7 @@ class PeopleUpdateItem {
|
||||
required this.id,
|
||||
this.isHidden,
|
||||
this.name,
|
||||
this.withArchived,
|
||||
});
|
||||
|
||||
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
|
||||
@@ -53,13 +54,23 @@ class PeopleUpdateItem {
|
||||
///
|
||||
String? name;
|
||||
|
||||
/// This property was added in v1.119.0
|
||||
///
|
||||
/// 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? withArchived;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateItem &&
|
||||
other.birthDate == birthDate &&
|
||||
other.featureFaceAssetId == featureFaceAssetId &&
|
||||
other.id == id &&
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name;
|
||||
other.name == name &&
|
||||
other.withArchived == withArchived;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -68,10 +79,11 @@ class PeopleUpdateItem {
|
||||
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isHidden == null ? 0 : isHidden!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode);
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isHidden=$isHidden, name=$name]';
|
||||
String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isHidden=$isHidden, name=$name, withArchived=$withArchived]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -96,6 +108,11 @@ class PeopleUpdateItem {
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -113,6 +130,7 @@ class PeopleUpdateItem {
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden'),
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
+21
-3
@@ -19,6 +19,7 @@ class PersonResponseDto {
|
||||
required this.name,
|
||||
required this.thumbnailPath,
|
||||
this.updatedAt,
|
||||
this.withArchived,
|
||||
});
|
||||
|
||||
DateTime? birthDate;
|
||||
@@ -40,6 +41,15 @@ class PersonResponseDto {
|
||||
///
|
||||
DateTime? updatedAt;
|
||||
|
||||
/// This property was added in v1.119.0
|
||||
///
|
||||
/// 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? withArchived;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
|
||||
other.birthDate == birthDate &&
|
||||
@@ -47,7 +57,8 @@ class PersonResponseDto {
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name &&
|
||||
other.thumbnailPath == thumbnailPath &&
|
||||
other.updatedAt == updatedAt;
|
||||
other.updatedAt == updatedAt &&
|
||||
other.withArchived == withArchived;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -57,10 +68,11 @@ class PersonResponseDto {
|
||||
(isHidden.hashCode) +
|
||||
(name.hashCode) +
|
||||
(thumbnailPath.hashCode) +
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode);
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt, withArchived=$withArchived]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -78,6 +90,11 @@ class PersonResponseDto {
|
||||
} else {
|
||||
// json[r'updatedAt'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -96,6 +113,7 @@ class PersonResponseDto {
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r''),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
+21
-3
@@ -17,6 +17,7 @@ class PersonUpdateDto {
|
||||
this.featureFaceAssetId,
|
||||
this.isHidden,
|
||||
this.name,
|
||||
this.withArchived,
|
||||
});
|
||||
|
||||
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
|
||||
@@ -49,12 +50,22 @@ class PersonUpdateDto {
|
||||
///
|
||||
String? name;
|
||||
|
||||
/// This property was added in v1.119.0
|
||||
///
|
||||
/// 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? withArchived;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonUpdateDto &&
|
||||
other.birthDate == birthDate &&
|
||||
other.featureFaceAssetId == featureFaceAssetId &&
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name;
|
||||
other.name == name &&
|
||||
other.withArchived == withArchived;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -62,10 +73,11 @@ class PersonUpdateDto {
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
|
||||
(isHidden == null ? 0 : isHidden!.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode);
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden, name=$name]';
|
||||
String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden, name=$name, withArchived=$withArchived]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -89,6 +101,11 @@ class PersonUpdateDto {
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -105,6 +122,7 @@ class PersonUpdateDto {
|
||||
featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden'),
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
+21
-3
@@ -20,6 +20,7 @@ class PersonWithFacesResponseDto {
|
||||
required this.name,
|
||||
required this.thumbnailPath,
|
||||
this.updatedAt,
|
||||
this.withArchived,
|
||||
});
|
||||
|
||||
DateTime? birthDate;
|
||||
@@ -43,6 +44,15 @@ class PersonWithFacesResponseDto {
|
||||
///
|
||||
DateTime? updatedAt;
|
||||
|
||||
/// This property was added in v1.119.0
|
||||
///
|
||||
/// 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? withArchived;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
|
||||
other.birthDate == birthDate &&
|
||||
@@ -51,7 +61,8 @@ class PersonWithFacesResponseDto {
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name &&
|
||||
other.thumbnailPath == thumbnailPath &&
|
||||
other.updatedAt == updatedAt;
|
||||
other.updatedAt == updatedAt &&
|
||||
other.withArchived == withArchived;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -62,10 +73,11 @@ class PersonWithFacesResponseDto {
|
||||
(isHidden.hashCode) +
|
||||
(name.hashCode) +
|
||||
(thumbnailPath.hashCode) +
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode);
|
||||
(updatedAt == null ? 0 : updatedAt!.hashCode) +
|
||||
(withArchived == null ? 0 : withArchived!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
|
||||
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt, withArchived=$withArchived]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -84,6 +96,11 @@ class PersonWithFacesResponseDto {
|
||||
} else {
|
||||
// json[r'updatedAt'] = null;
|
||||
}
|
||||
if (this.withArchived != null) {
|
||||
json[r'withArchived'] = this.withArchived;
|
||||
} else {
|
||||
// json[r'withArchived'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -103,6 +120,7 @@ class PersonWithFacesResponseDto {
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
|
||||
updatedAt: mapDateTime(json, r'updatedAt', r''),
|
||||
withArchived: mapValueOfType<bool>(json, r'withArchived'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 1.119.1+164
|
||||
version: 1.118.2+163
|
||||
|
||||
environment:
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
@@ -4152,6 +4152,14 @@
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "withArchived",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -7385,7 +7393,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
@@ -10098,6 +10106,10 @@
|
||||
"name": {
|
||||
"description": "Person name.",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"description": "This property was added in v1.119.0",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -10230,6 +10242,10 @@
|
||||
"description": "This property was added in v1.107.0",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"description": "This property was added in v1.119.0",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -10271,6 +10287,10 @@
|
||||
"name": {
|
||||
"description": "Person name.",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"description": "This property was added in v1.119.0",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
@@ -10304,6 +10324,10 @@
|
||||
"description": "This property was added in v1.107.0",
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"withArchived": {
|
||||
"description": "This property was added in v1.119.0",
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
+6
-6
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
@@ -22,9 +22,9 @@
|
||||
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.2.tgz",
|
||||
"integrity": "sha512-OOHK4sjXqkL7yQ7VEEHcf6+0jSvKjWqwnaCtY7AKD/VLEvRHMsxxu7eI8ErnjxHS8VwmekD4PeVCpu4qZEZSxg==",
|
||||
"version": "20.16.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz",
|
||||
"integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 1.119.1
|
||||
* 1.118.2
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
@@ -220,6 +220,8 @@ export type PersonWithFacesResponseDto = {
|
||||
thumbnailPath: string;
|
||||
/** This property was added in v1.107.0 */
|
||||
updatedAt?: string;
|
||||
/** This property was added in v1.119.0 */
|
||||
withArchived?: boolean;
|
||||
};
|
||||
export type SmartInfoResponseDto = {
|
||||
objects?: string[] | null;
|
||||
@@ -502,6 +504,8 @@ export type PersonResponseDto = {
|
||||
thumbnailPath: string;
|
||||
/** This property was added in v1.107.0 */
|
||||
updatedAt?: string;
|
||||
/** This property was added in v1.119.0 */
|
||||
withArchived?: boolean;
|
||||
};
|
||||
export type AssetFaceResponseDto = {
|
||||
boundingBoxX1: number;
|
||||
@@ -703,6 +707,8 @@ export type PeopleUpdateItem = {
|
||||
isHidden?: boolean;
|
||||
/** Person name. */
|
||||
name?: string;
|
||||
/** This property was added in v1.119.0 */
|
||||
withArchived?: boolean;
|
||||
};
|
||||
export type PeopleUpdateDto = {
|
||||
people: PeopleUpdateItem[];
|
||||
@@ -717,6 +723,8 @@ export type PersonUpdateDto = {
|
||||
isHidden?: boolean;
|
||||
/** Person name. */
|
||||
name?: string;
|
||||
/** This property was added in v1.119.0 */
|
||||
withArchived?: boolean;
|
||||
};
|
||||
export type MergePersonDto = {
|
||||
ids: string[];
|
||||
@@ -2410,13 +2418,16 @@ export function reassignFaces({ id, assetFaceUpdateDto }: {
|
||||
body: assetFaceUpdateDto
|
||||
})));
|
||||
}
|
||||
export function getPersonStatistics({ id }: {
|
||||
export function getPersonStatistics({ id, withArchived }: {
|
||||
id: string;
|
||||
withArchived?: boolean;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 200;
|
||||
data: PersonStatisticsResponseDto;
|
||||
}>(`/people/${encodeURIComponent(id)}/statistics`, {
|
||||
}>(`/people/${encodeURIComponent(id)}/statistics${QS.query(QS.explode({
|
||||
withArchived
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20241025@sha256:a55edebdb00f2cd034f99245612833ae4fbb05745f4b1503b8fc97f6a4311502 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20241022@sha256:22941f8bd36e27a2a659e755ce8ee3e3906adfa41a3ad15e81cad0ed333c14ff AS dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
@@ -42,7 +42,7 @@ RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20241025@sha256:ee25f3135ee6e70810c810cd9789068594ffb1a48e7c28fe77303553e904979e
|
||||
FROM ghcr.io/immich-app/base-server-prod:20241022@sha256:6676a716a11106887c98a2d4ac4677a92c6f80ba6da3e496de1b302a56882ef5
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
Generated
+9
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^10.0.1",
|
||||
@@ -83,7 +83,7 @@
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
@@ -5494,9 +5494,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.2.tgz",
|
||||
"integrity": "sha512-OOHK4sjXqkL7yQ7VEEHcf6+0jSvKjWqwnaCtY7AKD/VLEvRHMsxxu7eI8ErnjxHS8VwmekD4PeVCpu4qZEZSxg==",
|
||||
"version": "20.16.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz",
|
||||
"integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
@@ -18890,9 +18890,9 @@
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "20.17.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.2.tgz",
|
||||
"integrity": "sha512-OOHK4sjXqkL7yQ7VEEHcf6+0jSvKjWqwnaCtY7AKD/VLEvRHMsxxu7eI8ErnjxHS8VwmekD4PeVCpu4qZEZSxg==",
|
||||
"version": "20.16.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz",
|
||||
"integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==",
|
||||
"requires": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -24,10 +24,10 @@
|
||||
"typeorm": "typeorm",
|
||||
"lifecycle": "node ./dist/utils/lifecycle.js",
|
||||
"typeorm:migrations:create": "typeorm migration:create",
|
||||
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/bin/database.js",
|
||||
"typeorm:migrations:run": "typeorm migration:run -d ./dist/bin/database.js",
|
||||
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/bin/database.js",
|
||||
"typeorm:schema:drop": "typeorm query -d ./dist/bin/database.js 'DROP schema public cascade; CREATE schema public;'",
|
||||
"typeorm:migrations:generate": "typeorm migration:generate -d ./dist/database.config.js",
|
||||
"typeorm:migrations:run": "typeorm migration:run -d ./dist/database.config.js",
|
||||
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/database.config.js",
|
||||
"typeorm:schema:drop": "typeorm query -d ./dist/database.config.js 'DROP schema public cascade; CREATE schema public;'",
|
||||
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
|
||||
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
||||
"sync:sql": "node ./dist/bin/sync-sql.js",
|
||||
@@ -108,7 +108,7 @@
|
||||
"@types/lodash": "^4.14.197",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^3.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { OpenTelemetryModule } from 'nestjs-otel';
|
||||
import { commands } from 'src/commands';
|
||||
import { clsConfig, immichAppConfig } from 'src/config';
|
||||
import { controllers } from 'src/controllers';
|
||||
import { databaseConfig } from 'src/database.config';
|
||||
import { entities } from 'src/entities';
|
||||
import { ImmichWorker } from 'src/enum';
|
||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||
@@ -37,7 +38,7 @@ const middleware = [
|
||||
];
|
||||
|
||||
const configRepository = new ConfigRepository();
|
||||
const { bull, database, otel } = configRepository.getEnv();
|
||||
const { bull, otel } = configRepository.getEnv();
|
||||
|
||||
const imports = [
|
||||
BullModule.forRoot(bull.config),
|
||||
@@ -49,7 +50,7 @@ const imports = [
|
||||
inject: [ModuleRef],
|
||||
useFactory: (moduleRef: ModuleRef) => {
|
||||
return {
|
||||
...database.config,
|
||||
...databaseConfig,
|
||||
poolErrorHandler: (error) => {
|
||||
moduleRef.get(DatabaseService, { strict: false }).handleConnectionError(error);
|
||||
},
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
const { database } = new ConfigRepository().getEnv();
|
||||
|
||||
/**
|
||||
* @deprecated - DO NOT USE THIS
|
||||
*
|
||||
* this export is ONLY to be used for TypeORM commands in package.json#scripts
|
||||
*/
|
||||
export const dataSource = new DataSource({ ...database.config, host: 'localhost' });
|
||||
@@ -8,6 +8,7 @@ import { OpenTelemetryModule } from 'nestjs-otel';
|
||||
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { format } from 'sql-formatter';
|
||||
import { databaseConfig } from 'src/database.config';
|
||||
import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators';
|
||||
import { entities } from 'src/entities';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
@@ -73,12 +74,12 @@ class SqlGenerator {
|
||||
await rm(this.options.targetDir, { force: true, recursive: true });
|
||||
await mkdir(this.options.targetDir);
|
||||
|
||||
const { database, otel } = new ConfigRepository().getEnv();
|
||||
const { otel } = new ConfigRepository().getEnv();
|
||||
|
||||
const moduleFixture = await Test.createTestingModule({
|
||||
imports: [
|
||||
TypeOrmModule.forRoot({
|
||||
...database.config,
|
||||
...databaseConfig,
|
||||
host: 'localhost',
|
||||
entities,
|
||||
logging: ['query'],
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PersonResponseDto,
|
||||
PersonSearchDto,
|
||||
PersonStatisticsResponseDto,
|
||||
PersonStatsDto,
|
||||
PersonUpdateDto,
|
||||
} from 'src/dtos/person.dto';
|
||||
import { Permission } from 'src/enum';
|
||||
@@ -65,8 +66,12 @@ export class PersonController {
|
||||
|
||||
@Get(':id/statistics')
|
||||
@Authenticated({ permission: Permission.PERSON_STATISTICS })
|
||||
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
|
||||
return this.service.getStatistics(auth, id);
|
||||
getPersonStatistics(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Query() dto: PersonStatsDto,
|
||||
): Promise<PersonStatisticsResponseDto> {
|
||||
return this.service.getStatistics(auth, id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/thumbnail')
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
||||
|
||||
const { database } = new ConfigRepository().getEnv();
|
||||
const { url, host, port, username, password, name } = database;
|
||||
const urlOrParts = url
|
||||
? { url }
|
||||
: {
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
database: name,
|
||||
};
|
||||
|
||||
/* eslint unicorn/prefer-module: "off" -- We can fix this when migrating to ESM*/
|
||||
export const databaseConfig: PostgresConnectionOptions = {
|
||||
type: 'postgres',
|
||||
entities: [__dirname + '/entities/*.entity.{js,ts}'],
|
||||
migrations: [__dirname + '/migrations/*.{js,ts}'],
|
||||
subscribers: [__dirname + '/subscribers/*.{js,ts}'],
|
||||
migrationsRun: false,
|
||||
synchronize: false,
|
||||
connectTimeoutMS: 10_000, // 10 seconds
|
||||
parseInt8: true,
|
||||
...urlOrParts,
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated - DO NOT USE THIS
|
||||
*
|
||||
* this export is ONLY to be used for TypeORM commands in package.json#scripts
|
||||
*/
|
||||
export const dataSource = new DataSource({ ...databaseConfig, host: 'localhost' });
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsInt, IsNotEmpty, IsString, Max, Min, ValidateNested } from 'class-validator';
|
||||
import { IsArray, IsBoolean, IsInt, IsNotEmpty, IsString, Max, Min, ValidateNested } from 'class-validator';
|
||||
import { DateTime } from 'luxon';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -41,6 +41,11 @@ export class PersonUpdateDto extends PersonCreateDto {
|
||||
@Optional()
|
||||
@IsString()
|
||||
featureFaceAssetId?: string;
|
||||
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
@PropertyLifecycle({ addedAt: 'v1.119.0' })
|
||||
withArchived?: boolean;
|
||||
}
|
||||
|
||||
export class PeopleUpdateDto {
|
||||
@@ -93,6 +98,8 @@ export class PersonResponseDto {
|
||||
isHidden!: boolean;
|
||||
@PropertyLifecycle({ addedAt: 'v1.107.0' })
|
||||
updatedAt?: Date;
|
||||
@PropertyLifecycle({ addedAt: 'v1.119.0' })
|
||||
withArchived?: boolean;
|
||||
}
|
||||
|
||||
export class PersonWithFacesResponseDto extends PersonResponseDto {
|
||||
@@ -147,6 +154,11 @@ export class PersonStatisticsResponseDto {
|
||||
assets!: number;
|
||||
}
|
||||
|
||||
export class PersonStatsDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
withArchived?: boolean;
|
||||
}
|
||||
|
||||
export class PeopleResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
total!: number;
|
||||
@@ -167,6 +179,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
|
||||
thumbnailPath: person.thumbnailPath,
|
||||
isHidden: person.isHidden,
|
||||
updatedAt: person.updatedAt,
|
||||
withArchived: person.withArchived,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ export class AlbumEntity {
|
||||
albumUsers!: AlbumUserEntity[];
|
||||
|
||||
@ManyToMany(() => AssetEntity, (asset) => asset.albums)
|
||||
@JoinTable({ synchronize: false })
|
||||
@JoinTable()
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@OneToMany(() => SharedLinkEntity, (link) => link.album)
|
||||
|
||||
@@ -49,4 +49,7 @@ export class PersonEntity {
|
||||
|
||||
@Column({ default: false })
|
||||
isHidden!: boolean;
|
||||
|
||||
@Column({ default: false })
|
||||
withArchived!: boolean;
|
||||
}
|
||||
|
||||
@@ -363,11 +363,3 @@ export enum ImmichWorker {
|
||||
API = 'api',
|
||||
MICROSERVICES = 'microservices',
|
||||
}
|
||||
|
||||
export enum ImmichTelemetry {
|
||||
HOST = 'host',
|
||||
API = 'api',
|
||||
IO = 'io',
|
||||
REPO = 'repo',
|
||||
JOB = 'job',
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import { RegisterQueueOptions } from '@nestjs/bullmq';
|
||||
import { QueueOptions } from 'bullmq';
|
||||
import { RedisOptions } from 'ioredis';
|
||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
|
||||
import { ImmichEnvironment, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
|
||||
import { ImmichEnvironment, ImmichWorker, LogLevel } from 'src/enum';
|
||||
import { VectorExtension } from 'src/interfaces/database.interface';
|
||||
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
|
||||
|
||||
export const IConfigRepository = 'IConfigRepository';
|
||||
|
||||
@@ -37,7 +36,12 @@ export interface EnvData {
|
||||
};
|
||||
|
||||
database: {
|
||||
config: PostgresConnectionOptions;
|
||||
url?: string;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
skipMigrations: boolean;
|
||||
vectorExtension: VectorExtension;
|
||||
};
|
||||
@@ -73,7 +77,11 @@ export interface EnvData {
|
||||
telemetry: {
|
||||
apiPort: number;
|
||||
microservicesPort: number;
|
||||
metrics: Set<ImmichTelemetry>;
|
||||
enabled: boolean;
|
||||
apiMetrics: boolean;
|
||||
hostMetrics: boolean;
|
||||
repoMetrics: boolean;
|
||||
jobMetrics: boolean;
|
||||
};
|
||||
|
||||
storage: {
|
||||
|
||||
@@ -19,7 +19,7 @@ export enum DatabaseLock {
|
||||
StorageTemplateMigration = 420,
|
||||
VersionHistory = 500,
|
||||
CLIPDimSize = 512,
|
||||
Library = 1337,
|
||||
LibraryWatch = 1337,
|
||||
GetSystemConfig = 69,
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ export interface DeleteFacesOptions {
|
||||
sourceType: SourceType;
|
||||
}
|
||||
|
||||
export interface PersonStatsOptions {
|
||||
withArchived?: boolean;
|
||||
}
|
||||
|
||||
export type UnassignFacesOptions = DeleteFacesOptions;
|
||||
|
||||
export interface IPersonRepository {
|
||||
@@ -74,7 +78,7 @@ export interface IPersonRepository {
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]>;
|
||||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | null>;
|
||||
getStatistics(personId: string): Promise<PersonStatistics>;
|
||||
getStatistics(personId: string, options: PersonStatsOptions): Promise<PersonStatistics>;
|
||||
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
||||
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddWithArchivedToPerson1728944141526 implements MigrationInterface {
|
||||
name = 'AddWithArchivedToPerson1728944141526'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" ADD "withArchived" boolean NOT NULL DEFAULT false`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "withArchived"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddAlbumAssetCreatedAt1729793521993 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "albums_assets_assets" ADD COLUMN "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "albums_assets_assets" DROP COLUMN "createdAt"`);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ WHERE
|
||||
)
|
||||
AND ("entity"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"entity"."fileCreatedAt" ASC
|
||||
"entity"."localDateTime" ASC
|
||||
|
||||
-- AssetRepository.getByIds
|
||||
SELECT
|
||||
@@ -212,6 +212,7 @@ SELECT
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."thumbnailPath" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_thumbnailPath",
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."faceAssetId" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_faceAssetId",
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."isHidden" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_isHidden",
|
||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."withArchived" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_withArchived",
|
||||
"AssetEntity__AssetEntity_stack"."id" AS "AssetEntity__AssetEntity_stack_id",
|
||||
"AssetEntity__AssetEntity_stack"."ownerId" AS "AssetEntity__AssetEntity_stack_ownerId",
|
||||
"AssetEntity__AssetEntity_stack"."primaryAssetId" AS "AssetEntity__AssetEntity_stack_primaryAssetId",
|
||||
|
||||
@@ -17,7 +17,8 @@ SELECT
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
"person"."isHidden" AS "person_isHidden",
|
||||
"person"."withArchived" AS "person_withArchived"
|
||||
FROM
|
||||
"person" "person"
|
||||
LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
@@ -54,7 +55,8 @@ SELECT
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
"person"."isHidden" AS "person_isHidden",
|
||||
"person"."withArchived" AS "person_withArchived"
|
||||
FROM
|
||||
"person" "person"
|
||||
LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id"
|
||||
@@ -83,7 +85,8 @@ SELECT
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."withArchived" AS "AssetFaceEntity__AssetFaceEntity_person_withArchived"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
@@ -116,7 +119,8 @@ FROM
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."withArchived" AS "AssetFaceEntity__AssetFaceEntity_person_withArchived"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
@@ -153,6 +157,7 @@ FROM
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."withArchived" AS "AssetFaceEntity__AssetFaceEntity_person_withArchived",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
|
||||
@@ -213,7 +218,8 @@ SELECT
|
||||
"person"."birthDate" AS "person_birthDate",
|
||||
"person"."thumbnailPath" AS "person_thumbnailPath",
|
||||
"person"."faceAssetId" AS "person_faceAssetId",
|
||||
"person"."isHidden" AS "person_isHidden"
|
||||
"person"."isHidden" AS "person_isHidden",
|
||||
"person"."withArchived" AS "person_withArchived"
|
||||
FROM
|
||||
"person" "person"
|
||||
WHERE
|
||||
@@ -242,11 +248,18 @@ FROM
|
||||
"asset_faces" "face"
|
||||
LEFT JOIN "assets" "asset" ON "asset"."id" = "face"."assetId"
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
LEFT JOIN "person" "person" ON "person"."id" = "face"."personId"
|
||||
WHERE
|
||||
"face"."personId" = $1
|
||||
AND "asset"."isArchived" = false
|
||||
AND "asset"."deletedAt" IS NULL
|
||||
AND "asset"."livePhotoVideoId" IS NULL
|
||||
AND (
|
||||
(
|
||||
"person"."withArchived" = false
|
||||
AND "asset"."isArchived" = false
|
||||
)
|
||||
OR "person"."withArchived" = true
|
||||
)
|
||||
|
||||
-- PersonRepository.getNumberOfPeople
|
||||
SELECT
|
||||
|
||||
@@ -93,7 +93,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
)
|
||||
.leftJoinAndSelect('entity.exifInfo', 'exifInfo')
|
||||
.leftJoinAndSelect('entity.files', 'files')
|
||||
.orderBy('entity.fileCreatedAt', 'ASC')
|
||||
.orderBy('entity.localDateTime', 'ASC')
|
||||
.getMany();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ImmichTelemetry } from 'src/enum';
|
||||
import { clearEnvCache, ConfigRepository } from 'src/repositories/config.repository';
|
||||
|
||||
const getEnv = () => {
|
||||
@@ -13,8 +12,11 @@ const resetEnv = () => {
|
||||
'IMMICH_TRUSTED_PROXIES',
|
||||
'IMMICH_API_METRICS_PORT',
|
||||
'IMMICH_MICROSERVICES_METRICS_PORT',
|
||||
'IMMICH_TELEMETRY_INCLUDE',
|
||||
'IMMICH_TELEMETRY_EXCLUDE',
|
||||
'IMMICH_METRICS',
|
||||
'IMMICH_API_METRICS',
|
||||
'IMMICH_HOST_METRICS',
|
||||
'IMMICH_IO_METRICS',
|
||||
'IMMICH_JOB_METRICS',
|
||||
|
||||
'DB_URL',
|
||||
'DB_HOSTNAME',
|
||||
@@ -66,14 +68,12 @@ describe('getEnv', () => {
|
||||
it('should use defaults', () => {
|
||||
const { database } = getEnv();
|
||||
expect(database).toEqual({
|
||||
config: expect.objectContaining({
|
||||
type: 'postgres',
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
database: 'immich',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
}),
|
||||
url: undefined,
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
name: 'immich',
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
skipMigrations: false,
|
||||
vectorExtension: 'vectors',
|
||||
});
|
||||
@@ -210,7 +210,11 @@ describe('getEnv', () => {
|
||||
expect(telemetry).toEqual({
|
||||
apiPort: 8081,
|
||||
microservicesPort: 8082,
|
||||
metrics: new Set([]),
|
||||
enabled: false,
|
||||
apiMetrics: false,
|
||||
hostMetrics: false,
|
||||
jobMetrics: false,
|
||||
repoMetrics: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -221,29 +225,32 @@ describe('getEnv', () => {
|
||||
expect(telemetry).toMatchObject({
|
||||
apiPort: 2001,
|
||||
microservicesPort: 2002,
|
||||
metrics: expect.any(Set),
|
||||
});
|
||||
});
|
||||
|
||||
it('should run with telemetry enabled', () => {
|
||||
process.env.IMMICH_TELEMETRY_INCLUDE = 'all';
|
||||
process.env.IMMICH_METRICS = 'true';
|
||||
const { telemetry } = getEnv();
|
||||
expect(telemetry.metrics).toEqual(new Set(Object.values(ImmichTelemetry)));
|
||||
expect(telemetry).toMatchObject({
|
||||
enabled: true,
|
||||
apiMetrics: true,
|
||||
hostMetrics: true,
|
||||
jobMetrics: true,
|
||||
repoMetrics: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should run with telemetry enabled and jobs disabled', () => {
|
||||
process.env.IMMICH_TELEMETRY_INCLUDE = 'all';
|
||||
process.env.IMMICH_TELEMETRY_EXCLUDE = 'job';
|
||||
process.env.IMMICH_METRICS = 'true';
|
||||
process.env.IMMICH_JOB_METRICS = 'false';
|
||||
const { telemetry } = getEnv();
|
||||
expect(telemetry.metrics).toEqual(
|
||||
new Set([ImmichTelemetry.API, ImmichTelemetry.HOST, ImmichTelemetry.IO, ImmichTelemetry.REPO]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should run with specific telemetry metrics', () => {
|
||||
process.env.IMMICH_TELEMETRY_INCLUDE = 'io, host, api';
|
||||
const { telemetry } = getEnv();
|
||||
expect(telemetry.metrics).toEqual(new Set([ImmichTelemetry.API, ImmichTelemetry.HOST, ImmichTelemetry.IO]));
|
||||
expect(telemetry).toMatchObject({
|
||||
enabled: true,
|
||||
apiMetrics: true,
|
||||
hostMetrics: true,
|
||||
jobMetrics: false,
|
||||
repoMetrics: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { join, resolve } from 'node:path';
|
||||
import { join } from 'node:path';
|
||||
import { citiesFile, excludePaths } from 'src/constants';
|
||||
import { Telemetry } from 'src/decorators';
|
||||
import { ImmichEnvironment, ImmichTelemetry, ImmichWorker, LogLevel } from 'src/enum';
|
||||
import { ImmichEnvironment, ImmichWorker, LogLevel } from 'src/enum';
|
||||
import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
@@ -25,17 +25,18 @@ const stagingKeys = {
|
||||
};
|
||||
|
||||
const WORKER_TYPES = new Set(Object.values(ImmichWorker));
|
||||
const TELEMETRY_TYPES = new Set(Object.values(ImmichTelemetry));
|
||||
|
||||
const asSet = <T>(value: string | undefined, defaults: T[]) => {
|
||||
const asSet = (value: string | undefined, defaults: ImmichWorker[]) => {
|
||||
const values = (value || '').replaceAll(/\s/g, '').split(',').filter(Boolean);
|
||||
return new Set(values.length === 0 ? defaults : (values as T[]));
|
||||
return new Set(values.length === 0 ? defaults : (values as ImmichWorker[]));
|
||||
};
|
||||
|
||||
const parseBoolean = (value: string | undefined, defaultValue: boolean) => (value ? value === 'true' : defaultValue);
|
||||
|
||||
const getEnv = (): EnvData => {
|
||||
const includedWorkers = asSet(process.env.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]);
|
||||
const excludedWorkers = asSet(process.env.IMMICH_WORKERS_EXCLUDE, []);
|
||||
const workers = [...setDifference(includedWorkers, excludedWorkers)];
|
||||
const included = asSet(process.env.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]);
|
||||
const excluded = asSet(process.env.IMMICH_WORKERS_EXCLUDE, []);
|
||||
const workers = [...setDifference(included, excluded)];
|
||||
for (const worker of workers) {
|
||||
if (!WORKER_TYPES.has(worker)) {
|
||||
throw new Error(`Invalid worker(s) found: ${workers.join(',')}`);
|
||||
@@ -46,14 +47,10 @@ const getEnv = (): EnvData => {
|
||||
const isProd = environment === ImmichEnvironment.PRODUCTION;
|
||||
const buildFolder = process.env.IMMICH_BUILD_DATA || '/build';
|
||||
const folders = {
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
dist: resolve(`${__dirname}/..`),
|
||||
geodata: join(buildFolder, 'geodata'),
|
||||
web: join(buildFolder, 'www'),
|
||||
};
|
||||
|
||||
const databaseUrl = process.env.DB_URL;
|
||||
|
||||
let redisConfig = {
|
||||
host: process.env.REDIS_HOSTNAME || 'redis',
|
||||
port: Number.parseInt(process.env.REDIS_PORT || '') || 6379,
|
||||
@@ -72,18 +69,12 @@ const getEnv = (): EnvData => {
|
||||
}
|
||||
}
|
||||
|
||||
const includedTelemetries =
|
||||
process.env.IMMICH_TELEMETRY_INCLUDE === 'all'
|
||||
? new Set(Object.values(ImmichTelemetry))
|
||||
: asSet<ImmichTelemetry>(process.env.IMMICH_TELEMETRY_INCLUDE, []);
|
||||
|
||||
const excludedTelemetries = asSet<ImmichTelemetry>(process.env.IMMICH_TELEMETRY_EXCLUDE, []);
|
||||
const telemetries = setDifference(includedTelemetries, excludedTelemetries);
|
||||
for (const telemetry of telemetries) {
|
||||
if (!TELEMETRY_TYPES.has(telemetry)) {
|
||||
throw new Error(`Invalid telemetry found: ${telemetry}`);
|
||||
}
|
||||
}
|
||||
const globalEnabled = parseBoolean(process.env.IMMICH_METRICS, false);
|
||||
const hostMetrics = parseBoolean(process.env.IMMICH_HOST_METRICS, globalEnabled);
|
||||
const apiMetrics = parseBoolean(process.env.IMMICH_API_METRICS, globalEnabled);
|
||||
const repoMetrics = parseBoolean(process.env.IMMICH_IO_METRICS, globalEnabled);
|
||||
const jobMetrics = parseBoolean(process.env.IMMICH_JOB_METRICS, globalEnabled);
|
||||
const telemetryEnabled = globalEnabled || hostMetrics || apiMetrics || repoMetrics || jobMetrics;
|
||||
|
||||
return {
|
||||
host: process.env.IMMICH_HOST,
|
||||
@@ -122,25 +113,12 @@ const getEnv = (): EnvData => {
|
||||
},
|
||||
|
||||
database: {
|
||||
config: {
|
||||
type: 'postgres',
|
||||
entities: [`${folders.dist}/entities` + '/*.entity.{js,ts}'],
|
||||
migrations: [`${folders.dist}/migrations` + '/*.{js,ts}'],
|
||||
subscribers: [`${folders.dist}/subscribers` + '/*.{js,ts}'],
|
||||
migrationsRun: false,
|
||||
synchronize: false,
|
||||
connectTimeoutMS: 10_000, // 10 seconds
|
||||
parseInt8: true,
|
||||
...(databaseUrl
|
||||
? { url: databaseUrl }
|
||||
: {
|
||||
host: process.env.DB_HOSTNAME || 'database',
|
||||
port: Number(process.env.DB_PORT) || 5432,
|
||||
username: process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
database: process.env.DB_DATABASE_NAME || 'immich',
|
||||
}),
|
||||
},
|
||||
url: process.env.DB_URL,
|
||||
host: process.env.DB_HOSTNAME || 'database',
|
||||
port: Number(process.env.DB_PORT) || 5432,
|
||||
username: process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
name: process.env.DB_DATABASE_NAME || 'immich',
|
||||
|
||||
skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true',
|
||||
vectorExtension:
|
||||
@@ -158,9 +136,9 @@ const getEnv = (): EnvData => {
|
||||
|
||||
otel: {
|
||||
metrics: {
|
||||
hostMetrics: telemetries.has(ImmichTelemetry.HOST),
|
||||
hostMetrics,
|
||||
apiMetrics: {
|
||||
enable: telemetries.has(ImmichTelemetry.API),
|
||||
enable: apiMetrics,
|
||||
ignoreRoutes: excludePaths,
|
||||
},
|
||||
},
|
||||
@@ -190,7 +168,11 @@ const getEnv = (): EnvData => {
|
||||
telemetry: {
|
||||
apiPort: Number(process.env.IMMICH_API_METRICS_PORT || '') || 8081,
|
||||
microservicesPort: Number(process.env.IMMICH_MICROSERVICES_METRICS_PORT || '') || 8082,
|
||||
metrics: telemetries,
|
||||
enabled: telemetryEnabled,
|
||||
hostMetrics,
|
||||
apiMetrics,
|
||||
repoMetrics,
|
||||
jobMetrics,
|
||||
},
|
||||
|
||||
workers,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
PersonNameSearchOptions,
|
||||
PersonSearchOptions,
|
||||
PersonStatistics,
|
||||
PersonStatsOptions,
|
||||
UnassignFacesOptions,
|
||||
UpdateFacesData,
|
||||
} from 'src/interfaces/person.interface';
|
||||
@@ -212,17 +213,33 @@ export class PersonRepository implements IPersonRepository {
|
||||
return queryBuilder.getMany();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getStatistics(personId: string): Promise<PersonStatistics> {
|
||||
const items = await this.assetFaceRepository
|
||||
@GenerateSql({ params: [DummyValue.UUID, {}] })
|
||||
async getStatistics(personId: string, options: PersonStatsOptions): Promise<PersonStatistics> {
|
||||
/*
|
||||
* withArchived: true -> Return the count of all assets for a given person
|
||||
* withArchived: false -> Return the count of all unarchived assets for a given person
|
||||
* withArchived: undefined ->
|
||||
* - If person.withArchived = true -> Return the count of all assets for a given person
|
||||
* - If person.withArchived = false -> Return the count of all unarchived assets for a given person
|
||||
*/
|
||||
|
||||
const queryBuilder = this.assetFaceRepository
|
||||
.createQueryBuilder('face')
|
||||
.leftJoin('face.asset', 'asset')
|
||||
.where('face.personId = :personId', { personId })
|
||||
.andWhere('asset.isArchived = false')
|
||||
.andWhere('asset.deletedAt IS NULL')
|
||||
.andWhere('asset.livePhotoVideoId IS NULL')
|
||||
.select('COUNT(DISTINCT(asset.id))', 'count')
|
||||
.getRawOne();
|
||||
.select('COUNT(DISTINCT(asset.id))', 'count');
|
||||
|
||||
if (options.withArchived === false) {
|
||||
queryBuilder.andWhere('asset.isArchived = false');
|
||||
} else if (options.withArchived === undefined) {
|
||||
queryBuilder
|
||||
.leftJoin('face.person', 'person')
|
||||
.andWhere('((person.withArchived = false AND asset.isArchived = false) OR person.withArchived = true)');
|
||||
}
|
||||
|
||||
const items = await queryBuilder.getRawOne();
|
||||
return {
|
||||
assets: items.count ?? 0,
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import { snakeCase, startCase } from 'lodash';
|
||||
import { MetricService } from 'nestjs-otel';
|
||||
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { ImmichTelemetry, MetadataKey } from 'src/enum';
|
||||
import { MetadataKey } from 'src/enum';
|
||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMetricGroupRepository, ITelemetryRepository, MetricGroupOptions } from 'src/interfaces/telemetry.interface';
|
||||
@@ -99,18 +99,17 @@ export class TelemetryRepository implements ITelemetryRepository {
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {
|
||||
const { telemetry } = this.configRepository.getEnv();
|
||||
const { metrics } = telemetry;
|
||||
const { apiMetrics, hostMetrics, jobMetrics, repoMetrics } = telemetry;
|
||||
|
||||
this.api = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.API) });
|
||||
this.host = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.HOST) });
|
||||
this.jobs = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.JOB) });
|
||||
this.repo = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.REPO) });
|
||||
this.api = new MetricGroupRepository(metricService).configure({ enabled: apiMetrics });
|
||||
this.host = new MetricGroupRepository(metricService).configure({ enabled: hostMetrics });
|
||||
this.jobs = new MetricGroupRepository(metricService).configure({ enabled: jobMetrics });
|
||||
this.repo = new MetricGroupRepository(metricService).configure({ enabled: repoMetrics });
|
||||
}
|
||||
|
||||
setup({ repositories }: { repositories: ClassConstructor<unknown>[] }) {
|
||||
const { telemetry } = this.configRepository.getEnv();
|
||||
const { metrics } = telemetry;
|
||||
if (!metrics.has(ImmichTelemetry.REPO)) {
|
||||
if (!telemetry.enabled || !telemetry.repoMetrics) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { OAuthProfile } from 'src/interfaces/oauth.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { isGranted } from 'src/utils/access';
|
||||
import { HumanReadableSize } from 'src/utils/bytes';
|
||||
import { createUser } from 'src/utils/user';
|
||||
|
||||
export interface LoginDetails {
|
||||
isSecure: boolean;
|
||||
@@ -114,13 +115,16 @@ export class AuthService extends BaseService {
|
||||
throw new BadRequestException('The server already has an admin');
|
||||
}
|
||||
|
||||
const admin = await this.createUser({
|
||||
isAdmin: true,
|
||||
email: dto.email,
|
||||
name: dto.name,
|
||||
password: dto.password,
|
||||
storageLabel: 'admin',
|
||||
});
|
||||
const admin = await createUser(
|
||||
{ userRepo: this.userRepository, cryptoRepo: this.cryptoRepository },
|
||||
{
|
||||
isAdmin: true,
|
||||
email: dto.email,
|
||||
name: dto.name,
|
||||
password: dto.password,
|
||||
storageLabel: 'admin',
|
||||
},
|
||||
);
|
||||
|
||||
return mapUserAdmin(admin);
|
||||
}
|
||||
@@ -230,13 +234,16 @@ export class AuthService extends BaseService {
|
||||
});
|
||||
|
||||
const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
|
||||
user = await this.createUser({
|
||||
name: userName,
|
||||
email: profile.email,
|
||||
oauthId: profile.sub,
|
||||
quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null,
|
||||
storageLabel: storageLabel || null,
|
||||
});
|
||||
user = await createUser(
|
||||
{ userRepo: this.userRepository, cryptoRepo: this.cryptoRepository },
|
||||
{
|
||||
name: userName,
|
||||
email: profile.email,
|
||||
oauthId: profile.sub,
|
||||
quotaSizeInBytes: storageQuota * HumanReadableSize.GiB || null,
|
||||
storageLabel: storageLabel || null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.createLoginResponse(user, loginDetails);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import { BadRequestException, Inject } from '@nestjs/common';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IActivityRepository } from 'src/interfaces/activity.interface';
|
||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||
@@ -122,28 +119,4 @@ export class BaseService {
|
||||
checkAccess(request: AccessRequest) {
|
||||
return checkAccess(this.accessRepository, request);
|
||||
}
|
||||
|
||||
async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {
|
||||
const user = await this.userRepository.getByEmail(dto.email);
|
||||
if (user) {
|
||||
throw new BadRequestException('User exists');
|
||||
}
|
||||
|
||||
if (!dto.isAdmin) {
|
||||
const localAdmin = await this.userRepository.getAdmin();
|
||||
if (!localAdmin) {
|
||||
throw new BadRequestException('The first registered account must the administrator.');
|
||||
}
|
||||
}
|
||||
|
||||
const payload: Partial<UserEntity> = { ...dto };
|
||||
if (payload.password) {
|
||||
payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
|
||||
}
|
||||
if (payload.storageLabel) {
|
||||
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
||||
}
|
||||
|
||||
return this.userRepository.create(payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,14 +60,11 @@ describe(DatabaseService.name, () => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
config: {
|
||||
type: 'postgres',
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'immich',
|
||||
},
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
skipMigrations: false,
|
||||
vectorExtension: extension,
|
||||
},
|
||||
@@ -289,14 +286,11 @@ describe(DatabaseService.name, () => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
config: {
|
||||
type: 'postgres',
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'immich',
|
||||
},
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
skipMigrations: true,
|
||||
vectorExtension: DatabaseExtension.VECTORS,
|
||||
},
|
||||
@@ -312,14 +306,11 @@ describe(DatabaseService.name, () => {
|
||||
configMock.getEnv.mockReturnValue(
|
||||
mockEnvData({
|
||||
database: {
|
||||
config: {
|
||||
type: 'postgres',
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
database: 'immich',
|
||||
},
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
skipMigrations: true,
|
||||
vectorExtension: DatabaseExtension.VECTOR,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Stats } from 'node:fs';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { mapLibrary } from 'src/dtos/library.dto';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { AssetType, ImmichWorker } from 'src/enum';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import {
|
||||
@@ -55,7 +55,7 @@ describe(LibraryService.name, () => {
|
||||
it('should init cron job and handle config changes', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryScan);
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
|
||||
expect(jobMock.addCronJob).toHaveBeenCalled();
|
||||
expect(systemMock.get).toHaveBeenCalled();
|
||||
@@ -91,7 +91,7 @@ describe(LibraryService.name, () => {
|
||||
),
|
||||
);
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
|
||||
expect(storageMock.watch.mock.calls).toEqual(
|
||||
expect.arrayContaining([
|
||||
@@ -104,7 +104,7 @@ describe(LibraryService.name, () => {
|
||||
it('should not initialize watcher when watching is disabled', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
|
||||
expect(storageMock.watch).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -113,32 +113,17 @@ describe(LibraryService.name, () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
databaseMock.tryLock.mockResolvedValue(false);
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
|
||||
expect(storageMock.watch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not initialize library scan cron job when lock is taken', async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
databaseMock.tryLock.mockResolvedValue(false);
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
|
||||
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not initialize watcher or library scan job when running on api', async () => {
|
||||
await sut.onBootstrap(ImmichWorker.API);
|
||||
|
||||
expect(jobMock.addCronJob).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onConfigUpdateEvent', () => {
|
||||
beforeEach(async () => {
|
||||
systemMock.get.mockResolvedValue(defaults);
|
||||
databaseMock.tryLock.mockResolvedValue(true);
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
});
|
||||
|
||||
it('should do nothing if oldConfig is not provided', async () => {
|
||||
@@ -148,7 +133,7 @@ describe(LibraryService.name, () => {
|
||||
|
||||
it('should do nothing if instance does not have the watch lock', async () => {
|
||||
databaseMock.tryLock.mockResolvedValue(false);
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
await sut.onConfigUpdate({ newConfig: systemConfigStub.libraryScan as SystemConfig, oldConfig: defaults });
|
||||
expect(jobMock.updateCronJob).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -708,7 +693,7 @@ describe(LibraryService.name, () => {
|
||||
const mockClose = vitest.fn();
|
||||
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
await sut.delete(libraryStub.externalLibraryWithImportPaths1.id);
|
||||
|
||||
expect(mockClose).toHaveBeenCalled();
|
||||
@@ -842,7 +827,7 @@ describe(LibraryService.name, () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.getAll.mockResolvedValue([]);
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
await sut.create({
|
||||
ownerId: authStub.admin.user.id,
|
||||
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||
@@ -905,7 +890,7 @@ describe(LibraryService.name, () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
libraryMock.getAll.mockResolvedValue([]);
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
});
|
||||
|
||||
it('should throw an error if an import path is invalid', async () => {
|
||||
@@ -946,7 +931,7 @@ describe(LibraryService.name, () => {
|
||||
beforeEach(async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled);
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
});
|
||||
|
||||
it('should not watch library', async () => {
|
||||
@@ -962,7 +947,7 @@ describe(LibraryService.name, () => {
|
||||
beforeEach(async () => {
|
||||
systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
libraryMock.getAll.mockResolvedValue([]);
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
});
|
||||
|
||||
it('should watch library', async () => {
|
||||
@@ -1128,7 +1113,7 @@ describe(LibraryService.name, () => {
|
||||
const mockClose = vitest.fn();
|
||||
storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose }));
|
||||
|
||||
await sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||
await sut.onBootstrap();
|
||||
await sut.onShutdown();
|
||||
|
||||
expect(mockClose).toHaveBeenCalledTimes(2);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from 'src/dtos/library.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { AssetType, ImmichWorker } from 'src/enum';
|
||||
import { AssetType } from 'src/enum';
|
||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||
import { ArgOf } from 'src/interfaces/event.interface';
|
||||
import {
|
||||
@@ -36,32 +36,27 @@ import { validateCronExpression } from 'src/validation';
|
||||
@Injectable()
|
||||
export class LibraryService extends BaseService {
|
||||
private watchLibraries = false;
|
||||
private lock = false;
|
||||
private watchLock = false;
|
||||
private watchers: Record<string, () => Promise<void>> = {};
|
||||
|
||||
@OnEvent({ name: 'app.bootstrap' })
|
||||
async onBootstrap(workerType: ImmichWorker) {
|
||||
if (workerType !== ImmichWorker.MICROSERVICES) {
|
||||
return;
|
||||
}
|
||||
|
||||
async onBootstrap() {
|
||||
const config = await this.getConfig({ withCache: false });
|
||||
|
||||
const { watch, scan } = config.library;
|
||||
|
||||
// This ensures that library watching only occurs in one microservice
|
||||
this.lock = await this.databaseRepository.tryLock(DatabaseLock.Library);
|
||||
// TODO: we could make the lock be per-library instead of global
|
||||
this.watchLock = await this.databaseRepository.tryLock(DatabaseLock.LibraryWatch);
|
||||
|
||||
this.watchLibraries = this.lock && watch.enabled;
|
||||
this.watchLibraries = this.watchLock && watch.enabled;
|
||||
|
||||
if (this.lock) {
|
||||
this.jobRepository.addCronJob(
|
||||
'libraryScan',
|
||||
scan.cronExpression,
|
||||
() => handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL }), this.logger),
|
||||
scan.enabled,
|
||||
);
|
||||
}
|
||||
this.jobRepository.addCronJob(
|
||||
'libraryScan',
|
||||
scan.cronExpression,
|
||||
() => handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL }), this.logger),
|
||||
scan.enabled,
|
||||
);
|
||||
|
||||
if (this.watchLibraries) {
|
||||
await this.watchAll();
|
||||
@@ -70,7 +65,7 @@ export class LibraryService extends BaseService {
|
||||
|
||||
@OnEvent({ name: 'config.update', server: true })
|
||||
async onConfigUpdate({ newConfig: { library }, oldConfig }: ArgOf<'config.update'>) {
|
||||
if (!oldConfig || !this.lock) {
|
||||
if (!oldConfig || !this.watchLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,7 +180,7 @@ export class LibraryService extends BaseService {
|
||||
}
|
||||
|
||||
private async unwatchAll() {
|
||||
if (!this.lock) {
|
||||
if (!this.watchLock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -195,7 +190,7 @@ export class LibraryService extends BaseService {
|
||||
}
|
||||
|
||||
async watchAll() {
|
||||
if (!this.lock) {
|
||||
if (!this.watchLock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ const responseDto: PersonResponseDto = {
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
isHidden: false,
|
||||
updatedAt: expect.any(Date),
|
||||
withArchived: false,
|
||||
};
|
||||
|
||||
const statistics = { assets: 3 };
|
||||
@@ -118,6 +119,7 @@ describe(PersonService.name, () => {
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
isHidden: true,
|
||||
updatedAt: expect.any(Date),
|
||||
withArchived: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -218,6 +220,16 @@ describe(PersonService.name, () => {
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it("should update a person's withArchived", async () => {
|
||||
personMock.update.mockResolvedValue(personStub.withName);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
|
||||
await expect(sut.update(authStub.admin, 'person-1', { withArchived: true })).resolves.toEqual(responseDto);
|
||||
|
||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', withArchived: true });
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it("should update a person's date of birth", async () => {
|
||||
personMock.update.mockResolvedValue(personStub.withBirthDate);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
@@ -229,6 +241,7 @@ describe(PersonService.name, () => {
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
isHidden: false,
|
||||
updatedAt: expect.any(Date),
|
||||
withArchived: false,
|
||||
});
|
||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: '1976-06-30' });
|
||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||
@@ -381,6 +394,7 @@ describe(PersonService.name, () => {
|
||||
name: personStub.noName.name,
|
||||
thumbnailPath: personStub.noName.thumbnailPath,
|
||||
updatedAt: expect.any(Date),
|
||||
withArchived: personStub.noName.withArchived,
|
||||
});
|
||||
|
||||
expect(jobMock.queue).not.toHaveBeenCalledWith();
|
||||
@@ -1171,13 +1185,13 @@ describe(PersonService.name, () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.getStatistics.mockResolvedValue(statistics);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 });
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1', {})).resolves.toEqual({ assets: 3 });
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it('should require person.read permission', async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.getStatistics(authStub.admin, 'person-1', {})).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
PersonResponseDto,
|
||||
PersonSearchDto,
|
||||
PersonStatisticsResponseDto,
|
||||
PersonStatsDto,
|
||||
PersonUpdateDto,
|
||||
mapFaces,
|
||||
mapPerson,
|
||||
@@ -153,9 +154,9 @@ export class PersonService extends BaseService {
|
||||
return this.findOrFail(id).then(mapPerson);
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
||||
async getStatistics(auth: AuthDto, id: string, dto: PersonStatsDto): Promise<PersonStatisticsResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_READ, ids: [id] });
|
||||
return this.personRepository.getStatistics(id);
|
||||
return this.personRepository.getStatistics(id, dto);
|
||||
}
|
||||
|
||||
async getThumbnail(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
@@ -184,7 +185,7 @@ export class PersonService extends BaseService {
|
||||
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
|
||||
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId, withArchived } = dto;
|
||||
// TODO: set by faceId directly
|
||||
let faceId: string | undefined = undefined;
|
||||
if (assetId) {
|
||||
@@ -197,7 +198,14 @@ export class PersonService extends BaseService {
|
||||
faceId = face.id;
|
||||
}
|
||||
|
||||
const person = await this.personRepository.update({ id, faceAssetId: faceId, name, birthDate, isHidden });
|
||||
const person = await this.personRepository.update({
|
||||
id,
|
||||
faceAssetId: faceId,
|
||||
name,
|
||||
birthDate,
|
||||
isHidden,
|
||||
withArchived,
|
||||
});
|
||||
|
||||
if (assetId) {
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
||||
|
||||
@@ -15,6 +15,7 @@ import { JobName } from 'src/interfaces/job.interface';
|
||||
import { UserFindOptions } from 'src/interfaces/user.interface';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences';
|
||||
import { createUser } from 'src/utils/user';
|
||||
|
||||
@Injectable()
|
||||
export class UserAdminService extends BaseService {
|
||||
@@ -24,18 +25,17 @@ export class UserAdminService extends BaseService {
|
||||
}
|
||||
|
||||
async create(dto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
|
||||
const { notify, ...userDto } = dto;
|
||||
const { notify, ...rest } = dto;
|
||||
const config = await this.getConfig({ withCache: false });
|
||||
if (!config.oauth.enabled && !userDto.password) {
|
||||
if (!config.oauth.enabled && !rest.password) {
|
||||
throw new BadRequestException('password is required');
|
||||
}
|
||||
|
||||
const user = await this.createUser(userDto);
|
||||
const user = await createUser({ userRepo: this.userRepository, cryptoRepo: this.cryptoRepository }, rest);
|
||||
|
||||
await this.eventRepository.emit('user.signup', {
|
||||
notify: !!notify,
|
||||
id: user.id,
|
||||
tempPassword: user.shouldChangePassword ? userDto.password : undefined,
|
||||
tempPassword: user.shouldChangePassword ? rest.password : undefined,
|
||||
});
|
||||
|
||||
return mapUserAdmin(user);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
|
||||
type RepoDeps = { userRepo: IUserRepository; cryptoRepo: ICryptoRepository };
|
||||
|
||||
export const createUser = async (
|
||||
{ userRepo, cryptoRepo }: RepoDeps,
|
||||
dto: Partial<UserEntity> & { email: string },
|
||||
): Promise<UserEntity> => {
|
||||
const user = await userRepo.getByEmail(dto.email);
|
||||
if (user) {
|
||||
throw new BadRequestException('User exists');
|
||||
}
|
||||
|
||||
if (!dto.isAdmin) {
|
||||
const localAdmin = await userRepo.getAdmin();
|
||||
if (!localAdmin) {
|
||||
throw new BadRequestException('The first registered account must the administrator.');
|
||||
}
|
||||
}
|
||||
|
||||
const payload: Partial<UserEntity> = { ...dto };
|
||||
if (payload.password) {
|
||||
payload.password = await cryptoRepo.hashBcrypt(payload.password, SALT_ROUNDS);
|
||||
}
|
||||
if (payload.storageLabel) {
|
||||
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
||||
}
|
||||
|
||||
return userRepo.create(payload);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ async function bootstrap() {
|
||||
process.title = 'immich-api';
|
||||
|
||||
const { telemetry, network } = new ConfigRepository().getEnv();
|
||||
if (telemetry.metrics.size > 0) {
|
||||
if (telemetry.enabled) {
|
||||
bootstrapTelemetry(telemetry.apiPort);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { isStartUpError } from 'src/services/storage.service';
|
||||
|
||||
export async function bootstrap() {
|
||||
const { telemetry } = new ConfigRepository().getEnv();
|
||||
if (telemetry.metrics.size > 0) {
|
||||
if (telemetry.enabled) {
|
||||
bootstrapTelemetry(telemetry.microservicesPort);
|
||||
}
|
||||
|
||||
|
||||
Vendored
+9
@@ -15,6 +15,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
hidden: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@@ -29,6 +30,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: true,
|
||||
withArchived: false,
|
||||
}),
|
||||
withName: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@@ -43,6 +45,7 @@ export const personStub = {
|
||||
faceAssetId: 'assetFaceId',
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
withBirthDate: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@@ -57,6 +60,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
noThumbnail: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@@ -71,6 +75,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
newThumbnail: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@@ -85,6 +90,7 @@ export const personStub = {
|
||||
faceAssetId: 'asset-id',
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
primaryPerson: Object.freeze<PersonEntity>({
|
||||
id: 'person-1',
|
||||
@@ -99,6 +105,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
mergePerson: Object.freeze<PersonEntity>({
|
||||
id: 'person-2',
|
||||
@@ -113,6 +120,7 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
randomPerson: Object.freeze<PersonEntity>({
|
||||
id: 'person-3',
|
||||
@@ -127,5 +135,6 @@ export const personStub = {
|
||||
faceAssetId: null,
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
withArchived: false,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -16,16 +16,11 @@ const envData: EnvData = {
|
||||
},
|
||||
|
||||
database: {
|
||||
config: {
|
||||
type: 'postgres',
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
synchronize: false,
|
||||
migrationsRun: true,
|
||||
},
|
||||
host: 'database',
|
||||
port: 5432,
|
||||
username: 'postgres',
|
||||
password: 'postgres',
|
||||
name: 'immich',
|
||||
|
||||
skipMigrations: false,
|
||||
vectorExtension: DatabaseExtension.VECTORS,
|
||||
@@ -78,7 +73,11 @@ const envData: EnvData = {
|
||||
telemetry: {
|
||||
apiPort: 8081,
|
||||
microservicesPort: 8082,
|
||||
metrics: new Set(),
|
||||
enabled: false,
|
||||
hostMetrics: false,
|
||||
apiMetrics: false,
|
||||
jobMetrics: false,
|
||||
repoMetrics: false,
|
||||
},
|
||||
|
||||
workers: [ImmichWorker.API, ImmichWorker.MICROSERVICES],
|
||||
|
||||
Generated
+4
-4
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich-web",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "^2.7.8",
|
||||
@@ -74,13 +74,13 @@
|
||||
},
|
||||
"../open-api/typescript-sdk": {
|
||||
"name": "@immich/sdk",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"dependencies": {
|
||||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.17.0",
|
||||
"@types/node": "^20.16.12",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "1.119.1",
|
||||
"version": "1.118.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"scripts": {
|
||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||
|
||||
+33
-3
@@ -45,6 +45,8 @@
|
||||
import {
|
||||
mdiAccountBoxOutline,
|
||||
mdiAccountMultipleCheckOutline,
|
||||
mdiArchiveArrowDown,
|
||||
mdiArchiveArrowDownOutline,
|
||||
mdiArrowLeft,
|
||||
mdiCalendarEditOutline,
|
||||
mdiDotsVertical,
|
||||
@@ -72,8 +74,10 @@
|
||||
UNASSIGN_ASSETS = 'unassign-faces',
|
||||
}
|
||||
|
||||
$: isArchived = person.withArchived ? undefined : person.withArchived;
|
||||
|
||||
let assetStore = new AssetStore({
|
||||
isArchived: false,
|
||||
isArchived,
|
||||
personId: data.person.id,
|
||||
});
|
||||
|
||||
@@ -81,7 +85,7 @@
|
||||
$: thumbnailData = getPeopleThumbnailUrl(person);
|
||||
$: if (person) {
|
||||
handlePromiseError(updateAssetCount());
|
||||
handlePromiseError(assetStore.updateOptions({ personId: person.id }));
|
||||
handlePromiseError(assetStore.updateOptions({ personId: person.id, isArchived }));
|
||||
}
|
||||
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
@@ -338,6 +342,27 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleWithArhived = async () => {
|
||||
const withArchived = !person.withArchived;
|
||||
|
||||
try {
|
||||
await updatePerson({
|
||||
id: person.id,
|
||||
personUpdateDto: { withArchived },
|
||||
});
|
||||
|
||||
data.person.withArchived = withArchived;
|
||||
refreshAssetGrid = !refreshAssetGrid;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_archive_unarchive', { values: { archived: withArchived } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAssets = async (assetIds: string[]) => {
|
||||
$assetStore.removeAssets(assetIds);
|
||||
await updateAssetCount();
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
assetStore.destroy();
|
||||
});
|
||||
@@ -395,7 +420,7 @@
|
||||
<ChangeDate menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem unarchive={isAllArchive} onArchive={(assetIds) => $assetStore.removeAssets(assetIds)} />
|
||||
<DeleteAssets menuItem onAssetDelete={(assetIds) => $assetStore.removeAssets(assetIds)} />
|
||||
<DeleteAssets menuItem onAssetDelete={handleDeleteAssets} />
|
||||
</ButtonContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
@@ -423,6 +448,11 @@
|
||||
icon={mdiAccountMultipleCheckOutline}
|
||||
onClick={() => (viewMode = ViewMode.MERGE_PEOPLE)}
|
||||
/>
|
||||
<MenuOption
|
||||
text={$t('show_archived_or_unarchived_assets', { values: { with: !person.withArchived } })}
|
||||
icon={person.withArchived ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline}
|
||||
onClick={handleToggleWithArhived}
|
||||
/>
|
||||
</ButtonContextMenu>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { afterUpdate, tick } from 'svelte';
|
||||
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
||||
|
||||
const MAX_ASSET_COUNT = 5000;
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
@@ -247,8 +246,6 @@
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem unarchive={isAllArchived} onArchive={triggerAssetUpdate} />
|
||||
<DeleteAssets menuItem {onAssetDelete} />
|
||||
<hr />
|
||||
<AssetJobActions />
|
||||
</ButtonContextMenu>
|
||||
</AssetSelectControlBar>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>{$page.data.meta?.title || 'Web'} - Immich</title>
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="theme-color" content="currentColor" />
|
||||
<AppleHeader />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user