mirror of
https://github.com/immich-app/immich.git
synced 2025-12-13 00:01:09 -08:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc143046e3 | ||
|
|
e684062569 | ||
|
|
5c0538e52c | ||
|
|
84cf0d1670 | ||
|
|
bfcde05b1c | ||
|
|
b3b15e9b61 | ||
|
|
819e56d9ca | ||
|
|
9a98712db7 | ||
|
|
a185e06399 | ||
|
|
f2be9f7ad1 | ||
|
|
5c879acd5b | ||
|
|
28c664c769 | ||
|
|
fbd85a89e0 | ||
|
|
1c86293035 | ||
|
|
4a9d80298b | ||
|
|
362feb1e62 | ||
|
|
5503bf7a60 | ||
|
|
d20e2e268a | ||
|
|
a708649504 | ||
|
|
a808b8610e | ||
|
|
c70c9067b0 | ||
|
|
082471dfd9 | ||
|
|
9a098b4658 | ||
|
|
9d705097e8 | ||
|
|
6050485ad8 | ||
|
|
fb907d707d | ||
|
|
7d6cfd09e6 | ||
|
|
967c69317b | ||
|
|
128d653fc6 | ||
|
|
8b69114924 | ||
|
|
4b55888d16 | ||
|
|
8fbd650483 | ||
|
|
c778516ce2 | ||
|
|
2969e25ff7 | ||
|
|
c055e1aefe |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
|||||||
custom: ['https://buy.immich.app']
|
custom: ['https://buy.immich.app', 'https://immich.store']
|
||||||
|
|||||||
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.51",
|
"version": "2.2.52",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.51",
|
"version": "2.2.52",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.51",
|
"version": "2.2.52",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ Make sure to [set your reverse proxy](/docs/administration/reverse-proxy/) to al
|
|||||||
Also, check the disk space of your reverse proxy.
|
Also, check the disk space of your reverse proxy.
|
||||||
In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
|
In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
|
||||||
|
|
||||||
If you are using Cloudflare Tunnel, please know that they set a maxiumum filesize of 100 MB that cannot be changed.
|
If you are using Cloudflare Tunnel, please know that they set a maximum filesize of 100 MB that cannot be changed.
|
||||||
At times, files larger than this may work, potentially up to 1 GB. However, the official limit is 100 MB.
|
At times, files larger than this may work, potentially up to 1 GB. However, the official limit is 100 MB.
|
||||||
If you are having issues, we recommend switching to a different network deployment.
|
If you are having issues, we recommend switching to a different network deployment.
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ If you aren't able to or prefer not to mount Samba on the host (such as Windows
|
|||||||
Below is an example in the `docker-compose.yml`.
|
Below is an example in the `docker-compose.yml`.
|
||||||
|
|
||||||
Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`,
|
Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`,
|
||||||
corrolates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like.
|
correlates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like.
|
||||||
For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`.
|
For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`.
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ The default Immich log level is `Log` (commonly known as `Info`). The Immich adm
|
|||||||
Through this setting, you can manage all the settings related to machine learning in Immich, from the setting of remote machine learning to the model and its parameters
|
Through this setting, you can manage all the settings related to machine learning in Immich, from the setting of remote machine learning to the model and its parameters
|
||||||
You can choose to disable a certain type of machine learning, for example smart search or facial recognition.
|
You can choose to disable a certain type of machine learning, for example smart search or facial recognition.
|
||||||
|
|
||||||
|
### URL
|
||||||
|
|
||||||
|
The built in (`http://immich-machine-learning:3003`) machine learning server will be configured by default, but you can change this or add additional servers.
|
||||||
|
|
||||||
|
Hosting the `immich-machine-learning` container on a machine with a more powerful GPU can be helpful to for processing a large number of photos (such as during batch import) or for faster search.
|
||||||
|
|
||||||
|
If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.
|
||||||
|
|
||||||
### Smart Search
|
### Smart Search
|
||||||
|
|
||||||
The [smart search](/docs/features/searching) settings allow you to change the [CLIP model](https://openai.com/research/clip). Larger models will typically provide [more accurate search results](https://github.com/immich-app/immich/discussions/11862) but consume more processing power and RAM. When [changing the CLIP model](/docs/FAQ#can-i-use-a-custom-clip-model) it is mandatory to re-run the Smart Search job on all images to fully apply the change.
|
The [smart search](/docs/features/searching) settings allow you to change the [CLIP model](https://openai.com/research/clip). Larger models will typically provide [more accurate search results](https://github.com/immich-app/immich/discussions/11862) but consume more processing power and RAM. When [changing the CLIP model](/docs/FAQ#can-i-use-a-custom-clip-model) it is mandatory to re-run the Smart Search job on all images to fully apply the change.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ You can alternatively download these two files from your browser and move them t
|
|||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
|
|
||||||
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space.
|
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space.
|
||||||
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication.
|
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publicly exposed, so this password is only used for local authentication.
|
||||||
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this.
|
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this.
|
||||||
- Set your timezone by uncommenting the `TZ=` line.
|
- Set your timezone by uncommenting the `TZ=` line.
|
||||||
- Populate custom database information if necessary.
|
- Populate custom database information if necessary.
|
||||||
|
|||||||
@@ -168,6 +168,8 @@ Redis (Sentinel) URL example JSON before encoding:
|
|||||||
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
||||||
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
||||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
||||||
|
| `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server |
|
||||||
|
| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server |
|
||||||
|
|
||||||
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
||||||
|
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ The **CPU** value was specified in a different format with a default of `4000m`
|
|||||||
The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000`
|
The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passtrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough)
|
Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passthrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough)
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
|
|||||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -1,4 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.128.0",
|
||||||
|
"url": "https://v1.128.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.127.0",
|
"label": "v1.127.0",
|
||||||
"url": "https://v1.127.0.archive.immich.app"
|
"url": "https://v1.127.0.archive.immich.app"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module.exports = {
|
|||||||
preflight: false, // disable Tailwind's reset
|
preflight: false, // disable Tailwind's reset
|
||||||
},
|
},
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}', './{docs,blog}/**/*.{md,mdx}'], // my markdown stuff is in ../docs, not /src
|
content: ['./src/**/*.{js,jsx,ts,tsx}', './{docs,blog}/**/*.{md,mdx}'], // my markdown stuff is in ../docs, not /src
|
||||||
darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns
|
darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settings
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
|||||||
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
},
|
},
|
||||||
"../cli": {
|
"../cli": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.51",
|
"version": "2.2.52",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -92,7 +92,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -526,6 +526,47 @@ describe('/libraries', () => {
|
|||||||
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not reimport a modified file more than once', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/reimport`],
|
||||||
|
});
|
||||||
|
|
||||||
|
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||||
|
|
||||||
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
|
||||||
|
|
||||||
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/albums/nature/el_torcal_rocks.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
|
||||||
|
|
||||||
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
|
libraryId: library.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assets.count).toEqual(1);
|
||||||
|
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
|
|
||||||
|
expect(asset).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'asset.jpg',
|
||||||
|
exifInfo: expect.objectContaining({
|
||||||
|
model: 'NIKON D750',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
|
});
|
||||||
|
|
||||||
it('should set an asset offline if its file is missing', async () => {
|
it('should set an asset offline if its file is missing', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoginResponseDto, getAssetInfo, getAssetStatistics, scanLibrary } from '@immich/sdk';
|
import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
@@ -6,8 +6,6 @@ import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'sr
|
|||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
|
|
||||||
|
|
||||||
describe('/trash', () => {
|
describe('/trash', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let ws: Socket;
|
let ws: Socket;
|
||||||
@@ -81,8 +79,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
expect(assets.items.length).toBe(1);
|
expect(assets.items.length).toBe(1);
|
||||||
@@ -90,8 +87,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
|
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
|
||||||
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
|
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
|
||||||
@@ -116,8 +112,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
expect(assets.items.length).toBe(1);
|
expect(assets.items.length).toBe(1);
|
||||||
@@ -125,8 +120,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
|
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
|
||||||
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
|
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
|
||||||
@@ -180,8 +174,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
expect(assets.count).toBe(1);
|
expect(assets.count).toBe(1);
|
||||||
@@ -189,9 +182,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
|
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
|
||||||
@@ -201,6 +192,8 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
|
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
|
||||||
|
|
||||||
|
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,7 +231,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
@@ -247,7 +240,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
|
||||||
const before = await utils.getAssetInfo(admin.accessToken, assetId);
|
const before = await utils.getAssetInfo(admin.accessToken, assetId);
|
||||||
@@ -261,6 +254,8 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
const after = await utils.getAssetInfo(admin.accessToken, assetId);
|
const after = await utils.getAssetInfo(admin.accessToken, assetId);
|
||||||
expect(after.isTrashed).toBe(true);
|
expect(after.isTrashed).toBe(true);
|
||||||
|
|
||||||
|
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
"machine_learning_smart_search_description": "Search for images semantically using CLIP embeddings",
|
"machine_learning_smart_search_description": "Search for images semantically using CLIP embeddings",
|
||||||
"machine_learning_smart_search_enabled": "Enable smart search",
|
"machine_learning_smart_search_enabled": "Enable smart search",
|
||||||
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
||||||
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last.",
|
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.",
|
||||||
"manage_concurrency": "Manage Concurrency",
|
"manage_concurrency": "Manage Concurrency",
|
||||||
"manage_log_settings": "Manage log settings",
|
"manage_log_settings": "Manage log settings",
|
||||||
"map_dark_style": "Dark style",
|
"map_dark_style": "Dark style",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.127.0"
|
version = "1.128.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ custom_lint:
|
|||||||
- lib/entities/*.entity.dart
|
- lib/entities/*.entity.dart
|
||||||
- lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart
|
- lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart
|
||||||
- lib/infrastructure/entities/*.entity.dart
|
- lib/infrastructure/entities/*.entity.dart
|
||||||
- lib/infrastructure/repositories/{store,db}.repository.dart
|
- lib/infrastructure/repositories/{store,db,log}.repository.dart
|
||||||
- lib/providers/infrastructure/db.provider.dart
|
- lib/providers/infrastructure/db.provider.dart
|
||||||
# acceptable exceptions for the time being (until Isar is fully replaced)
|
# acceptable exceptions for the time being (until Isar is fully replaced)
|
||||||
- integration_test/test_utils/general_helper.dart
|
- integration_test/test_utils/general_helper.dart
|
||||||
@@ -76,17 +76,17 @@ custom_lint:
|
|||||||
- lib/routing/router.dart
|
- lib/routing/router.dart
|
||||||
- lib/services/immich_logger.service.dart # not really a service... more a util
|
- lib/services/immich_logger.service.dart # not really a service... more a util
|
||||||
- lib/utils/{db,migration}.dart
|
- lib/utils/{db,migration}.dart
|
||||||
|
- lib/utils/bootstrap.dart
|
||||||
- lib/widgets/asset_grid/asset_grid_data_structure.dart
|
- lib/widgets/asset_grid/asset_grid_data_structure.dart
|
||||||
- test/**.dart
|
- test/**.dart
|
||||||
# refactor the remaining providers
|
# refactor the remaining providers
|
||||||
- lib/providers/{db,user}.provider.dart
|
- lib/providers/db.provider.dart
|
||||||
- lib/providers/backup/backup.provider.dart
|
|
||||||
|
|
||||||
- import_rule_openapi:
|
- import_rule_openapi:
|
||||||
message: openapi must only be used through ApiRepositories
|
message: openapi must only be used through ApiRepositories
|
||||||
restrict: package:openapi
|
restrict: package:openapi
|
||||||
allowed:
|
allowed:
|
||||||
# requried / wanted
|
# required / wanted
|
||||||
- lib/repositories/*_api.repository.dart
|
- lib/repositories/*_api.repository.dart
|
||||||
# acceptable exceptions for the time being
|
# acceptable exceptions for the time being
|
||||||
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
|
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 185,
|
"android.injected.version.code" => 186,
|
||||||
"android.injected.version.name" => "1.127.0",
|
"android.injected.version.name" => "1.128.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
|||||||
import 'package:immich_mobile/main.dart' as app;
|
import 'package:immich_mobile/main.dart' as app;
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
import 'package:integration_test/integration_test.dart';
|
import 'package:integration_test/integration_test.dart';
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
// ignore: depend_on_referenced_packages
|
// ignore: depend_on_referenced_packages
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
@@ -39,7 +39,8 @@ class ImmichTestHelper {
|
|||||||
static Future<void> loadApp(WidgetTester tester) async {
|
static Future<void> loadApp(WidgetTester tester) async {
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
// Clear all data from Isar (reuse existing instance if available)
|
// Clear all data from Isar (reuse existing instance if available)
|
||||||
final db = Isar.getInstance() ?? await app.loadDb();
|
final db = await Bootstrap.initIsar();
|
||||||
|
await Bootstrap.initDomain(db);
|
||||||
await Store.clear();
|
await Store.clear();
|
||||||
await db.writeTxn(() => db.clear());
|
await db.writeTxn(() => db.clear());
|
||||||
// Load main Widget
|
// Load main Widget
|
||||||
|
|||||||
@@ -541,7 +541,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -685,7 +685,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -715,7 +715,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -748,7 +748,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -791,7 +791,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -831,7 +831,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 194;
|
CURRENT_PROJECT_VERSION = 195;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.126.1</string>
|
<string>1.127.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>194</string>
|
<string>195</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Release"
|
desc "iOS Release"
|
||||||
lane :release do
|
lane :release do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.127.0"
|
version_number: "1.128.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
const int noDbId = -9223372036854775808; // from Isar
|
const int noDbId = -9223372036854775808; // from Isar
|
||||||
const double downloadCompleted = -1;
|
const double downloadCompleted = -1;
|
||||||
const double downloadFailed = -2;
|
const double downloadFailed = -2;
|
||||||
|
|
||||||
|
// Number of log entries to retain on app start
|
||||||
|
const int kLogTruncateLimit = 250;
|
||||||
|
|||||||
16
mobile/lib/domain/interfaces/log.interface.dart
Normal file
16
mobile/lib/domain/interfaces/log.interface.dart
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
|
||||||
|
abstract interface class ILogRepository {
|
||||||
|
Future<bool> insert(LogMessage log);
|
||||||
|
|
||||||
|
Future<bool> insertAll(Iterable<LogMessage> logs);
|
||||||
|
|
||||||
|
Future<List<LogMessage>> getAll();
|
||||||
|
|
||||||
|
Future<bool> deleteAll();
|
||||||
|
|
||||||
|
/// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs
|
||||||
|
Future<void> truncate({int limit = 250});
|
||||||
|
}
|
||||||
69
mobile/lib/domain/models/log.model.dart
Normal file
69
mobile/lib/domain/models/log.model.dart
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
/// Log levels according to dart logging [Level]
|
||||||
|
enum LogLevel {
|
||||||
|
ALL,
|
||||||
|
FINEST,
|
||||||
|
FINER,
|
||||||
|
FINE,
|
||||||
|
CONFIG,
|
||||||
|
INFO,
|
||||||
|
WARNING,
|
||||||
|
SEVERE,
|
||||||
|
SHOUT,
|
||||||
|
OFF,
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogMessage {
|
||||||
|
final String message;
|
||||||
|
final LogLevel level;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final String? logger;
|
||||||
|
final String? error;
|
||||||
|
final String? stack;
|
||||||
|
|
||||||
|
const LogMessage({
|
||||||
|
required this.message,
|
||||||
|
required this.level,
|
||||||
|
required this.createdAt,
|
||||||
|
this.logger,
|
||||||
|
this.error,
|
||||||
|
this.stack,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant LogMessage other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.message == message &&
|
||||||
|
other.level == level &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.logger == logger &&
|
||||||
|
other.error == error &&
|
||||||
|
other.stack == stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return message.hashCode ^
|
||||||
|
level.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
logger.hashCode ^
|
||||||
|
error.hashCode ^
|
||||||
|
stack.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''LogMessage: {
|
||||||
|
message: $message,
|
||||||
|
level: $level,
|
||||||
|
createdAt: $createdAt,
|
||||||
|
logger: ${logger ?? '<NA>'},
|
||||||
|
error: ${error ?? '<NA>'},
|
||||||
|
stack: ${stack ?? '<NA>'},
|
||||||
|
}''';
|
||||||
|
}
|
||||||
|
}
|
||||||
153
mobile/lib/domain/services/log.service.dart
Normal file
153
mobile/lib/domain/services/log.service.dart
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
class LogService {
|
||||||
|
final ILogRepository _logRepository;
|
||||||
|
final IStoreRepository _storeRepository;
|
||||||
|
|
||||||
|
final List<LogMessage> _msgBuffer = [];
|
||||||
|
|
||||||
|
/// Whether to buffer logs in memory before writing to the database.
|
||||||
|
/// This is useful when logging in quick succession, as it increases performance
|
||||||
|
/// and reduces NAND wear. However, it may cause the logs to be lost in case of a crash / in isolates.
|
||||||
|
final bool _shouldBuffer;
|
||||||
|
Timer? _flushTimer;
|
||||||
|
|
||||||
|
late final StreamSubscription<LogRecord> _logSubscription;
|
||||||
|
|
||||||
|
LogService._(
|
||||||
|
this._logRepository,
|
||||||
|
this._storeRepository,
|
||||||
|
this._shouldBuffer,
|
||||||
|
) {
|
||||||
|
// Listen to log messages and write them to the database
|
||||||
|
_logSubscription = Logger.root.onRecord.listen(_writeLogToDatabase);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LogService? _instance;
|
||||||
|
static LogService get I {
|
||||||
|
if (_instance == null) {
|
||||||
|
throw const LoggerUnInitializedException();
|
||||||
|
}
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LogService> init({
|
||||||
|
required ILogRepository logRepo,
|
||||||
|
required IStoreRepository storeRepo,
|
||||||
|
bool shouldBuffer = true,
|
||||||
|
}) async {
|
||||||
|
if (_instance != null) {
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
_instance = await create(
|
||||||
|
logRepo: logRepo,
|
||||||
|
storeRepo: storeRepo,
|
||||||
|
shouldBuffer: shouldBuffer,
|
||||||
|
);
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<LogService> create({
|
||||||
|
required ILogRepository logRepo,
|
||||||
|
required IStoreRepository storeRepo,
|
||||||
|
bool shouldBuffer = true,
|
||||||
|
}) async {
|
||||||
|
final instance = LogService._(logRepo, storeRepo, shouldBuffer);
|
||||||
|
// Truncate logs to 250
|
||||||
|
await logRepo.truncate(limit: kLogTruncateLimit);
|
||||||
|
// Get log level from store
|
||||||
|
final level = await instance._storeRepository.tryGet(StoreKey.logLevel);
|
||||||
|
if (level != null) {
|
||||||
|
Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO;
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setlogLevel(LogLevel level) async {
|
||||||
|
await _storeRepository.insert(StoreKey.logLevel, level.index);
|
||||||
|
Logger.root.level = level.toLevel();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<LogMessage>> getMessages() async {
|
||||||
|
final logsFromDb = await _logRepository.getAll();
|
||||||
|
if (_msgBuffer.isNotEmpty) {
|
||||||
|
return [..._msgBuffer.reversed, ...logsFromDb];
|
||||||
|
}
|
||||||
|
return logsFromDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearLogs() async {
|
||||||
|
_flushTimer?.cancel();
|
||||||
|
_flushTimer = null;
|
||||||
|
_msgBuffer.clear();
|
||||||
|
await _logRepository.deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flush pending log messages to persistent storage
|
||||||
|
Future<void> flush() async {
|
||||||
|
if (_flushTimer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_flushTimer!.cancel();
|
||||||
|
await _flushBufferToDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> dispose() {
|
||||||
|
_flushTimer?.cancel();
|
||||||
|
_logSubscription.cancel();
|
||||||
|
return _flushBufferToDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeLogToDatabase(LogRecord r) {
|
||||||
|
final record = LogMessage(
|
||||||
|
message: r.message,
|
||||||
|
level: r.level.toLogLevel(),
|
||||||
|
createdAt: r.time,
|
||||||
|
logger: r.loggerName,
|
||||||
|
error: r.error?.toString(),
|
||||||
|
stack: r.stackTrace?.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_shouldBuffer) {
|
||||||
|
_msgBuffer.add(record);
|
||||||
|
_flushTimer ??= Timer(
|
||||||
|
const Duration(seconds: 5),
|
||||||
|
() => unawaited(_flushBufferToDatabase()),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
unawaited(_logRepository.insert(record));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _flushBufferToDatabase() async {
|
||||||
|
_flushTimer = null;
|
||||||
|
final buffer = [..._msgBuffer];
|
||||||
|
_msgBuffer.clear();
|
||||||
|
await _logRepository.insertAll(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoggerUnInitializedException implements Exception {
|
||||||
|
const LoggerUnInitializedException();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'Logger is not initialized. Call init()';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Log levels according to dart logging [Level]
|
||||||
|
extension LevelDomainToInfraExtension on Level {
|
||||||
|
LogLevel toLogLevel() =>
|
||||||
|
LogLevel.values.elementAtOrNull(Level.LEVELS.indexOf(this)) ??
|
||||||
|
LogLevel.INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on LogLevel {
|
||||||
|
Level toLevel() => Level.LEVELS.elementAtOrNull(index) ?? Level.INFO;
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// ignore_for_file: constant_identifier_names
|
|
||||||
|
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
|
|
||||||
part 'logger_message.entity.g.dart';
|
|
||||||
|
|
||||||
@Collection(inheritance: false)
|
|
||||||
class LoggerMessage {
|
|
||||||
Id id = Isar.autoIncrement;
|
|
||||||
String message;
|
|
||||||
String? details;
|
|
||||||
@Enumerated(EnumType.ordinal)
|
|
||||||
LogLevel level = LogLevel.INFO;
|
|
||||||
DateTime createdAt;
|
|
||||||
String? context1;
|
|
||||||
String? context2;
|
|
||||||
|
|
||||||
LoggerMessage({
|
|
||||||
required this.message,
|
|
||||||
required this.details,
|
|
||||||
required this.level,
|
|
||||||
required this.createdAt,
|
|
||||||
required this.context1,
|
|
||||||
required this.context2,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'InAppLoggerMessage(message: $message, level: $level, createdAt: $createdAt)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log levels according to dart logging [Level]
|
|
||||||
enum LogLevel {
|
|
||||||
ALL,
|
|
||||||
FINEST,
|
|
||||||
FINER,
|
|
||||||
FINE,
|
|
||||||
CONFIG,
|
|
||||||
INFO,
|
|
||||||
WARNING,
|
|
||||||
SEVERE,
|
|
||||||
SHOUT,
|
|
||||||
OFF,
|
|
||||||
}
|
|
||||||
|
|
||||||
extension LevelExtension on Level {
|
|
||||||
LogLevel toLogLevel() => LogLevel.values[Level.LEVELS.indexOf(this)];
|
|
||||||
}
|
|
||||||
52
mobile/lib/infrastructure/entities/log.entity.dart
Normal file
52
mobile/lib/infrastructure/entities/log.entity.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
part 'log.entity.g.dart';
|
||||||
|
|
||||||
|
@Collection(inheritance: false)
|
||||||
|
class LoggerMessage {
|
||||||
|
Id id = Isar.autoIncrement;
|
||||||
|
String message;
|
||||||
|
String? details;
|
||||||
|
@Enumerated(EnumType.ordinal)
|
||||||
|
LogLevel level = LogLevel.INFO;
|
||||||
|
DateTime createdAt;
|
||||||
|
String? context1;
|
||||||
|
String? context2;
|
||||||
|
|
||||||
|
LoggerMessage({
|
||||||
|
required this.message,
|
||||||
|
required this.details,
|
||||||
|
required this.level,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.context1,
|
||||||
|
required this.context2,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'LoggerMessage(message: $message, level: $level, createdAt: $createdAt)';
|
||||||
|
}
|
||||||
|
|
||||||
|
LogMessage toDto() {
|
||||||
|
return LogMessage(
|
||||||
|
message: message,
|
||||||
|
level: level,
|
||||||
|
createdAt: createdAt,
|
||||||
|
logger: context1,
|
||||||
|
error: details,
|
||||||
|
stack: context2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LoggerMessage fromDto(LogMessage log) {
|
||||||
|
return LoggerMessage(
|
||||||
|
message: log.message,
|
||||||
|
details: log.error,
|
||||||
|
level: log.level,
|
||||||
|
createdAt: log.createdAt,
|
||||||
|
context1: log.logger,
|
||||||
|
context2: log.stack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
part of 'logger_message.entity.dart';
|
part of 'log.entity.dart';
|
||||||
|
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
// IsarCollectionGenerator
|
// IsarCollectionGenerator
|
||||||
53
mobile/lib/infrastructure/repositories/log.repository.dart
Normal file
53
mobile/lib/infrastructure/repositories/log.repository.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
class IsarLogRepository extends IsarDatabaseRepository
|
||||||
|
implements ILogRepository {
|
||||||
|
final Isar _db;
|
||||||
|
const IsarLogRepository(super.db) : _db = db;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> deleteAll() async {
|
||||||
|
await transaction(() async => await _db.loggerMessages.clear());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<LogMessage>> getAll() async {
|
||||||
|
final logs =
|
||||||
|
await _db.loggerMessages.where().sortByCreatedAtDesc().findAll();
|
||||||
|
return logs.map((l) => l.toDto()).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> insert(LogMessage log) async {
|
||||||
|
final logEntity = LoggerMessage.fromDto(log);
|
||||||
|
await transaction(() async {
|
||||||
|
await _db.loggerMessages.put(logEntity);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> insertAll(Iterable<LogMessage> logs) async {
|
||||||
|
await transaction(() async {
|
||||||
|
final logEntities =
|
||||||
|
logs.map((log) => LoggerMessage.fromDto(log)).toList();
|
||||||
|
await _db.loggerMessages.putAll(logEntities);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> truncate({int limit = 250}) async {
|
||||||
|
await transaction(() async {
|
||||||
|
final count = await _db.loggerMessages.count();
|
||||||
|
if (count <= limit) return;
|
||||||
|
final toRemove = count - limit;
|
||||||
|
await _db.loggerMessages.where().limit(toRemove).deleteAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/database.interface.dart';
|
import 'package:immich_mobile/interfaces/database.interface.dart';
|
||||||
|
|
||||||
abstract interface class IBackupRepository implements IDatabaseRepository {
|
abstract interface class IBackupAlbumRepository implements IDatabaseRepository {
|
||||||
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort});
|
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort});
|
||||||
|
|
||||||
Future<List<String>> getIdsBySelection(BackupSelection backup);
|
Future<List<String>> getIdsBySelection(BackupSelection backup);
|
||||||
@@ -22,6 +22,10 @@ abstract interface class IUserRepository implements IDatabaseRepository {
|
|||||||
Future<User> me();
|
Future<User> me();
|
||||||
|
|
||||||
Future<void> clearTable();
|
Future<void> clearTable();
|
||||||
|
|
||||||
|
Future<List<int>> getTimelineUserIds(int id);
|
||||||
|
|
||||||
|
Stream<List<int>> watchTimelineUsers(int id);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserSort { id }
|
enum UserSort { id }
|
||||||
|
|||||||
@@ -10,20 +10,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/locales.dart';
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
@@ -33,23 +20,22 @@ import 'package:immich_mobile/providers/theme.provider.dart';
|
|||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
||||||
import 'package:immich_mobile/theme/theme_data.dart';
|
import 'package:immich_mobile/theme/theme_data.dart';
|
||||||
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
||||||
import 'package:immich_mobile/utils/download.dart';
|
import 'package:immich_mobile/utils/download.dart';
|
||||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:timezone/data/latest.dart';
|
import 'package:timezone/data/latest.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ImmichWidgetsBinding();
|
ImmichWidgetsBinding();
|
||||||
final db = await loadDb();
|
final db = await Bootstrap.initIsar();
|
||||||
|
await Bootstrap.initDomain(db);
|
||||||
await initApp();
|
await initApp();
|
||||||
await migrateDatabaseIfNeeded(db);
|
await migrateDatabaseIfNeeded(db);
|
||||||
HttpOverrides.global = HttpSSLCertOverride();
|
HttpOverrides.global = HttpSSLCertOverride();
|
||||||
@@ -80,9 +66,6 @@ Future<void> initApp() async {
|
|||||||
|
|
||||||
await DynamicTheme.fetchSystemPalette();
|
await DynamicTheme.fetchSystemPalette();
|
||||||
|
|
||||||
// Initialize Immich Logger Service
|
|
||||||
ImmichLogger();
|
|
||||||
|
|
||||||
final log = Logger("ImmichErrorLogger");
|
final log = Logger("ImmichErrorLogger");
|
||||||
|
|
||||||
FlutterError.onError = (details) {
|
FlutterError.onError = (details) {
|
||||||
@@ -122,29 +105,6 @@ Future<void> initApp() async {
|
|||||||
await FileDownloader().trackTasks();
|
await FileDownloader().trackTasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Isar> loadDb() async {
|
|
||||||
final dir = await getApplicationDocumentsDirectory();
|
|
||||||
Isar db = await Isar.open(
|
|
||||||
[
|
|
||||||
StoreValueSchema,
|
|
||||||
ExifInfoSchema,
|
|
||||||
AssetSchema,
|
|
||||||
AlbumSchema,
|
|
||||||
UserSchema,
|
|
||||||
BackupAlbumSchema,
|
|
||||||
DuplicatedAssetSchema,
|
|
||||||
LoggerMessageSchema,
|
|
||||||
ETagSchema,
|
|
||||||
if (Platform.isAndroid) AndroidDeviceAssetSchema,
|
|
||||||
if (Platform.isIOS) IOSDeviceAssetSchema,
|
|
||||||
],
|
|
||||||
directory: dir.path,
|
|
||||||
maxSizeMiB: 1024,
|
|
||||||
);
|
|
||||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
|
||||||
return db;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImmichApp extends ConsumerStatefulWidget {
|
class ImmichApp extends ConsumerStatefulWidget {
|
||||||
const ImmichApp({super.key});
|
const ImmichApp({super.key});
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
|
||||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
import 'package:immich_mobile/services/immich_logger.service.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
@@ -17,8 +18,11 @@ class AppLogPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final immichLogger = ImmichLogger();
|
final immichLogger = LogService.I;
|
||||||
final logMessages = useState(immichLogger.messages);
|
final shouldReload = useState(false);
|
||||||
|
final logMessages = useFuture(
|
||||||
|
useMemoized(() => immichLogger.getMessages(), [shouldReload.value]),
|
||||||
|
);
|
||||||
|
|
||||||
Widget colorStatusIndicator(Color color) {
|
Widget colorStatusIndicator(Color color) {
|
||||||
return Column(
|
return Column(
|
||||||
@@ -71,7 +75,7 @@ class AppLogPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
immichLogger.clearLogs();
|
immichLogger.clearLogs();
|
||||||
logMessages.value = [];
|
shouldReload.value = !shouldReload.value;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Builder(
|
Builder(
|
||||||
@@ -84,7 +88,7 @@ class AppLogPage extends HookConsumerWidget {
|
|||||||
size: 20.0,
|
size: 20.0,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
immichLogger.shareLogs(iconContext);
|
ImmichLogger.shareLogs(iconContext);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -105,9 +109,9 @@ class AppLogPage extends HookConsumerWidget {
|
|||||||
separatorBuilder: (context, index) {
|
separatorBuilder: (context, index) {
|
||||||
return const Divider(height: 0);
|
return const Divider(height: 0);
|
||||||
},
|
},
|
||||||
itemCount: logMessages.value.length,
|
itemCount: logMessages.data?.length ?? 0,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
var logMessage = logMessages.value[index];
|
var logMessage = logMessages.data![index];
|
||||||
return ListTile(
|
return ListTile(
|
||||||
onTap: () => context.pushRoute(
|
onTap: () => context.pushRoute(
|
||||||
AppLogDetailRoute(
|
AppLogDetailRoute(
|
||||||
@@ -128,7 +132,7 @@ class AppLogPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}",
|
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12.0,
|
fontSize: 12.0,
|
||||||
color: context.colorScheme.onSurfaceSecondary,
|
color: context.colorScheme.onSurfaceSecondary,
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class AppLogDetailPage extends HookConsumerWidget {
|
class AppLogDetailPage extends HookConsumerWidget {
|
||||||
const AppLogDetailPage({super.key, required this.logMessage});
|
const AppLogDetailPage({super.key, required this.logMessage});
|
||||||
|
|
||||||
final LoggerMessage logMessage;
|
final LogMessage logMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -126,14 +126,14 @@ class AppLogDetailPage extends HookConsumerWidget {
|
|||||||
child: ListView(
|
child: ListView(
|
||||||
children: [
|
children: [
|
||||||
buildTextWithCopyButton("MESSAGE", logMessage.message),
|
buildTextWithCopyButton("MESSAGE", logMessage.message),
|
||||||
if (logMessage.details != null)
|
if (logMessage.error != null)
|
||||||
buildTextWithCopyButton("DETAILS", logMessage.details.toString()),
|
buildTextWithCopyButton("DETAILS", logMessage.error.toString()),
|
||||||
if (logMessage.context1 != null)
|
if (logMessage.logger != null)
|
||||||
buildLogContext1(logMessage.context1.toString()),
|
buildLogContext1(logMessage.logger.toString()),
|
||||||
if (logMessage.context2 != null)
|
if (logMessage.stack != null)
|
||||||
buildTextWithCopyButton(
|
buildTextWithCopyButton(
|
||||||
"STACK TRACE",
|
"STACK TRACE",
|
||||||
logMessage.context2.toString(),
|
logMessage.stack.toString(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class PhotosPage extends HookConsumerWidget {
|
|||||||
: const SizedBox(),
|
: const SizedBox(),
|
||||||
renderListProvider: timelineUsers.length > 1
|
renderListProvider: timelineUsers.length > 1
|
||||||
? multiUsersTimelineProvider(timelineUsers)
|
? multiUsersTimelineProvider(timelineUsers)
|
||||||
: singleUserTimelineProvider(currentUser!.isarId),
|
: singleUserTimelineProvider(currentUser?.isarId),
|
||||||
buildLoadingIndicator: buildLoadingIndicator,
|
buildLoadingIndicator: buildLoadingIndicator,
|
||||||
onRefresh: refreshAssets,
|
onRefresh: refreshAssets,
|
||||||
stackEnabled: true,
|
stackEnabled: true,
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||||
import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
import 'package:immich_mobile/providers/notification_permission.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
enum AppLifeCycleEnum {
|
enum AppLifeCycleEnum {
|
||||||
@@ -112,7 +114,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
_ref.read(websocketProvider.notifier).disconnect();
|
_ref.read(websocketProvider.notifier).disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImmichLogger().flush();
|
unawaited(LogService.I.flush());
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleAppDetached() {
|
void handleAppDetached() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'package:immich_mobile/entities/album.entity.dart';
|
|||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
import 'package:immich_mobile/models/auth/auth_state.model.dart';
|
import 'package:immich_mobile/models/auth/auth_state.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/available_album.model.dart';
|
import 'package:immich_mobile/models/backup/available_album.model.dart';
|
||||||
@@ -23,21 +23,34 @@ import 'package:immich_mobile/models/server_info/server_disk_info.model.dart';
|
|||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
import 'package:immich_mobile/repositories/album_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/services/backup.service.dart';
|
import 'package:immich_mobile/services/backup.service.dart';
|
||||||
|
import 'package:immich_mobile/services/backup_album.service.dart';
|
||||||
import 'package:immich_mobile/services/server_info.service.dart';
|
import 'package:immich_mobile/services/server_info.service.dart';
|
||||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
|
||||||
|
|
||||||
|
final backupProvider =
|
||||||
|
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
||||||
|
return BackupNotifier(
|
||||||
|
ref.watch(backupServiceProvider),
|
||||||
|
ref.watch(serverInfoServiceProvider),
|
||||||
|
ref.watch(authProvider),
|
||||||
|
ref.watch(backgroundServiceProvider),
|
||||||
|
ref.watch(galleryPermissionNotifier.notifier),
|
||||||
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
|
ref.watch(fileMediaRepositoryProvider),
|
||||||
|
ref.watch(backupAlbumServiceProvider),
|
||||||
|
ref,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
class BackupNotifier extends StateNotifier<BackUpState> {
|
class BackupNotifier extends StateNotifier<BackUpState> {
|
||||||
BackupNotifier(
|
BackupNotifier(
|
||||||
this._backupService,
|
this._backupService,
|
||||||
@@ -45,10 +58,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
this._authState,
|
this._authState,
|
||||||
this._backgroundService,
|
this._backgroundService,
|
||||||
this._galleryPermissionNotifier,
|
this._galleryPermissionNotifier,
|
||||||
this._db,
|
|
||||||
this._albumMediaRepository,
|
this._albumMediaRepository,
|
||||||
this._fileMediaRepository,
|
this._fileMediaRepository,
|
||||||
this._backupRepository,
|
this._backupAlbumService,
|
||||||
this.ref,
|
this.ref,
|
||||||
) : super(
|
) : super(
|
||||||
BackUpState(
|
BackUpState(
|
||||||
@@ -96,10 +108,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final AuthState _authState;
|
final AuthState _authState;
|
||||||
final BackgroundService _backgroundService;
|
final BackgroundService _backgroundService;
|
||||||
final GalleryPermissionNotifier _galleryPermissionNotifier;
|
final GalleryPermissionNotifier _galleryPermissionNotifier;
|
||||||
final Isar _db;
|
|
||||||
final IAlbumMediaRepository _albumMediaRepository;
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
final IFileMediaRepository _fileMediaRepository;
|
final IFileMediaRepository _fileMediaRepository;
|
||||||
final IBackupRepository _backupRepository;
|
final BackupAlbumService _backupAlbumService;
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -260,9 +271,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
state = state.copyWith(availableAlbums: availableAlbums);
|
state = state.copyWith(availableAlbums: availableAlbums);
|
||||||
|
|
||||||
final List<BackupAlbum> excludedBackupAlbums =
|
final List<BackupAlbum> excludedBackupAlbums =
|
||||||
await _backupRepository.getAllBySelection(BackupSelection.exclude);
|
await _backupAlbumService.getAllBySelection(BackupSelection.exclude);
|
||||||
final List<BackupAlbum> selectedBackupAlbums =
|
final List<BackupAlbum> selectedBackupAlbums =
|
||||||
await _backupRepository.getAllBySelection(BackupSelection.select);
|
await _backupAlbumService.getAllBySelection(BackupSelection.select);
|
||||||
|
|
||||||
final Set<AvailableAlbum> selectedAlbums = {};
|
final Set<AvailableAlbum> selectedAlbums = {};
|
||||||
for (final BackupAlbum ba in selectedBackupAlbums) {
|
for (final BackupAlbum ba in selectedBackupAlbums) {
|
||||||
@@ -439,7 +450,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Save user selection of selected albums and excluded albums to database
|
/// Save user selection of selected albums and excluded albums to database
|
||||||
Future<void> _updatePersistentAlbumsSelection() {
|
Future<void> _updatePersistentAlbumsSelection() async {
|
||||||
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||||
final selected = state.selectedBackupAlbums.map(
|
final selected = state.selectedBackupAlbums.map(
|
||||||
(e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select),
|
(e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select),
|
||||||
@@ -447,29 +458,30 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final excluded = state.excludedBackupAlbums.map(
|
final excluded = state.excludedBackupAlbums.map(
|
||||||
(e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude),
|
(e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude),
|
||||||
);
|
);
|
||||||
final backupAlbums = selected.followedBy(excluded).toList();
|
final candidates = selected.followedBy(excluded).toList();
|
||||||
backupAlbums.sortBy((e) => e.id);
|
candidates.sortBy((e) => e.id);
|
||||||
return _db.writeTxn(() async {
|
|
||||||
final dbAlbums = await _db.backupAlbums.where().sortById().findAll();
|
final savedBackupAlbums =
|
||||||
final List<int> toDelete = [];
|
await _backupAlbumService.getAll(sort: BackupAlbumSort.id);
|
||||||
final List<BackupAlbum> toUpsert = [];
|
final List<int> toDelete = [];
|
||||||
// stores the most recent `lastBackup` per album but always keeps the `selection` the user just made
|
final List<BackupAlbum> toUpsert = [];
|
||||||
diffSortedListsSync(
|
|
||||||
dbAlbums,
|
diffSortedListsSync(
|
||||||
backupAlbums,
|
savedBackupAlbums,
|
||||||
compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
|
candidates,
|
||||||
both: (BackupAlbum a, BackupAlbum b) {
|
compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id),
|
||||||
b.lastBackup =
|
both: (BackupAlbum a, BackupAlbum b) {
|
||||||
a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup;
|
b.lastBackup =
|
||||||
toUpsert.add(b);
|
a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup;
|
||||||
return true;
|
toUpsert.add(b);
|
||||||
},
|
return true;
|
||||||
onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId),
|
},
|
||||||
onlySecond: (BackupAlbum b) => toUpsert.add(b),
|
onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId),
|
||||||
);
|
onlySecond: (BackupAlbum b) => toUpsert.add(b),
|
||||||
await _db.backupAlbums.deleteAll(toDelete);
|
);
|
||||||
await _db.backupAlbums.putAll(toUpsert);
|
|
||||||
});
|
await _backupAlbumService.deleteAll(toDelete);
|
||||||
|
await _backupAlbumService.updateAll(toUpsert);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invoke backup process
|
/// Invoke backup process
|
||||||
@@ -686,14 +698,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resumeBackup() async {
|
Future<void> resumeBackup() async {
|
||||||
final List<BackupAlbum> selectedBackupAlbums = await _db.backupAlbums
|
final List<BackupAlbum> selectedBackupAlbums =
|
||||||
.filter()
|
await _backupAlbumService.getAllBySelection(BackupSelection.select);
|
||||||
.selectionEqualTo(BackupSelection.select)
|
final List<BackupAlbum> excludedBackupAlbums =
|
||||||
.findAll();
|
await _backupAlbumService.getAllBySelection(BackupSelection.exclude);
|
||||||
final List<BackupAlbum> excludedBackupAlbums = await _db.backupAlbums
|
|
||||||
.filter()
|
|
||||||
.selectionEqualTo(BackupSelection.exclude)
|
|
||||||
.findAll();
|
|
||||||
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
Set<AvailableAlbum> selectedAlbums = state.selectedBackupAlbums;
|
||||||
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
Set<AvailableAlbum> excludedAlbums = state.excludedBackupAlbums;
|
||||||
if (selectedAlbums.isNotEmpty) {
|
if (selectedAlbums.isNotEmpty) {
|
||||||
@@ -756,23 +764,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BackUpProgressEnum get backupProgress => state.backupProgress;
|
BackUpProgressEnum get backupProgress => state.backupProgress;
|
||||||
|
|
||||||
void updateBackupProgress(BackUpProgressEnum backupProgress) {
|
void updateBackupProgress(BackUpProgressEnum backupProgress) {
|
||||||
state = state.copyWith(backupProgress: backupProgress);
|
state = state.copyWith(backupProgress: backupProgress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final backupProvider =
|
|
||||||
StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
|
|
||||||
return BackupNotifier(
|
|
||||||
ref.watch(backupServiceProvider),
|
|
||||||
ref.watch(serverInfoServiceProvider),
|
|
||||||
ref.watch(authProvider),
|
|
||||||
ref.watch(backgroundServiceProvider),
|
|
||||||
ref.watch(galleryPermissionNotifier.notifier),
|
|
||||||
ref.watch(dbProvider),
|
|
||||||
ref.watch(albumMediaRepositoryProvider),
|
|
||||||
ref.watch(fileMediaRepositoryProvider),
|
|
||||||
ref.watch(backupRepositoryProvider),
|
|
||||||
ref,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/repositories/backup.repository.dart';
|
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||||
@@ -24,6 +23,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
|||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/backup_album.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||||
@@ -37,7 +37,7 @@ final manualUploadProvider =
|
|||||||
ref.watch(localNotificationService),
|
ref.watch(localNotificationService),
|
||||||
ref.watch(backupProvider.notifier),
|
ref.watch(backupProvider.notifier),
|
||||||
ref.watch(backupServiceProvider),
|
ref.watch(backupServiceProvider),
|
||||||
ref.watch(backupRepositoryProvider),
|
ref.watch(backupAlbumServiceProvider),
|
||||||
ref,
|
ref,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -47,14 +47,14 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||||||
final LocalNotificationService _localNotificationService;
|
final LocalNotificationService _localNotificationService;
|
||||||
final BackupNotifier _backupProvider;
|
final BackupNotifier _backupProvider;
|
||||||
final BackupService _backupService;
|
final BackupService _backupService;
|
||||||
final BackupRepository _backupRepository;
|
final BackupAlbumService _backupAlbumService;
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
ManualUploadNotifier(
|
ManualUploadNotifier(
|
||||||
this._localNotificationService,
|
this._localNotificationService,
|
||||||
this._backupProvider,
|
this._backupProvider,
|
||||||
this._backupService,
|
this._backupService,
|
||||||
this._backupRepository,
|
this._backupAlbumService,
|
||||||
this.ref,
|
this.ref,
|
||||||
) : super(
|
) : super(
|
||||||
ManualUploadState(
|
ManualUploadState(
|
||||||
@@ -210,9 +210,9 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final selectedBackupAlbums =
|
final selectedBackupAlbums =
|
||||||
await _backupRepository.getAllBySelection(BackupSelection.select);
|
await _backupAlbumService.getAllBySelection(BackupSelection.select);
|
||||||
final excludedBackupAlbums =
|
final excludedBackupAlbums = await _backupAlbumService
|
||||||
await _backupRepository.getAllBySelection(BackupSelection.exclude);
|
.getAllBySelection(BackupSelection.exclude);
|
||||||
|
|
||||||
// Get candidates from selected albums and excluded albums
|
// Get candidates from selected albums and excluded albums
|
||||||
Set<BackupCandidate> candidates =
|
Set<BackupCandidate> candidates =
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:immich_mobile/entities/user.entity.dart';
|
|||||||
|
|
||||||
class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
||||||
final PartnerService _partnerService;
|
final PartnerService _partnerService;
|
||||||
|
late final StreamSubscription<List<User>> streamSub;
|
||||||
|
|
||||||
PartnerSharedWithNotifier(this._partnerService) : super([]) {
|
PartnerSharedWithNotifier(this._partnerService) : super([]) {
|
||||||
Function eq = const ListEquality<User>().equals;
|
Function eq = const ListEquality<User>().equals;
|
||||||
@@ -16,7 +17,7 @@ class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
|||||||
state = partners;
|
state = partners;
|
||||||
}
|
}
|
||||||
}).then((_) {
|
}).then((_) {
|
||||||
_partnerService.watchSharedWith().listen((partners) {
|
streamSub = _partnerService.watchSharedWith().listen((partners) {
|
||||||
if (!eq(state, partners)) {
|
if (!eq(state, partners)) {
|
||||||
state = partners;
|
state = partners;
|
||||||
}
|
}
|
||||||
@@ -27,6 +28,14 @@ class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
|
|||||||
Future<bool> updatePartner(User partner, {required bool inTimeline}) {
|
Future<bool> updatePartner(User partner, {required bool inTimeline}) {
|
||||||
return _partnerService.updatePartner(partner, inTimeline: inTimeline);
|
return _partnerService.updatePartner(partner, inTimeline: inTimeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (mounted) {
|
||||||
|
streamSub.cancel();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final partnerSharedWithProvider =
|
final partnerSharedWithProvider =
|
||||||
@@ -38,6 +47,7 @@ final partnerSharedWithProvider =
|
|||||||
|
|
||||||
class PartnerSharedByNotifier extends StateNotifier<List<User>> {
|
class PartnerSharedByNotifier extends StateNotifier<List<User>> {
|
||||||
final PartnerService _partnerService;
|
final PartnerService _partnerService;
|
||||||
|
late final StreamSubscription<List<User>> streamSub;
|
||||||
|
|
||||||
PartnerSharedByNotifier(this._partnerService) : super([]) {
|
PartnerSharedByNotifier(this._partnerService) : super([]) {
|
||||||
Function eq = const ListEquality<User>().equals;
|
Function eq = const ListEquality<User>().equals;
|
||||||
@@ -54,11 +64,11 @@ class PartnerSharedByNotifier extends StateNotifier<List<User>> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
late final StreamSubscription<List<User>> streamSub;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
streamSub.cancel();
|
if (mounted) {
|
||||||
|
streamSub.cancel();
|
||||||
|
}
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import 'package:immich_mobile/providers/locale_provider.dart';
|
|||||||
import 'package:immich_mobile/services/timeline.service.dart';
|
import 'package:immich_mobile/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
|
||||||
final singleUserTimelineProvider = StreamProvider.family<RenderList, int>(
|
final singleUserTimelineProvider = StreamProvider.family<RenderList, int?>(
|
||||||
(ref, userId) {
|
(ref, userId) {
|
||||||
|
if (userId == null) {
|
||||||
|
return const Stream.empty();
|
||||||
|
}
|
||||||
|
|
||||||
ref.watch(localeProvider);
|
ref.watch(localeProvider);
|
||||||
final timelineService = ref.watch(timelineServiceProvider);
|
final timelineService = ref.watch(timelineServiceProvider);
|
||||||
return timelineService.watchHomeTimeline(userId);
|
return timelineService.watchHomeTimeline(userId);
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
|||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:immich_mobile/services/user.service.dart';
|
||||||
|
|
||||||
class CurrentUserProvider extends StateNotifier<User?> {
|
class CurrentUserProvider extends StateNotifier<User?> {
|
||||||
CurrentUserProvider(this._apiService) : super(null) {
|
CurrentUserProvider(this._apiService) : super(null) {
|
||||||
@@ -47,18 +46,14 @@ final currentUserProvider =
|
|||||||
});
|
});
|
||||||
|
|
||||||
class TimelineUserIdsProvider extends StateNotifier<List<int>> {
|
class TimelineUserIdsProvider extends StateNotifier<List<int>> {
|
||||||
TimelineUserIdsProvider(Isar db, User? currentUser) : super([]) {
|
TimelineUserIdsProvider(this._userService) : super([]) {
|
||||||
final query = db.users
|
_userService.getTimelineUserIds().then((users) => state = users);
|
||||||
.filter()
|
streamSub =
|
||||||
.inTimelineEqualTo(true)
|
_userService.watchTimelineUserIds().listen((users) => state = users);
|
||||||
.or()
|
|
||||||
.isarIdEqualTo(currentUser?.isarId ?? Isar.autoIncrement)
|
|
||||||
.isarIdProperty();
|
|
||||||
query.findAll().then((users) => state = users);
|
|
||||||
streamSub = query.watch().listen((users) => state = users);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
late final StreamSubscription<List<int>> streamSub;
|
late final StreamSubscription<List<int>> streamSub;
|
||||||
|
final UserService _userService;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -69,8 +64,5 @@ class TimelineUserIdsProvider extends StateNotifier<List<int>> {
|
|||||||
|
|
||||||
final timelineUsersIdsProvider =
|
final timelineUsersIdsProvider =
|
||||||
StateNotifierProvider<TimelineUserIdsProvider, List<int>>((ref) {
|
StateNotifierProvider<TimelineUserIdsProvider, List<int>>((ref) {
|
||||||
return TimelineUserIdsProvider(
|
return TimelineUserIdsProvider(ref.watch(userServiceProvider));
|
||||||
ref.watch(dbProvider),
|
|
||||||
ref.watch(currentUserProvider),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/database.repository.dart';
|
import 'package:immich_mobile/repositories/database.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
final backupRepositoryProvider =
|
final backupAlbumRepositoryProvider =
|
||||||
Provider((ref) => BackupRepository(ref.watch(dbProvider)));
|
Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider)));
|
||||||
|
|
||||||
class BackupRepository extends DatabaseRepository implements IBackupRepository {
|
class BackupAlbumRepository extends DatabaseRepository
|
||||||
BackupRepository(super.db);
|
implements IBackupAlbumRepository {
|
||||||
|
BackupAlbumRepository(super.db);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
|
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
|
||||||
|
|||||||
@@ -70,4 +70,26 @@ class UserRepository extends DatabaseRepository implements IUserRepository {
|
|||||||
await db.users.clear();
|
await db.users.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<int>> getTimelineUserIds(int id) {
|
||||||
|
return db.users
|
||||||
|
.filter()
|
||||||
|
.inTimelineEqualTo(true)
|
||||||
|
.or()
|
||||||
|
.isarIdEqualTo(id)
|
||||||
|
.isarIdProperty()
|
||||||
|
.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<int>> watchTimelineUsers(int id) {
|
||||||
|
return db.users
|
||||||
|
.filter()
|
||||||
|
.inTimelineEqualTo(true)
|
||||||
|
.or()
|
||||||
|
.isarIdEqualTo(id)
|
||||||
|
.isarIdProperty()
|
||||||
|
.watch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,48 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||||
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
||||||
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/library/places/places_collection.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/library/library.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart';
|
import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/album/album_asset_selection.page.dart';
|
import 'package:immich_mobile/pages/album/album_asset_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/album/album_options.page.dart';
|
import 'package:immich_mobile/pages/album/album_options.page.dart';
|
||||||
import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart';
|
import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart';
|
||||||
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
|
import 'package:immich_mobile/pages/album/album_viewer.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/albums/albums.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/gallery_viewer.page.dart';
|
import 'package:immich_mobile/pages/common/gallery_viewer.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/headers_settings.page.dart';
|
import 'package:immich_mobile/pages/common/headers_settings.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/common/native_video_viewer.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/settings.page.dart';
|
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||||
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
||||||
import 'package:immich_mobile/pages/editing/edit.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/editing/crop.page.dart';
|
import 'package:immich_mobile/pages/editing/crop.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/editing/edit.page.dart';
|
||||||
import 'package:immich_mobile/pages/editing/filter.page.dart';
|
import 'package:immich_mobile/pages/editing/filter.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/archive.page.dart';
|
import 'package:immich_mobile/pages/library/archive.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/favorite.page.dart';
|
import 'package:immich_mobile/pages/library/favorite.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/library.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/places/places_collection.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/trash.page.dart';
|
import 'package:immich_mobile/pages/library/trash.page.dart';
|
||||||
import 'package:immich_mobile/pages/login/change_password.page.dart';
|
import 'package:immich_mobile/pages/login/change_password.page.dart';
|
||||||
import 'package:immich_mobile/pages/login/login.page.dart';
|
import 'package:immich_mobile/pages/login/login.page.dart';
|
||||||
@@ -54,10 +58,6 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
|
|||||||
import 'package:immich_mobile/pages/search/person_result.page.dart';
|
import 'package:immich_mobile/pages/search/person_result.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/recently_added.page.dart';
|
import 'package:immich_mobile/pages/search/recently_added.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart';
|
|
||||||
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ class AllVideosRoute extends PageRouteInfo<void> {
|
|||||||
class AppLogDetailRoute extends PageRouteInfo<AppLogDetailRouteArgs> {
|
class AppLogDetailRoute extends PageRouteInfo<AppLogDetailRouteArgs> {
|
||||||
AppLogDetailRoute({
|
AppLogDetailRoute({
|
||||||
Key? key,
|
Key? key,
|
||||||
required LoggerMessage logMessage,
|
required LogMessage logMessage,
|
||||||
List<PageRouteInfo>? children,
|
List<PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
AppLogDetailRoute.name,
|
AppLogDetailRoute.name,
|
||||||
@@ -419,7 +419,7 @@ class AppLogDetailRouteArgs {
|
|||||||
|
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
|
||||||
final LoggerMessage logMessage;
|
final LogMessage logMessage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import 'package:immich_mobile/interfaces/album.interface.dart';
|
|||||||
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
import 'package:immich_mobile/interfaces/album_api.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
import 'package:immich_mobile/interfaces/album_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
|
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
|
||||||
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
import 'package:immich_mobile/models/albums/album_search.model.dart';
|
||||||
import 'package:immich_mobile/repositories/album.repository.dart';
|
import 'package:immich_mobile/repositories/album.repository.dart';
|
||||||
@@ -36,7 +36,7 @@ final albumServiceProvider = Provider(
|
|||||||
ref.watch(entityServiceProvider),
|
ref.watch(entityServiceProvider),
|
||||||
ref.watch(albumRepositoryProvider),
|
ref.watch(albumRepositoryProvider),
|
||||||
ref.watch(assetRepositoryProvider),
|
ref.watch(assetRepositoryProvider),
|
||||||
ref.watch(backupRepositoryProvider),
|
ref.watch(backupAlbumRepositoryProvider),
|
||||||
ref.watch(albumMediaRepositoryProvider),
|
ref.watch(albumMediaRepositoryProvider),
|
||||||
ref.watch(albumApiRepositoryProvider),
|
ref.watch(albumApiRepositoryProvider),
|
||||||
),
|
),
|
||||||
@@ -48,7 +48,7 @@ class AlbumService {
|
|||||||
final EntityService _entityService;
|
final EntityService _entityService;
|
||||||
final IAlbumRepository _albumRepository;
|
final IAlbumRepository _albumRepository;
|
||||||
final IAssetRepository _assetRepository;
|
final IAssetRepository _assetRepository;
|
||||||
final IBackupRepository _backupAlbumRepository;
|
final IBackupAlbumRepository _backupAlbumRepository;
|
||||||
final IAlbumMediaRepository _albumMediaRepository;
|
final IAlbumMediaRepository _albumMediaRepository;
|
||||||
final IAlbumApiRepository _albumApiRepository;
|
final IAlbumApiRepository _albumApiRepository;
|
||||||
final Logger _log = Logger('AlbumService');
|
final Logger _log = Logger('AlbumService');
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import 'package:immich_mobile/entities/user.entity.dart';
|
|||||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||||
@@ -39,7 +39,7 @@ final assetServiceProvider = Provider(
|
|||||||
ref.watch(exifInfoRepositoryProvider),
|
ref.watch(exifInfoRepositoryProvider),
|
||||||
ref.watch(userRepositoryProvider),
|
ref.watch(userRepositoryProvider),
|
||||||
ref.watch(etagRepositoryProvider),
|
ref.watch(etagRepositoryProvider),
|
||||||
ref.watch(backupRepositoryProvider),
|
ref.watch(backupAlbumRepositoryProvider),
|
||||||
ref.watch(apiServiceProvider),
|
ref.watch(apiServiceProvider),
|
||||||
ref.watch(syncServiceProvider),
|
ref.watch(syncServiceProvider),
|
||||||
ref.watch(userServiceProvider),
|
ref.watch(userServiceProvider),
|
||||||
@@ -55,7 +55,7 @@ class AssetService {
|
|||||||
final IExifInfoRepository _exifInfoRepository;
|
final IExifInfoRepository _exifInfoRepository;
|
||||||
final IUserRepository _userRepository;
|
final IUserRepository _userRepository;
|
||||||
final IETagRepository _etagRepository;
|
final IETagRepository _etagRepository;
|
||||||
final IBackupRepository _backupRepository;
|
final IBackupAlbumRepository _backupRepository;
|
||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
final SyncService _syncService;
|
final SyncService _syncService;
|
||||||
final UserService _userService;
|
final UserService _userService;
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||||
import 'package:immich_mobile/main.dart';
|
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||||
@@ -48,6 +47,7 @@ import 'package:immich_mobile/services/network.service.dart';
|
|||||||
import 'package:immich_mobile/services/sync.service.dart';
|
import 'package:immich_mobile/services/sync.service.dart';
|
||||||
import 'package:immich_mobile/services/user.service.dart';
|
import 'package:immich_mobile/services/user.service.dart';
|
||||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||||
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||||
import 'package:network_info_plus/network_info_plus.dart';
|
import 'package:network_info_plus/network_info_plus.dart';
|
||||||
@@ -369,7 +369,8 @@ class BackgroundService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _onAssetsChanged() async {
|
Future<bool> _onAssetsChanged() async {
|
||||||
final db = await loadDb();
|
final db = await Bootstrap.initIsar();
|
||||||
|
await Bootstrap.initDomain(db);
|
||||||
|
|
||||||
HttpOverrides.global = HttpSSLCertOverride();
|
HttpOverrides.global = HttpSSLCertOverride();
|
||||||
ApiService apiService = ApiService();
|
ApiService apiService = ApiService();
|
||||||
@@ -377,7 +378,7 @@ class BackgroundService {
|
|||||||
AppSettingsService settingsService = AppSettingsService();
|
AppSettingsService settingsService = AppSettingsService();
|
||||||
AlbumRepository albumRepository = AlbumRepository(db);
|
AlbumRepository albumRepository = AlbumRepository(db);
|
||||||
AssetRepository assetRepository = AssetRepository(db);
|
AssetRepository assetRepository = AssetRepository(db);
|
||||||
BackupRepository backupRepository = BackupRepository(db);
|
BackupAlbumRepository backupRepository = BackupAlbumRepository(db);
|
||||||
ExifInfoRepository exifInfoRepository = ExifInfoRepository(db);
|
ExifInfoRepository exifInfoRepository = ExifInfoRepository(db);
|
||||||
ETagRepository eTagRepository = ETagRepository(db);
|
ETagRepository eTagRepository = ETagRepository(db);
|
||||||
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
|
||||||
@@ -719,7 +720,6 @@ enum IosBackgroundTask { fetch, processing }
|
|||||||
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
void _nativeEntry() {
|
void _nativeEntry() {
|
||||||
HttpOverrides.global = HttpSSLCertOverride();
|
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
DartPluginRegistrant.ensureInitialized();
|
DartPluginRegistrant.ensureInitialized();
|
||||||
BackgroundService backgroundService = BackgroundService();
|
BackgroundService backgroundService = BackgroundService();
|
||||||
|
|||||||
34
mobile/lib/services/backup_album.service.dart
Normal file
34
mobile/lib/services/backup_album.service.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||||
|
import 'package:immich_mobile/repositories/backup.repository.dart';
|
||||||
|
|
||||||
|
final backupAlbumServiceProvider = Provider<BackupAlbumService>((ref) {
|
||||||
|
return BackupAlbumService(ref.watch(backupAlbumRepositoryProvider));
|
||||||
|
});
|
||||||
|
|
||||||
|
class BackupAlbumService {
|
||||||
|
final IBackupAlbumRepository _backupAlbumRepository;
|
||||||
|
|
||||||
|
BackupAlbumService(this._backupAlbumRepository);
|
||||||
|
|
||||||
|
Future<List<BackupAlbum>> getAll({BackupAlbumSort? sort}) {
|
||||||
|
return _backupAlbumRepository.getAll(sort: sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getIdsBySelection(BackupSelection backup) {
|
||||||
|
return _backupAlbumRepository.getIdsBySelection(backup);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BackupAlbum>> getAllBySelection(BackupSelection backup) {
|
||||||
|
return _backupAlbumRepository.getAllBySelection(backup);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteAll(List<int> ids) {
|
||||||
|
return _backupAlbumRepository.deleteAll(ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateAll(List<BackupAlbum> backupAlbums) {
|
||||||
|
return _backupAlbumRepository.updateAll(backupAlbums);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
|
|||||||
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
import 'package:immich_mobile/repositories/exif_info.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||||
import 'package:immich_mobile/utils/diff.dart';
|
import 'package:immich_mobile/utils/diff.dart';
|
||||||
|
|
||||||
/// Finds duplicates originating from missing EXIF information
|
/// Finds duplicates originating from missing EXIF information
|
||||||
@@ -123,6 +124,8 @@ class BackupVerificationService {
|
|||||||
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
||||||
final List<Asset> result = [];
|
final List<Asset> result = [];
|
||||||
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
|
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
|
||||||
|
final db = await Bootstrap.initIsar();
|
||||||
|
await Bootstrap.initDomain(db);
|
||||||
await tuple.fileMediaRepository.enableBackgroundAccess();
|
await tuple.fileMediaRepository.enableBackgroundAccess();
|
||||||
final ApiService apiService = ApiService();
|
final ApiService apiService = ApiService();
|
||||||
apiService.setEndpoint(tuple.endpoint);
|
apiService.setEndpoint(tuple.endpoint);
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:isar/isar.dart';
|
|
||||||
import 'package:logging/logging.dart';
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
@@ -18,75 +14,10 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
///
|
///
|
||||||
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
|
||||||
/// and generate a csv file.
|
/// and generate a csv file.
|
||||||
class ImmichLogger {
|
abstract final class ImmichLogger {
|
||||||
static final ImmichLogger _instance = ImmichLogger._internal();
|
const ImmichLogger();
|
||||||
final maxLogEntries = 500;
|
|
||||||
final Isar _db = Isar.getInstance()!;
|
|
||||||
List<LoggerMessage> _msgBuffer = [];
|
|
||||||
Timer? _timer;
|
|
||||||
|
|
||||||
factory ImmichLogger() => _instance;
|
static Future<void> shareLogs(BuildContext context) async {
|
||||||
|
|
||||||
ImmichLogger._internal() {
|
|
||||||
_removeOverflowMessages();
|
|
||||||
final int levelId = Store.get(StoreKey.logLevel, 5); // 5 is INFO
|
|
||||||
Logger.root.level = Level.LEVELS[levelId];
|
|
||||||
Logger.root.onRecord.listen(_writeLogToDatabase);
|
|
||||||
}
|
|
||||||
|
|
||||||
set level(Level level) => Logger.root.level = level;
|
|
||||||
|
|
||||||
List<LoggerMessage> get messages {
|
|
||||||
final inDb =
|
|
||||||
_db.loggerMessages.where(sort: Sort.desc).anyId().findAllSync();
|
|
||||||
return _msgBuffer.isEmpty ? inDb : _msgBuffer.reversed.toList() + inDb;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _removeOverflowMessages() {
|
|
||||||
final msgCount = _db.loggerMessages.countSync();
|
|
||||||
if (msgCount > maxLogEntries) {
|
|
||||||
final numberOfEntryToBeDeleted = msgCount - maxLogEntries;
|
|
||||||
_db.writeTxn(
|
|
||||||
() => _db.loggerMessages
|
|
||||||
.where()
|
|
||||||
.limit(numberOfEntryToBeDeleted)
|
|
||||||
.deleteAll(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _writeLogToDatabase(LogRecord record) {
|
|
||||||
debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
|
|
||||||
final lm = LoggerMessage(
|
|
||||||
message: record.message,
|
|
||||||
details: record.error?.toString(),
|
|
||||||
level: record.level.toLogLevel(),
|
|
||||||
createdAt: record.time,
|
|
||||||
context1: record.loggerName,
|
|
||||||
context2: record.stackTrace?.toString(),
|
|
||||||
);
|
|
||||||
_msgBuffer.add(lm);
|
|
||||||
|
|
||||||
// delayed batch writing to database: increases performance when logging
|
|
||||||
// messages in quick succession and reduces NAND wear
|
|
||||||
_timer ??= Timer(const Duration(seconds: 5), _flushBufferToDatabase);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _flushBufferToDatabase() {
|
|
||||||
_timer = null;
|
|
||||||
final buffer = _msgBuffer;
|
|
||||||
_msgBuffer = [];
|
|
||||||
_db.writeTxn(() => _db.loggerMessages.putAll(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearLogs() {
|
|
||||||
_timer?.cancel();
|
|
||||||
_timer = null;
|
|
||||||
_msgBuffer.clear();
|
|
||||||
_db.writeTxn(() => _db.loggerMessages.clear());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> shareLogs(BuildContext context) async {
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
final dateTime = DateTime.now().toIso8601String();
|
final dateTime = DateTime.now().toIso8601String();
|
||||||
final filePath = '${tempDir.path}/Immich_log_$dateTime.log';
|
final filePath = '${tempDir.path}/Immich_log_$dateTime.log';
|
||||||
@@ -94,13 +25,13 @@ class ImmichLogger {
|
|||||||
final io = logFile.openWrite();
|
final io = logFile.openWrite();
|
||||||
try {
|
try {
|
||||||
// Write messages
|
// Write messages
|
||||||
for (final m in messages) {
|
for (final m in await LogService.I.getMessages()) {
|
||||||
final created = m.createdAt;
|
final created = m.createdAt;
|
||||||
final level = m.level.name.padRight(8);
|
final level = m.level.name.padRight(8);
|
||||||
final logger = (m.context1 ?? "<UNKNOWN_LOGGER>").padRight(20);
|
final logger = (m.logger ?? "<UNKNOWN_LOGGER>").padRight(20);
|
||||||
final message = m.message;
|
final message = m.message;
|
||||||
final error = m.details != null ? " ${m.details} |" : "";
|
final error = m.error == null ? "" : " ${m.error} |";
|
||||||
final stack = m.context2 != null ? "\n${m.context2!}" : "";
|
final stack = m.stack == null ? "" : "\n${m.stack!}";
|
||||||
io.write('$created | $level | $logger | $message |$error$stack\n');
|
io.write('$created | $level | $logger | $message |$error$stack\n');
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -115,16 +46,6 @@ class ImmichLogger {
|
|||||||
[XFile(filePath)],
|
[XFile(filePath)],
|
||||||
subject: "Immich logs $dateTime",
|
subject: "Immich logs $dateTime",
|
||||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||||
).then(
|
).then((value) => logFile.delete());
|
||||||
(value) => logFile.delete(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Flush pending log messages to persistent storage
|
|
||||||
void flush() {
|
|
||||||
if (_timer != null) {
|
|
||||||
_timer!.cancel();
|
|
||||||
_db.writeTxnSync(() => _db.loggerMessages.putAllSync(_msgBuffer));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,4 +107,14 @@ class UserService {
|
|||||||
Future<void> clearTable() {
|
Future<void> clearTable() {
|
||||||
return _userRepository.clearTable();
|
return _userRepository.clearTable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<int>> getTimelineUserIds() async {
|
||||||
|
final me = await _userRepository.me();
|
||||||
|
return _userRepository.getTimelineUserIds(me.isarId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<int>> watchTimelineUserIds() async* {
|
||||||
|
final me = await _userRepository.me();
|
||||||
|
yield* _userRepository.watchTimelineUsers(me.isarId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
56
mobile/lib/utils/bootstrap.dart
Normal file
56
mobile/lib/utils/bootstrap.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
|
import 'package:isar/isar.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
abstract final class Bootstrap {
|
||||||
|
static Future<Isar> initIsar() async {
|
||||||
|
if (Isar.getInstance() != null) {
|
||||||
|
return Isar.getInstance()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
|
return await Isar.open(
|
||||||
|
[
|
||||||
|
StoreValueSchema,
|
||||||
|
ExifInfoSchema,
|
||||||
|
AssetSchema,
|
||||||
|
AlbumSchema,
|
||||||
|
UserSchema,
|
||||||
|
BackupAlbumSchema,
|
||||||
|
DuplicatedAssetSchema,
|
||||||
|
LoggerMessageSchema,
|
||||||
|
ETagSchema,
|
||||||
|
if (Platform.isAndroid) AndroidDeviceAssetSchema,
|
||||||
|
if (Platform.isIOS) IOSDeviceAssetSchema,
|
||||||
|
],
|
||||||
|
directory: dir.path,
|
||||||
|
maxSizeMiB: 1024,
|
||||||
|
inspector: kDebugMode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> initDomain(Isar db) async {
|
||||||
|
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||||
|
await LogService.init(
|
||||||
|
logRepo: IsarLogRepository(db),
|
||||||
|
storeRepo: IsarStoreRepository(db),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
|
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart';
|
import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/local_storage_settings.dart';
|
import 'package:immich_mobile/widgets/settings/local_storage_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
|
||||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
|
||||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
|
||||||
import 'package:immich_mobile/widgets/settings/ssl_client_cert_settings.dart';
|
import 'package:immich_mobile/widgets/settings/ssl_client_cert_settings.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
@@ -33,7 +34,8 @@ class AdvancedSettings extends HookConsumerWidget {
|
|||||||
|
|
||||||
useValueChanged(
|
useValueChanged(
|
||||||
levelId.value,
|
levelId.value,
|
||||||
(_, __) => ImmichLogger().level = Level.LEVELS[levelId.value],
|
(_, __) =>
|
||||||
|
LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()),
|
||||||
);
|
);
|
||||||
|
|
||||||
final advancedSettings = [
|
final advancedSettings = [
|
||||||
|
|||||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.127.0
|
- API version: 1.128.0
|
||||||
- Generator version: 7.8.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
|
|||||||
3
mobile/openapi/lib/model/source_type.dart
generated
3
mobile/openapi/lib/model/source_type.dart
generated
@@ -25,11 +25,13 @@ class SourceType {
|
|||||||
|
|
||||||
static const machineLearning = SourceType._(r'machine-learning');
|
static const machineLearning = SourceType._(r'machine-learning');
|
||||||
static const exif = SourceType._(r'exif');
|
static const exif = SourceType._(r'exif');
|
||||||
|
static const manual = SourceType._(r'manual');
|
||||||
|
|
||||||
/// List of all possible values in this [enum][SourceType].
|
/// List of all possible values in this [enum][SourceType].
|
||||||
static const values = <SourceType>[
|
static const values = <SourceType>[
|
||||||
machineLearning,
|
machineLearning,
|
||||||
exif,
|
exif,
|
||||||
|
manual,
|
||||||
];
|
];
|
||||||
|
|
||||||
static SourceType? fromJson(dynamic value) => SourceTypeTypeTransformer().decode(value);
|
static SourceType? fromJson(dynamic value) => SourceTypeTypeTransformer().decode(value);
|
||||||
@@ -70,6 +72,7 @@ class SourceTypeTypeTransformer {
|
|||||||
switch (data) {
|
switch (data) {
|
||||||
case r'machine-learning': return SourceType.machineLearning;
|
case r'machine-learning': return SourceType.machineLearning;
|
||||||
case r'exif': return SourceType.exif;
|
case r'exif': return SourceType.exif;
|
||||||
|
case r'manual': return SourceType.manual;
|
||||||
default:
|
default:
|
||||||
if (!allowNull) {
|
if (!allowNull) {
|
||||||
throw ArgumentError('Unknown enum value to decode: $data');
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
|||||||
@@ -407,7 +407,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.2"
|
version: "0.0.2"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: fake_async
|
name: fake_async
|
||||||
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.127.0+185
|
version: 1.128.0+186
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
@@ -113,6 +113,7 @@ dev_dependencies:
|
|||||||
mocktail: ^1.0.3
|
mocktail: ^1.0.3
|
||||||
immich_mobile_immich_lint:
|
immich_mobile_immich_lint:
|
||||||
path: './immich_lint'
|
path: './immich_lint'
|
||||||
|
fake_async: ^1.3.1
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
186
mobile/test/domain/services/log_service_test.dart
Normal file
186
mobile/test/domain/services/log_service_test.dart
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
import '../../infrastructure/repository.mock.dart';
|
||||||
|
import '../../test_utils.dart';
|
||||||
|
|
||||||
|
final _kInfoLog = LogMessage(
|
||||||
|
message: '#Info Message',
|
||||||
|
level: LogLevel.INFO,
|
||||||
|
createdAt: DateTime(2025, 2, 26),
|
||||||
|
logger: 'Info Logger',
|
||||||
|
);
|
||||||
|
|
||||||
|
final _kWarnLog = LogMessage(
|
||||||
|
message: '#Warn Message',
|
||||||
|
level: LogLevel.WARNING,
|
||||||
|
createdAt: DateTime(2025, 2, 27),
|
||||||
|
logger: 'Warn Logger',
|
||||||
|
);
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late LogService sut;
|
||||||
|
late ILogRepository mockLogRepo;
|
||||||
|
late IStoreRepository mockStoreRepo;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
mockLogRepo = MockLogRepository();
|
||||||
|
mockStoreRepo = MockStoreRepository();
|
||||||
|
|
||||||
|
registerFallbackValue(_kInfoLog);
|
||||||
|
|
||||||
|
when(() => mockLogRepo.truncate(limit: any(named: 'limit')))
|
||||||
|
.thenAnswer((_) async => {});
|
||||||
|
when(() => mockStoreRepo.tryGet<int>(StoreKey.logLevel))
|
||||||
|
.thenAnswer((_) async => LogLevel.FINE.index);
|
||||||
|
when(() => mockLogRepo.getAll()).thenAnswer((_) async => []);
|
||||||
|
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
|
||||||
|
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
|
||||||
|
|
||||||
|
sut =
|
||||||
|
await LogService.create(logRepo: mockLogRepo, storeRepo: mockStoreRepo);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await sut.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Log Service Init:", () {
|
||||||
|
test('Truncates the existing logs on init', () {
|
||||||
|
final limit =
|
||||||
|
verify(() => mockLogRepo.truncate(limit: captureAny(named: 'limit')))
|
||||||
|
.captured
|
||||||
|
.firstOrNull as int?;
|
||||||
|
expect(limit, kLogTruncateLimit);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Sets log level based on the store setting', () {
|
||||||
|
verify(() => mockStoreRepo.tryGet<int>(StoreKey.logLevel)).called(1);
|
||||||
|
expect(Logger.root.level, Level.FINE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Log Service Set Level:", () {
|
||||||
|
setUp(() async {
|
||||||
|
when(() => mockStoreRepo.insert<int>(StoreKey.logLevel, any()))
|
||||||
|
.thenAnswer((_) async => true);
|
||||||
|
await sut.setlogLevel(LogLevel.SHOUT);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Updates the log level in store', () {
|
||||||
|
final index = verify(
|
||||||
|
() => mockStoreRepo.insert<int>(StoreKey.logLevel, captureAny()),
|
||||||
|
).captured.firstOrNull;
|
||||||
|
expect(index, LogLevel.SHOUT.index);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Sets log level on logger', () {
|
||||||
|
expect(Logger.root.level, Level.SHOUT);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Log Service Buffer:", () {
|
||||||
|
test('Buffers logs until timer elapses', () {
|
||||||
|
TestUtils.fakeAsync((time) async {
|
||||||
|
sut = await LogService.create(
|
||||||
|
logRepo: mockLogRepo,
|
||||||
|
storeRepo: mockStoreRepo,
|
||||||
|
shouldBuffer: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final logger = Logger(_kInfoLog.logger!);
|
||||||
|
logger.info(_kInfoLog.message);
|
||||||
|
expect(await sut.getMessages(), hasLength(1));
|
||||||
|
logger.warning(_kWarnLog.message);
|
||||||
|
expect(await sut.getMessages(), hasLength(2));
|
||||||
|
time.elapse(const Duration(seconds: 6));
|
||||||
|
expect(await sut.getMessages(), isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Batch inserts all logs on timer', () {
|
||||||
|
TestUtils.fakeAsync((time) async {
|
||||||
|
sut = await LogService.create(
|
||||||
|
logRepo: mockLogRepo,
|
||||||
|
storeRepo: mockStoreRepo,
|
||||||
|
shouldBuffer: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final logger = Logger(_kInfoLog.logger!);
|
||||||
|
logger.info(_kInfoLog.message);
|
||||||
|
time.elapse(const Duration(seconds: 6));
|
||||||
|
final insert = verify(() => mockLogRepo.insertAll(captureAny()));
|
||||||
|
insert.called(1);
|
||||||
|
// ignore: prefer-correct-json-casts
|
||||||
|
final captured = insert.captured.firstOrNull as List<LogMessage>;
|
||||||
|
expect(captured.firstOrNull?.message, _kInfoLog.message);
|
||||||
|
expect(captured.firstOrNull?.logger, _kInfoLog.logger);
|
||||||
|
|
||||||
|
verifyNever(() => mockLogRepo.insert(captureAny()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Does not buffer when off', () {
|
||||||
|
TestUtils.fakeAsync((time) async {
|
||||||
|
sut = await LogService.create(
|
||||||
|
logRepo: mockLogRepo,
|
||||||
|
storeRepo: mockStoreRepo,
|
||||||
|
shouldBuffer: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
final logger = Logger(_kInfoLog.logger!);
|
||||||
|
logger.info(_kInfoLog.message);
|
||||||
|
// Ensure nothing gets buffer. This works because we mock log repo getAll to return nothing
|
||||||
|
expect(await sut.getMessages(), isEmpty);
|
||||||
|
|
||||||
|
final insert = verify(() => mockLogRepo.insert(captureAny()));
|
||||||
|
insert.called(1);
|
||||||
|
final captured = insert.captured.firstOrNull as LogMessage;
|
||||||
|
expect(captured.message, _kInfoLog.message);
|
||||||
|
expect(captured.logger, _kInfoLog.logger);
|
||||||
|
|
||||||
|
verifyNever(() => mockLogRepo.insertAll(captureAny()));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group("Log Service Get messages:", () {
|
||||||
|
setUp(() {
|
||||||
|
when(() => mockLogRepo.getAll()).thenAnswer((_) async => [_kInfoLog]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fetches result from DB', () async {
|
||||||
|
expect(await sut.getMessages(), hasLength(1));
|
||||||
|
verify(() => mockLogRepo.getAll()).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Combines result from both DB + Buffer', () {
|
||||||
|
TestUtils.fakeAsync((time) async {
|
||||||
|
sut = await LogService.create(
|
||||||
|
logRepo: mockLogRepo,
|
||||||
|
storeRepo: mockStoreRepo,
|
||||||
|
shouldBuffer: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final logger = Logger(_kWarnLog.logger!);
|
||||||
|
logger.warning(_kWarnLog.message);
|
||||||
|
expect(await sut.getMessages(), hasLength(2)); // 1 - DB, 1 - Buff
|
||||||
|
|
||||||
|
final messages = await sut.getMessages();
|
||||||
|
// Logged time is assigned in the service for messages in the buffer, so compare manually
|
||||||
|
expect(messages.firstOrNull?.message, _kWarnLog.message);
|
||||||
|
expect(messages.firstOrNull?.logger, _kWarnLog.logger);
|
||||||
|
|
||||||
|
expect(messages.elementAtOrNull(1), _kInfoLog);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class MockStoreRepository extends Mock implements IStoreRepository {}
|
class MockStoreRepository extends Mock implements IStoreRepository {}
|
||||||
|
|
||||||
|
class MockLogRepository extends Mock implements ILogRepository {}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/user.interface.dart';
|
import 'package:immich_mobile/interfaces/user.interface.dart';
|
||||||
import 'package:immich_mobile/services/immich_logger.service.dart';
|
|
||||||
import 'package:immich_mobile/services/sync.service.dart';
|
import 'package:immich_mobile/services/sync.service.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
@@ -70,7 +71,10 @@ void main() {
|
|||||||
db.writeTxnSync(() => db.clearSync());
|
db.writeTxnSync(() => db.clearSync());
|
||||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||||
await Store.put(StoreKey.currentUser, owner);
|
await Store.put(StoreKey.currentUser, owner);
|
||||||
ImmichLogger();
|
await LogService.init(
|
||||||
|
logRepo: IsarLogRepository(db),
|
||||||
|
storeRepo: IsarStoreRepository(db),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
final List<Asset> initialAssets = [
|
final List<Asset> initialAssets = [
|
||||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:immich_mobile/interfaces/asset.interface.dart';
|
|||||||
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
import 'package:immich_mobile/interfaces/asset_media.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/auth.interface.dart';
|
import 'package:immich_mobile/interfaces/auth.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/auth_api.interface.dart';
|
import 'package:immich_mobile/interfaces/auth_api.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/backup.interface.dart';
|
import 'package:immich_mobile/interfaces/backup_album.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
import 'package:immich_mobile/interfaces/etag.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
|
||||||
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
import 'package:immich_mobile/interfaces/file_media.interface.dart';
|
||||||
@@ -18,7 +18,7 @@ class MockAssetRepository extends Mock implements IAssetRepository {}
|
|||||||
|
|
||||||
class MockUserRepository extends Mock implements IUserRepository {}
|
class MockUserRepository extends Mock implements IUserRepository {}
|
||||||
|
|
||||||
class MockBackupRepository extends Mock implements IBackupRepository {}
|
class MockBackupRepository extends Mock implements IBackupAlbumRepository {}
|
||||||
|
|
||||||
class MockExifInfoRepository extends Mock implements IExifInfoRepository {}
|
class MockExifInfoRepository extends Mock implements IExifInfoRepository {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:fake_async/fake_async.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
@@ -11,8 +13,8 @@ import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
|||||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/log.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
@@ -88,4 +90,36 @@ abstract final class TestUtils {
|
|||||||
WidgetController.hitTestWarningShouldBeFatal = true;
|
WidgetController.hitTestWarningShouldBeFatal = true;
|
||||||
HttpOverrides.global = MockHttpOverrides();
|
HttpOverrides.global = MockHttpOverrides();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Workaround till the following issue is resolved
|
||||||
|
// https://github.com/dart-lang/test/issues/2307
|
||||||
|
static T fakeAsync<T>(
|
||||||
|
Future<T> Function(FakeAsync _) callback, {
|
||||||
|
DateTime? initialTime,
|
||||||
|
}) {
|
||||||
|
late final T result;
|
||||||
|
Object? error;
|
||||||
|
StackTrace? stack;
|
||||||
|
FakeAsync(initialTime: initialTime).run((FakeAsync async) {
|
||||||
|
bool shouldPump = true;
|
||||||
|
unawaited(
|
||||||
|
callback(async).then<void>(
|
||||||
|
(value) => result = value,
|
||||||
|
onError: (e, s) {
|
||||||
|
error = e;
|
||||||
|
stack = s;
|
||||||
|
},
|
||||||
|
).whenComplete(() => shouldPump = false),
|
||||||
|
);
|
||||||
|
|
||||||
|
while (shouldPump) {
|
||||||
|
async.flushMicrotasks();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error != null) {
|
||||||
|
Error.throwWithStackTrace(error!, stack!);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7655,7 +7655,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@@ -11952,7 +11952,8 @@
|
|||||||
"SourceType": {
|
"SourceType": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"machine-learning",
|
"machine-learning",
|
||||||
"exif"
|
"exif",
|
||||||
|
"manual"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.127.0
|
* 1.128.0
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
@@ -3453,7 +3453,8 @@ export enum AlbumUserRole {
|
|||||||
}
|
}
|
||||||
export enum SourceType {
|
export enum SourceType {
|
||||||
MachineLearning = "machine-learning",
|
MachineLearning = "machine-learning",
|
||||||
Exif = "exif"
|
Exif = "exif",
|
||||||
|
Manual = "manual"
|
||||||
}
|
}
|
||||||
export enum AssetTypeEnum {
|
export enum AssetTypeEnum {
|
||||||
Image = "IMAGE",
|
Image = "IMAGE",
|
||||||
|
|||||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^11.0.1",
|
"@nestjs/bullmq": "^11.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.127.0",
|
"version": "1.128.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Test } from '@nestjs/testing';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ClassConstructor } from 'class-transformer';
|
import { ClassConstructor } from 'class-transformer';
|
||||||
import { PostgresJSDialect } from 'kysely-postgres-js';
|
import { PostgresJSDialect } from 'kysely-postgres-js';
|
||||||
|
import { ClsModule } from 'nestjs-cls';
|
||||||
import { KyselyModule } from 'nestjs-kysely';
|
import { KyselyModule } from 'nestjs-kysely';
|
||||||
import { OpenTelemetryModule } from 'nestjs-otel';
|
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||||
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||||
@@ -77,7 +78,7 @@ class SqlGenerator {
|
|||||||
await mkdir(this.options.targetDir);
|
await mkdir(this.options.targetDir);
|
||||||
|
|
||||||
process.env.DB_HOSTNAME = 'localhost';
|
process.env.DB_HOSTNAME = 'localhost';
|
||||||
const { database, otel } = new ConfigRepository().getEnv();
|
const { database, cls, otel } = new ConfigRepository().getEnv();
|
||||||
|
|
||||||
const moduleFixture = await Test.createTestingModule({
|
const moduleFixture = await Test.createTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -92,6 +93,7 @@ class SqlGenerator {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
ClsModule.forRoot(cls.config),
|
||||||
TypeOrmModule.forRoot({
|
TypeOrmModule.forRoot({
|
||||||
...database.config.typeorm,
|
...database.config.typeorm,
|
||||||
entities,
|
entities,
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
|||||||
|
|
||||||
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
|
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
|
||||||
|
|
||||||
|
export const MACHINE_LEARNING_PING_TIMEOUT = Number(process.env.MACHINE_LEARNING_PING_TIMEOUT || 2000);
|
||||||
|
export const MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME = Number(
|
||||||
|
process.env.MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME || 30_000,
|
||||||
|
);
|
||||||
|
|
||||||
export const citiesFile = 'cities500.txt';
|
export const citiesFile = 'cities500.txt';
|
||||||
|
|
||||||
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
|
export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
|
||||||
|
|||||||
16
server/src/db.d.ts
vendored
16
server/src/db.d.ts
vendored
@@ -29,7 +29,7 @@ export type JsonPrimitive = boolean | number | string | null;
|
|||||||
|
|
||||||
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
|
||||||
|
|
||||||
export type Sourcetype = 'exif' | 'machine-learning';
|
export type Sourcetype = 'exif' | 'machine-learning' | 'manual';
|
||||||
|
|
||||||
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ export interface Activity {
|
|||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
isLiked: Generated<boolean>;
|
isLiked: Generated<boolean>;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ export interface Albums {
|
|||||||
order: Generated<string>;
|
order: Generated<string>;
|
||||||
ownerId: string;
|
ownerId: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AlbumsAssetsAssets {
|
export interface AlbumsAssetsAssets {
|
||||||
@@ -79,6 +81,7 @@ export interface ApiKeys {
|
|||||||
name: string;
|
name: string;
|
||||||
permissions: Permission[];
|
permissions: Permission[];
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +106,7 @@ export interface AssetFiles {
|
|||||||
path: string;
|
path: string;
|
||||||
type: string;
|
type: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssetJobStatus {
|
export interface AssetJobStatus {
|
||||||
@@ -143,6 +147,7 @@ export interface Assets {
|
|||||||
thumbhash: Buffer | null;
|
thumbhash: Buffer | null;
|
||||||
type: string;
|
type: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AssetStack {
|
export interface AssetStack {
|
||||||
@@ -221,6 +226,7 @@ export interface Libraries {
|
|||||||
ownerId: string;
|
ownerId: string;
|
||||||
refreshedAt: Timestamp | null;
|
refreshedAt: Timestamp | null;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Memories {
|
export interface Memories {
|
||||||
@@ -236,6 +242,7 @@ export interface Memories {
|
|||||||
showAt: Timestamp | null;
|
showAt: Timestamp | null;
|
||||||
type: string;
|
type: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MemoriesAssetsAssets {
|
export interface MemoriesAssetsAssets {
|
||||||
@@ -271,6 +278,7 @@ export interface Partners {
|
|||||||
sharedById: string;
|
sharedById: string;
|
||||||
sharedWithId: string;
|
sharedWithId: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Person {
|
export interface Person {
|
||||||
@@ -285,6 +293,7 @@ export interface Person {
|
|||||||
ownerId: string;
|
ownerId: string;
|
||||||
thumbnailPath: Generated<string>;
|
thumbnailPath: Generated<string>;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Sessions {
|
export interface Sessions {
|
||||||
@@ -294,6 +303,7 @@ export interface Sessions {
|
|||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
token: string;
|
token: string;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
userId: string;
|
userId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,6 +313,7 @@ export interface SessionSyncCheckpoints {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
type: SyncEntityType;
|
type: SyncEntityType;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -358,6 +369,7 @@ export interface Tags {
|
|||||||
id: Generated<string>;
|
id: Generated<string>;
|
||||||
parentId: string | null;
|
parentId: string | null;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
userId: string;
|
userId: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
@@ -399,9 +411,11 @@ export interface Users {
|
|||||||
status: Generated<string>;
|
status: Generated<string>;
|
||||||
storageLabel: string | null;
|
storageLabel: string | null;
|
||||||
updatedAt: Generated<Timestamp>;
|
updatedAt: Generated<Timestamp>;
|
||||||
|
updateId: Generated<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsersAudit {
|
export interface UsersAudit {
|
||||||
|
id: Generated<string>;
|
||||||
userId: string;
|
userId: string;
|
||||||
deletedAt: Generated<Timestamp>;
|
deletedAt: Generated<Timestamp>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ export class ActivityEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_activity_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
albumId!: string;
|
albumId!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
Index,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
@@ -39,6 +40,10 @@ export class AlbumEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_albums_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@DeleteDateColumn({ type: 'timestamptz' })
|
@DeleteDateColumn({ type: 'timestamptz' })
|
||||||
deletedAt!: Date | null;
|
deletedAt!: Date | null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('api_keys')
|
@Entity('api_keys')
|
||||||
export class APIKeyEntity {
|
export class APIKeyEntity {
|
||||||
@@ -27,4 +27,8 @@ export class APIKeyEntity {
|
|||||||
|
|
||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_api_keys_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export class AssetFileEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_asset_files_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
type!: AssetFileType;
|
type!: AssetFileType;
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ export class AssetEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_assets_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@DeleteDateColumn({ type: 'timestamptz', nullable: true })
|
@DeleteDateColumn({ type: 'timestamptz', nullable: true })
|
||||||
deletedAt!: Date | null;
|
deletedAt!: Date | null;
|
||||||
|
|
||||||
@@ -256,6 +260,7 @@ export function hasPeople<O>(qb: SelectQueryBuilder<DB, 'assets', O>, personIds:
|
|||||||
.selectFrom('asset_faces')
|
.selectFrom('asset_faces')
|
||||||
.select('assetId')
|
.select('assetId')
|
||||||
.where('personId', '=', anyUuid(personIds!))
|
.where('personId', '=', anyUuid(personIds!))
|
||||||
|
.where('deletedAt', 'is', null)
|
||||||
.groupBy('assetId')
|
.groupBy('assetId')
|
||||||
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
.having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length)
|
||||||
.as('has_people'),
|
.as('has_people'),
|
||||||
@@ -347,7 +352,7 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
|
|||||||
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
|
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
|
||||||
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
|
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
|
||||||
options.isArchived ??= options.withArchived ? undefined : false;
|
options.isArchived ??= options.withArchived ? undefined : false;
|
||||||
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore);
|
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
|
||||||
return kysely
|
return kysely
|
||||||
.withPlugin(joinDeduplicationPlugin)
|
.withPlugin(joinDeduplicationPlugin)
|
||||||
.selectFrom('assets')
|
.selectFrom('assets')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
Index,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
@@ -42,6 +43,10 @@ export class LibraryEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_libraries_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@DeleteDateColumn({ type: 'timestamptz' })
|
@DeleteDateColumn({ type: 'timestamptz' })
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
DeleteDateColumn,
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
Index,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
@@ -30,6 +31,10 @@ export class MemoryEntity<T extends MemoryType = MemoryType> {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_memories_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@DeleteDateColumn({ type: 'timestamptz' })
|
@DeleteDateColumn({ type: 'timestamptz' })
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
Index,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
@Entity('partners')
|
@Entity('partners')
|
||||||
export class PartnerEntity {
|
export class PartnerEntity {
|
||||||
@@ -23,6 +32,10 @@ export class PartnerEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_partners_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: false })
|
@Column({ type: 'boolean', default: false })
|
||||||
inTimeline!: boolean;
|
inTimeline!: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
Index,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToMany,
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@@ -23,6 +24,10 @@ export class PersonEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_person_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ExpressionBuilder } from 'kysely';
|
import { ExpressionBuilder } from 'kysely';
|
||||||
import { DB } from 'src/db';
|
import { DB } from 'src/db';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('sessions')
|
@Entity('sessions')
|
||||||
export class SessionEntity {
|
export class SessionEntity {
|
||||||
@@ -23,6 +23,10 @@ export class SessionEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_sessions_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId!: string;
|
||||||
|
|
||||||
@Column({ default: '' })
|
@Column({ default: '' })
|
||||||
deviceType!: string;
|
deviceType!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SessionEntity } from 'src/entities/session.entity';
|
import { SessionEntity } from 'src/entities/session.entity';
|
||||||
import { SyncEntityType } from 'src/enum';
|
import { SyncEntityType } from 'src/enum';
|
||||||
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('session_sync_checkpoints')
|
@Entity('session_sync_checkpoints')
|
||||||
export class SessionSyncCheckpointEntity {
|
export class SessionSyncCheckpointEntity {
|
||||||
@@ -19,6 +19,10 @@ export class SessionSyncCheckpointEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_session_sync_checkpoints_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
ack!: string;
|
ack!: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
Index,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
@@ -30,6 +31,10 @@ export class TagEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_tags_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', nullable: true, default: null })
|
@Column({ type: 'varchar', nullable: true, default: null })
|
||||||
color!: string | null;
|
color!: string | null;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('users_audit')
|
@Entity('users_audit')
|
||||||
@Index('IDX_users_audit_deleted_at_asc_user_id_asc', ['deletedAt', 'userId'])
|
|
||||||
export class UserAuditEntity {
|
export class UserAuditEntity {
|
||||||
@PrimaryGeneratedColumn('increment')
|
@PrimaryColumn({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
id!: number;
|
id!: string;
|
||||||
|
|
||||||
@Column({ type: 'uuid' })
|
@Column({ type: 'uuid' })
|
||||||
userId!: string;
|
userId!: string;
|
||||||
|
|
||||||
@CreateDateColumn({ type: 'timestamptz' })
|
@Index('IDX_users_audit_deleted_at')
|
||||||
|
@CreateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' })
|
||||||
deletedAt!: Date;
|
deletedAt!: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ export class UserEntity {
|
|||||||
@UpdateDateColumn({ type: 'timestamptz' })
|
@UpdateDateColumn({ type: 'timestamptz' })
|
||||||
updatedAt!: Date;
|
updatedAt!: Date;
|
||||||
|
|
||||||
|
@Index('IDX_users_update_id')
|
||||||
|
@Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' })
|
||||||
|
updateId?: string;
|
||||||
|
|
||||||
@OneToMany(() => TagEntity, (tag) => tag.user)
|
@OneToMany(() => TagEntity, (tag) => tag.user)
|
||||||
tags!: TagEntity[];
|
tags!: TagEntity[];
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ export enum AssetStatus {
|
|||||||
export enum SourceType {
|
export enum SourceType {
|
||||||
MACHINE_LEARNING = 'machine-learning',
|
MACHINE_LEARNING = 'machine-learning',
|
||||||
EXIF = 'exif',
|
EXIF = 'exif',
|
||||||
|
MANUAL = 'manual',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ManualJobName {
|
export enum ManualJobName {
|
||||||
|
|||||||
134
server/src/migrations/1740586617223-AddUpdateIdColumns.ts
Normal file
134
server/src/migrations/1740586617223-AddUpdateIdColumns.ts
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddUpdateIdColumns1740586617223 implements MigrationInterface {
|
||||||
|
name = 'AddUpdateIdColumns1740586617223'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
create or replace function immich_uuid_v7(p_timestamp timestamp with time zone default clock_timestamp())
|
||||||
|
returns uuid
|
||||||
|
as $$
|
||||||
|
select encode(
|
||||||
|
set_bit(
|
||||||
|
set_bit(
|
||||||
|
overlay(uuid_send(gen_random_uuid())
|
||||||
|
placing substring(int8send(floor(extract(epoch from p_timestamp) * 1000)::bigint) from 3)
|
||||||
|
from 1 for 6
|
||||||
|
),
|
||||||
|
52, 1
|
||||||
|
),
|
||||||
|
53, 1
|
||||||
|
),
|
||||||
|
'hex')::uuid;
|
||||||
|
$$
|
||||||
|
language SQL
|
||||||
|
volatile;
|
||||||
|
`)
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION updated_at() RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
as $$
|
||||||
|
BEGIN
|
||||||
|
return new;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
`)
|
||||||
|
await queryRunner.query(`ALTER TABLE "person" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "asset_files" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "libraries" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "tags" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "assets" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "albums" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "sessions" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "session_sync_checkpoints" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "partners" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "memories" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "api_keys" ADD "updateId" uuid`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "activity" ADD "updateId" uuid`);
|
||||||
|
|
||||||
|
await queryRunner.query(`UPDATE "person" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "asset_files" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "libraries" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "tags" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "assets" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "users" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "albums" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "sessions" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "session_sync_checkpoints" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "partners" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "memories" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "api_keys" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
await queryRunner.query(`UPDATE "activity" SET "updateId" = immich_uuid_v7("updatedAt")`);
|
||||||
|
|
||||||
|
await queryRunner.query(`ALTER TABLE "person" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "asset_files" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "libraries" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "tags" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "albums" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "sessions" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "session_sync_checkpoints" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "partners" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "memories" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "api_keys" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "activity" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`);
|
||||||
|
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_person_update_id" ON "person" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_asset_files_update_id" ON "asset_files" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_libraries_update_id" ON "libraries" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_tags_update_id" ON "tags" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_assets_update_id" ON "assets" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_users_update_id" ON "users" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_albums_update_id" ON "albums" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_sessions_update_id" ON "sessions" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_session_sync_checkpoints_update_id" ON "session_sync_checkpoints" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_partners_update_id" ON "partners" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_memories_update_id" ON "memories" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_api_keys_update_id" ON "api_keys" ("updateId")`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_activity_update_id" ON "activity" ("updateId")`);
|
||||||
|
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION updated_at() RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
as $$
|
||||||
|
DECLARE
|
||||||
|
clock_timestamp TIMESTAMP := clock_timestamp();
|
||||||
|
BEGIN
|
||||||
|
new."updatedAt" = clock_timestamp;
|
||||||
|
new."updateId" = immich_uuid_v7(clock_timestamp);
|
||||||
|
return new;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "api_keys" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "memories" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "partners" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "session_sync_checkpoints" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "libraries" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "asset_files" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "updateId"`);
|
||||||
|
await queryRunner.query(`DROP FUNCTION immich_uuid_v7`);
|
||||||
|
await queryRunner.query(`
|
||||||
|
CREATE OR REPLACE FUNCTION updated_at() RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
as $$
|
||||||
|
BEGIN
|
||||||
|
new."updatedAt" = now();
|
||||||
|
return new;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class UsersAuditUuidv7PrimaryKey1740595460866 implements MigrationInterface {
|
||||||
|
name = 'UsersAuditUuidv7PrimaryKey1740595460866'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_users_audit_deleted_at_asc_user_id_asc"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" DROP CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" DROP COLUMN "id"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" ADD "id" uuid NOT NULL DEFAULT immich_uuid_v7()`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" ADD CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" PRIMARY KEY ("id")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" ALTER COLUMN "deletedAt" SET DEFAULT clock_timestamp()`)
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_users_audit_deleted_at" ON "users_audit" ("deletedAt")`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_users_audit_deleted_at"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" DROP CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" DROP COLUMN "id"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" ADD "id" SERIAL NOT NULL`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" ADD CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" PRIMARY KEY ("id")`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "users_audit" ALTER COLUMN "deletedAt" SET DEFAULT now()`);
|
||||||
|
await queryRunner.query(`CREATE INDEX "IDX_users_audit_deleted_at_asc_user_id_asc" ON "users_audit" ("userId", "deletedAt") `);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
server/src/migrations/1740619600996-AddManualSourceType.ts
Normal file
27
server/src/migrations/1740619600996-AddManualSourceType.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddManualSourceType1740619600996 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TYPE sourceType ADD VALUE 'manual'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// Prior to this migration, manually tagged pictures had the 'machine-learning' type
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "asset_faces" SET "sourceType" = 'machine-learning' WHERE "sourceType" = 'manual';`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Postgres doesn't allow removing values from enums, we have to recreate the type
|
||||||
|
await queryRunner.query(`ALTER TYPE sourceType RENAME TO oldSourceType`);
|
||||||
|
await queryRunner.query(`CREATE TYPE sourceType AS ENUM ('machine-learning', 'exif');`);
|
||||||
|
|
||||||
|
await queryRunner.query(`ALTER TABLE "asset_faces" ALTER COLUMN "sourceType" DROP DEFAULT;`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "asset_faces" ALTER COLUMN "sourceType" TYPE sourceType USING "sourceType"::text::sourceType;`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "asset_faces" ALTER COLUMN "sourceType" SET DEFAULT 'machine-learning'::sourceType;`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`DROP TYPE oldSourceType;`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class UnsetStackedAssetsFromDuplicateStatus1740654480319 implements MigrationInterface {
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`
|
||||||
|
update assets
|
||||||
|
set "duplicateId" = null
|
||||||
|
where "stackId" is not null`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(): Promise<void> {
|
||||||
|
// No need to revert this migration
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,9 +55,10 @@ with
|
|||||||
inner join "exif" on "a"."id" = "exif"."assetId"
|
inner join "exif" on "a"."id" = "exif"."assetId"
|
||||||
)
|
)
|
||||||
select
|
select
|
||||||
(
|
date_part(
|
||||||
(now() at time zone 'UTC')::date - ("localDateTime" at time zone 'UTC')::date
|
'year',
|
||||||
) / 365 as "yearsAgo",
|
("localDateTime" at time zone 'UTC')::date
|
||||||
|
)::int as "year",
|
||||||
json_agg("res") as "assets"
|
json_agg("res") as "assets"
|
||||||
from
|
from
|
||||||
"res"
|
"res"
|
||||||
@@ -333,6 +334,7 @@ with
|
|||||||
and "assets"."duplicateId" is not null
|
and "assets"."duplicateId" is not null
|
||||||
and "assets"."deletedAt" is null
|
and "assets"."deletedAt" is null
|
||||||
and "assets"."isVisible" = $2
|
and "assets"."isVisible" = $2
|
||||||
|
and "assets"."stackId" is null
|
||||||
group by
|
group by
|
||||||
"assets"."duplicateId"
|
"assets"."duplicateId"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ with
|
|||||||
and "assets"."isVisible" = $3
|
and "assets"."isVisible" = $3
|
||||||
and "assets"."type" = $4
|
and "assets"."type" = $4
|
||||||
and "assets"."id" != $5::uuid
|
and "assets"."id" != $5::uuid
|
||||||
|
and "assets"."stackId" is null
|
||||||
order by
|
order by
|
||||||
smart_search.embedding <=> $6
|
smart_search.embedding <=> $6
|
||||||
limit
|
limit
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ export class AssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
|
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
|
||||||
getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise<DayOfYearAssets[]> {
|
getByDayOfYear(ownerIds: string[], { day, month }: MonthDay) {
|
||||||
return this.db
|
return this.db
|
||||||
.with('res', (qb) =>
|
.with('res', (qb) =>
|
||||||
qb
|
qb
|
||||||
@@ -239,16 +239,12 @@ export class AssetRepository {
|
|||||||
.select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo')),
|
.select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo')),
|
||||||
)
|
)
|
||||||
.selectFrom('res')
|
.selectFrom('res')
|
||||||
.select(
|
.select(sql<number>`date_part('year', ("localDateTime" at time zone 'UTC')::date)::int`.as('year'))
|
||||||
sql<number>`((now() at time zone 'UTC')::date - ("localDateTime" at time zone 'UTC')::date) / 365`.as(
|
|
||||||
'yearsAgo',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.select((eb) => eb.fn.jsonAgg(eb.table('res')).as('assets'))
|
.select((eb) => eb.fn.jsonAgg(eb.table('res')).as('assets'))
|
||||||
.groupBy(sql`("localDateTime" at time zone 'UTC')::date`)
|
.groupBy(sql`("localDateTime" at time zone 'UTC')::date`)
|
||||||
.orderBy(sql`("localDateTime" at time zone 'UTC')::date`, 'desc')
|
.orderBy(sql`("localDateTime" at time zone 'UTC')::date`, 'desc')
|
||||||
.limit(10)
|
.limit(10)
|
||||||
.execute() as any as Promise<DayOfYearAssets[]>;
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
@@ -794,6 +790,7 @@ export class AssetRepository {
|
|||||||
.where('assets.duplicateId', 'is not', null)
|
.where('assets.duplicateId', 'is not', null)
|
||||||
.where('assets.deletedAt', 'is', null)
|
.where('assets.deletedAt', 'is', null)
|
||||||
.where('assets.isVisible', '=', true)
|
.where('assets.isVisible', '=', true)
|
||||||
|
.where('assets.stackId', 'is', null)
|
||||||
.groupBy('assets.duplicateId'),
|
.groupBy('assets.duplicateId'),
|
||||||
)
|
)
|
||||||
.with('unique', (qb) =>
|
.with('unique', (qb) =>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
import { ImmichWorker } from 'src/enum';
|
import { ImmichWorker } from 'src/enum';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository, MyConsoleLogger } from 'src/repositories/logging.repository';
|
||||||
import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
describe(LoggingRepository.name, () => {
|
describe(LoggingRepository.name, () => {
|
||||||
@@ -18,23 +18,25 @@ describe(LoggingRepository.name, () => {
|
|||||||
} as unknown as Mocked<ClsService>;
|
} as unknown as Mocked<ClsService>;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('formatContext', () => {
|
describe(MyConsoleLogger.name, () => {
|
||||||
it('should use colors', () => {
|
describe('formatContext', () => {
|
||||||
configMock.getEnv.mockReturnValue(mockEnvData({ noColor: false }));
|
it('should use colors', () => {
|
||||||
|
sut = new LoggingRepository(clsMock, configMock);
|
||||||
|
sut.setAppName(ImmichWorker.API);
|
||||||
|
|
||||||
sut = new LoggingRepository(clsMock, configMock);
|
const logger = new MyConsoleLogger(clsMock, { color: true });
|
||||||
sut.setAppName(ImmichWorker.API);
|
|
||||||
|
|
||||||
expect(sut['formatContext']('context')).toBe('\u001B[33m[Api:context]\u001B[39m ');
|
expect(logger.formatContext('context')).toBe('\u001B[33m[Api:context]\u001B[39m ');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not use colors when noColor is true', () => {
|
it('should not use colors when color is false', () => {
|
||||||
configMock.getEnv.mockReturnValue(mockEnvData({ noColor: true }));
|
sut = new LoggingRepository(clsMock, configMock);
|
||||||
|
sut.setAppName(ImmichWorker.API);
|
||||||
|
|
||||||
sut = new LoggingRepository(clsMock, configMock);
|
const logger = new MyConsoleLogger(clsMock, { color: false });
|
||||||
sut.setAppName(ImmichWorker.API);
|
|
||||||
|
|
||||||
expect(sut['formatContext']('context')).toBe('[Api:context] ');
|
expect(logger.formatContext('context')).toBe('[Api:context] ');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { Telemetry } from 'src/decorators';
|
|||||||
import { LogLevel } from 'src/enum';
|
import { LogLevel } from 'src/enum';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
|
|
||||||
|
type LogDetails = any[];
|
||||||
|
type LogFunction = () => string;
|
||||||
|
|
||||||
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
|
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
|
||||||
|
|
||||||
enum LogColor {
|
enum LogColor {
|
||||||
@@ -16,38 +19,26 @@ enum LogColor {
|
|||||||
CYAN_BRIGHT = 96,
|
CYAN_BRIGHT = 96,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({ scope: Scope.TRANSIENT })
|
let appName: string | undefined;
|
||||||
@Telemetry({ enabled: false })
|
let logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
|
||||||
export class LoggingRepository extends ConsoleLogger {
|
|
||||||
private static logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
|
export class MyConsoleLogger extends ConsoleLogger {
|
||||||
private noColor: boolean;
|
private isColorEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cls: ClsService,
|
private cls: ClsService,
|
||||||
configRepository: ConfigRepository,
|
options?: { color?: boolean; context?: string },
|
||||||
) {
|
) {
|
||||||
super(LoggingRepository.name);
|
super(options?.context || MyConsoleLogger.name);
|
||||||
|
this.isColorEnabled = options?.color || false;
|
||||||
const { noColor } = configRepository.getEnv();
|
|
||||||
this.noColor = noColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static appName?: string = undefined;
|
|
||||||
|
|
||||||
setAppName(name: string): void {
|
|
||||||
LoggingRepository.appName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLevelEnabled(level: LogLevel) {
|
isLevelEnabled(level: LogLevel) {
|
||||||
return isLogLevelEnabled(level, LoggingRepository.logLevels);
|
return isLogLevelEnabled(level, logLevels);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLogLevel(level: LogLevel | false): void {
|
formatContext(context: string): string {
|
||||||
LoggingRepository.logLevels = level ? LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)) : [];
|
let prefix = appName || '';
|
||||||
}
|
|
||||||
|
|
||||||
protected formatContext(context: string): string {
|
|
||||||
let prefix = LoggingRepository.appName || '';
|
|
||||||
if (context) {
|
if (context) {
|
||||||
prefix += (prefix ? ':' : '') + context;
|
prefix += (prefix ? ':' : '') + context;
|
||||||
}
|
}
|
||||||
@@ -74,6 +65,105 @@ export class LoggingRepository extends ConsoleLogger {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private withColor(text: string, color: LogColor) {
|
private withColor(text: string, color: LogColor) {
|
||||||
return this.noColor ? text : `\u001B[${color}m${text}\u001B[39m`;
|
return this.isColorEnabled ? `\u001B[${color}m${text}\u001B[39m` : text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ scope: Scope.TRANSIENT })
|
||||||
|
@Telemetry({ enabled: false })
|
||||||
|
export class LoggingRepository {
|
||||||
|
private logger: MyConsoleLogger;
|
||||||
|
|
||||||
|
constructor(cls: ClsService, configRepository: ConfigRepository) {
|
||||||
|
const { noColor } = configRepository.getEnv();
|
||||||
|
this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor });
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppName(name: string): void {
|
||||||
|
appName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setContext(context: string) {
|
||||||
|
this.logger.setContext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
isLevelEnabled(level: LogLevel) {
|
||||||
|
return this.logger.isLevelEnabled(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLogLevel(level: LogLevel | false): void {
|
||||||
|
logLevels = level ? LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose(message: string, ...details: LogDetails) {
|
||||||
|
this.handleMessage(LogLevel.VERBOSE, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
verboseFn(message: LogFunction, ...details: LogDetails) {
|
||||||
|
this.handleFunction(LogLevel.VERBOSE, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message: string, ...details: LogDetails) {
|
||||||
|
this.handleMessage(LogLevel.DEBUG, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugFn(message: LogFunction, ...details: LogDetails) {
|
||||||
|
this.handleFunction(LogLevel.DEBUG, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
log(message: string, ...details: LogDetails) {
|
||||||
|
this.handleMessage(LogLevel.LOG, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string, ...details: LogDetails) {
|
||||||
|
this.handleMessage(LogLevel.WARN, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string | Error, ...details: LogDetails) {
|
||||||
|
this.handleMessage(LogLevel.ERROR, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
fatal(message: string, ...details: LogDetails) {
|
||||||
|
this.handleMessage(LogLevel.FATAL, message, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFunction(level: LogLevel, message: LogFunction, details: LogDetails[]) {
|
||||||
|
if (this.logger.isLevelEnabled(level)) {
|
||||||
|
this.handleMessage(level, message(), details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleMessage(level: LogLevel, message: string | Error, details: LogDetails[]) {
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.VERBOSE: {
|
||||||
|
this.logger.verbose(message, ...details);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LogLevel.DEBUG: {
|
||||||
|
this.logger.debug(message, ...details);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LogLevel.LOG: {
|
||||||
|
this.logger.log(message, ...details);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LogLevel.WARN: {
|
||||||
|
this.logger.warn(message, ...details);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LogLevel.ERROR: {
|
||||||
|
this.logger.error(message, ...details);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LogLevel.FATAL: {
|
||||||
|
this.logger.fatal(message, ...details);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME, MACHINE_LEARNING_PING_TIMEOUT } from 'src/constants';
|
||||||
import { CLIPConfig } from 'src/dtos/model-config.dto';
|
import { CLIPConfig } from 'src/dtos/model-config.dto';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
|
||||||
@@ -55,16 +56,80 @@ export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | Fa
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MachineLearningRepository {
|
export class MachineLearningRepository {
|
||||||
|
// Note that deleted URL's are not removed from this map (ie: they're leaked)
|
||||||
|
// Cleaning them up is low priority since there should be very few over a
|
||||||
|
// typical server uptime cycle
|
||||||
|
private urlAvailability: {
|
||||||
|
[url: string]:
|
||||||
|
| {
|
||||||
|
active: boolean;
|
||||||
|
lastChecked: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
};
|
||||||
|
|
||||||
constructor(private logger: LoggingRepository) {
|
constructor(private logger: LoggingRepository) {
|
||||||
this.logger.setContext(MachineLearningRepository.name);
|
this.logger.setContext(MachineLearningRepository.name);
|
||||||
|
this.urlAvailability = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
private setUrlAvailability(url: string, active: boolean) {
|
||||||
|
const current = this.urlAvailability[url];
|
||||||
|
if (current?.active !== active) {
|
||||||
|
this.logger.verbose(`Setting ${url} ML server to ${active ? 'active' : 'inactive'}.`);
|
||||||
|
}
|
||||||
|
this.urlAvailability[url] = {
|
||||||
|
active,
|
||||||
|
lastChecked: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async checkAvailability(url: string) {
|
||||||
|
let active = false;
|
||||||
|
try {
|
||||||
|
const response = await fetch(new URL('/ping', url), {
|
||||||
|
signal: AbortSignal.timeout(MACHINE_LEARNING_PING_TIMEOUT),
|
||||||
|
});
|
||||||
|
active = response.ok;
|
||||||
|
} catch {}
|
||||||
|
this.setUrlAvailability(url, active);
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async shouldSkipUrl(url: string) {
|
||||||
|
const availability = this.urlAvailability[url];
|
||||||
|
if (availability === undefined) {
|
||||||
|
// If this is a new endpoint, then check inline and skip if it fails
|
||||||
|
if (!(await this.checkAvailability(url))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!availability.active && Date.now() - availability.lastChecked < MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME) {
|
||||||
|
// If this is an old inactive endpoint that hasn't been checked in a
|
||||||
|
// while then check but don't wait for the result, just skip it
|
||||||
|
// This avoids delays on every search whilst allowing higher priority
|
||||||
|
// ML servers to recover over time.
|
||||||
|
void this.checkAvailability(url);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async predict<T>(urls: string[], payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
|
private async predict<T>(urls: string[], payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
|
||||||
const formData = await this.getFormData(payload, config);
|
const formData = await this.getFormData(payload, config);
|
||||||
|
let urlCounter = 0;
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
|
urlCounter++;
|
||||||
|
const isLast = urlCounter >= urls.length;
|
||||||
|
if (!isLast && (await this.shouldSkipUrl(url))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData });
|
const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData });
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
this.setUrlAvailability(url, true);
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +141,7 @@ export class MachineLearningRepository {
|
|||||||
`Machine learning request to "${url}" failed: ${error instanceof Error ? error.message : error}`,
|
`Machine learning request to "${url}" failed: ${error instanceof Error ? error.message : error}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.setUrlAvailability(url, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Machine learning request '${JSON.stringify(config)}' failed for all URLs`);
|
throw new Error(`Machine learning request '${JSON.stringify(config)}' failed for all URLs`);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { citiesFile } from 'src/constants';
|
|||||||
import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
|
import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
|
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
|
||||||
import { LogLevel, SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
@@ -137,9 +137,7 @@ export class MapRepository {
|
|||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|
||||||
if (response) {
|
if (response) {
|
||||||
if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) {
|
this.logger.verboseFn(() => `Raw: ${JSON.stringify(response, null, 2)}`);
|
||||||
this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { countryCode, name: city, admin1Name } = response;
|
const { countryCode, name: city, admin1Name } = response;
|
||||||
const country = getName(countryCode, 'en') ?? null;
|
const country = getName(countryCode, 'en') ?? null;
|
||||||
@@ -167,9 +165,8 @@ export class MapRepository {
|
|||||||
return { country: null, state: null, city: null };
|
return { country: null, state: null, city: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) {
|
this.logger.verboseFn(() => `Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
|
||||||
this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
|
|
||||||
}
|
|
||||||
const { admin_a3 } = ne_response;
|
const { admin_a3 } = ne_response;
|
||||||
const country = getName(admin_a3, 'en') ?? null;
|
const country = getName(admin_a3, 'en') ?? null;
|
||||||
const state = null;
|
const state = null;
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ export class MetadataRepository {
|
|||||||
this.logger.setContext(MetadataRepository.name);
|
this.logger.setContext(MetadataRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setMaxConcurrency(concurrency: number) {
|
||||||
|
this.exiftool.batchCluster.setMaxProcs(concurrency);
|
||||||
|
}
|
||||||
|
|
||||||
async teardown() {
|
async teardown() {
|
||||||
await this.exiftool.end();
|
await this.exiftool.end();
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user