Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan Jogenfors
43d66c7a36 ignore perm errors 2025-02-07 00:30:28 +01:00
119 changed files with 1249 additions and 2825 deletions

6
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.49",
"version": "2.2.48",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.49",
"version": "2.2.48",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -52,7 +52,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.126.0",
"version": "1.125.7",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.49",
"version": "2.2.48",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.0"
constraints = "4.52.0"
version = "4.50.0"
constraints = "4.50.0"
hashes = [
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
"h1:0qvD5ZKn2tMZ8cOjQrUSITIC9tKCZbrSaSswV9lOyiU=",
"h1:4N0gplrZ0zOsJv3Kx1VfIx2FwrZHbYU0Un2yfiLZIGQ=",
"h1:81AMQq4kNKU/35U8ElQegUxG4E6xB0erIjG5xVmjIyo=",
"h1:EEQNADUmV3IL6x00yzy04i7OCSLeOMgM9XQkV3w71gA=",
"h1:HD0KI7td6oiSSAnJNn8UPSGf+hKiTo4JVQYfAiU1SqM=",
"h1:Hl+o5LtcvZg2f3l1hh9vaG/DFK6k+dTIZSeM0lXyfpo=",
"h1:ZUO2oIJ6jtZdvl816h0cEIiIeZ/fFCF64+abGEVxZZM=",
"h1:Zio80fnEeUKdlSOhTVskMEFSLUQ6TMsMKnXc+Dy2P2A=",
"h1:aLLvg36evTyqjtXGV2MjAV8imktXFmry7p/xCu9GQC4=",
"h1:azL05eWyy2V8SWkbZZImPWvv8ynG4eqmrbZhjXBDFug=",
"h1:ckMysHY4fJmr7o58XMi+DdgOTB/U/Mf1u1JA9ly3g/I=",
"h1:jxOwjDNjt5WCb4YjjiMsman91O8Y+MAPz6UwJ4a6F+0=",
"h1:u4OfnjSLa4Wk1IUFAzrvMnGgr8MvRHEWVDHEScPK2E8=",
"h1:wQkR1oeSkzlHn3rnVuLJRJLBHlg4EHt7Y64DeTjfkjQ=",
"zh:0ef99ed39472a94e6a0d6fa733cf0a46bce3bf66eba2873efae8846efdddc237",
"zh:2929cbbffcead171d45c88e4a7a59e9c013ea775dafa68b10da8db7cd04b6140",
"zh:462601c87118088e1a718842e367af7d8e7620598d426980a6d6b33de759865e",
"zh:56766eb62a74a9d88d9efb8486dd3a0c5c9db873d0a980ae9ef1e8af27d74231",
"zh:6b4e8810d99498a5a20a5872982a0f1354e79cfc4a7dfe7cc656f1c7eaae47d8",
"zh:6d65bdb4ec94b6eecc8abe26d94e2ca09262dc1e7a9934db829f418be0119920",
"zh:71adeaf31e41a358ec6095004062e43f56ee7d4b2504e5613ab351d511695641",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
"zh:89761c15908ccc2cf9c50bb5cb3be45d3ad0c45fc7c608c6b95f48c0288b7160",
"zh:8cc5d7c5939da89cfd01f3e51c84f3576564783acea9db86bd9e32049805ed96",
"zh:987cff8225b1dd436cdcb4fc6228689ae7e4281de6896412a2a9a3325c49f05e",
"zh:991e83ebb89867d71e01a1c215ed159efb425683b0a44707be8579eb0a337f06",
"zh:ab8177ae2d8f5cfa90043a6f867435012cae115f6061b832a7e2462e0ae87a67",
"zh:d1ca34df1398f201274a6a18102975148c10ca15aa43cfc56cc9897620929509",
"zh:d34946f70201baf6dda03e3b294c6bbe40d95d0278e97b9f636ded94822b24ac",
]
}

View File

@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.52.0"
version = "4.50.0"
}
}
}

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.52.0"
constraints = "4.52.0"
version = "4.50.0"
constraints = "4.50.0"
hashes = [
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
"h1:0qvD5ZKn2tMZ8cOjQrUSITIC9tKCZbrSaSswV9lOyiU=",
"h1:4N0gplrZ0zOsJv3Kx1VfIx2FwrZHbYU0Un2yfiLZIGQ=",
"h1:81AMQq4kNKU/35U8ElQegUxG4E6xB0erIjG5xVmjIyo=",
"h1:EEQNADUmV3IL6x00yzy04i7OCSLeOMgM9XQkV3w71gA=",
"h1:HD0KI7td6oiSSAnJNn8UPSGf+hKiTo4JVQYfAiU1SqM=",
"h1:Hl+o5LtcvZg2f3l1hh9vaG/DFK6k+dTIZSeM0lXyfpo=",
"h1:ZUO2oIJ6jtZdvl816h0cEIiIeZ/fFCF64+abGEVxZZM=",
"h1:Zio80fnEeUKdlSOhTVskMEFSLUQ6TMsMKnXc+Dy2P2A=",
"h1:aLLvg36evTyqjtXGV2MjAV8imktXFmry7p/xCu9GQC4=",
"h1:azL05eWyy2V8SWkbZZImPWvv8ynG4eqmrbZhjXBDFug=",
"h1:ckMysHY4fJmr7o58XMi+DdgOTB/U/Mf1u1JA9ly3g/I=",
"h1:jxOwjDNjt5WCb4YjjiMsman91O8Y+MAPz6UwJ4a6F+0=",
"h1:u4OfnjSLa4Wk1IUFAzrvMnGgr8MvRHEWVDHEScPK2E8=",
"h1:wQkR1oeSkzlHn3rnVuLJRJLBHlg4EHt7Y64DeTjfkjQ=",
"zh:0ef99ed39472a94e6a0d6fa733cf0a46bce3bf66eba2873efae8846efdddc237",
"zh:2929cbbffcead171d45c88e4a7a59e9c013ea775dafa68b10da8db7cd04b6140",
"zh:462601c87118088e1a718842e367af7d8e7620598d426980a6d6b33de759865e",
"zh:56766eb62a74a9d88d9efb8486dd3a0c5c9db873d0a980ae9ef1e8af27d74231",
"zh:6b4e8810d99498a5a20a5872982a0f1354e79cfc4a7dfe7cc656f1c7eaae47d8",
"zh:6d65bdb4ec94b6eecc8abe26d94e2ca09262dc1e7a9934db829f418be0119920",
"zh:71adeaf31e41a358ec6095004062e43f56ee7d4b2504e5613ab351d511695641",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
"zh:89761c15908ccc2cf9c50bb5cb3be45d3ad0c45fc7c608c6b95f48c0288b7160",
"zh:8cc5d7c5939da89cfd01f3e51c84f3576564783acea9db86bd9e32049805ed96",
"zh:987cff8225b1dd436cdcb4fc6228689ae7e4281de6896412a2a9a3325c49f05e",
"zh:991e83ebb89867d71e01a1c215ed159efb425683b0a44707be8579eb0a337f06",
"zh:ab8177ae2d8f5cfa90043a6f867435012cae115f6061b832a7e2462e0ae87a67",
"zh:d1ca34df1398f201274a6a18102975148c10ca15aa43cfc56cc9897620929509",
"zh:d34946f70201baf6dda03e3b294c6bbe40d95d0278e97b9f636ded94822b24ac",
]
}

View File

@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.52.0"
version = "4.50.0"
}
}
}

View File

@@ -103,7 +103,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:11.5.1-ubuntu@sha256:9a4ab78cec1a2ec7d1ca5dfd5aacec6412706a1bc9e971fc7184e2f6696a63f5
image: grafana/grafana:11.4.0-ubuntu@sha256:afccec22ba0e4815cca1d2bf3836e414322390dc78d77f1851976ffa8d61051c
volumes:
- grafana-data:/var/lib/grafana

View File

@@ -76,7 +76,7 @@ To see local changes to `@immich/ui` in Immich, do the following:
### Mobile app
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x and FVM to be installed on your system.
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.

View File

@@ -11,7 +11,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- ARM NN (Mali)
- CUDA (NVIDIA GPUs with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher)
- OpenVINO (Intel GPUs such as Iris Xe and Arc)
- OpenVINO (Intel discrete GPUs such as Iris Xe and Arc)
## Limitations
@@ -43,9 +43,8 @@ You do not need to redo any machine learning jobs after enabling hardware accele
#### OpenVINO
- Integrated GPUs are more likely to experience issues than discrete GPUs, especially for older processors or servers with low RAM.
- The server must have a discrete GPU, i.e. Iris Xe or Arc. Expect issues when attempting to use integrated graphics.
- Ensure the server's kernel version is new enough to use the device for hardware accceleration.
- Expect higher RAM usage when using OpenVINO compared to CPU processing.
## Setup

View File

@@ -44,7 +44,7 @@ export default function VersionSwitcher(): JSX.Element {
return (
versions.length > 0 && (
<DropdownNavbarItem
className="version-switcher-34ab39"
className="navbar__item"
label={label}
mobile={windowSize === 'mobile'}
items={versions.map(({ label, url }) => ({

View File

@@ -75,11 +75,6 @@ div[class^='announcementBar_'] {
font-weight: 500;
}
/* workaround for version switcher PR 15894 */
div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
display: none;
}
code {
font-weight: 600;
}

View File

@@ -1,8 +1,4 @@
[
{
"label": "v1.126.0",
"url": "https://v1.126.0.archive.immich.app"
},
{
"label": "v1.125.7",
"url": "https://v1.125.7.archive.immich.app"

View File

@@ -32,6 +32,9 @@ services:
- database
ports:
- 2285:2285
cap_drop:
# We need this to perform testing on permission errors
- DAC_OVERRIDE
redis:
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae

8
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.126.0",
"version": "1.125.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.126.0",
"version": "1.125.7",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -45,7 +45,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.49",
"version": "2.2.48",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -92,7 +92,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.126.0",
"version": "1.125.7",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.126.0",
"version": "1.125.7",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -1,5 +1,5 @@
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
import { chmodSync, cpSync, existsSync, promises, rmSync, unlinkSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
@@ -492,6 +492,34 @@ describe('/libraries', () => {
utils.removeImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
});
it('should handle permission errors on import paths without error', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/`],
});
const stat = await promises.stat(`${testAssetDir}/temp/directoryA`);
const mode = stat.mode;
chmodSync(`${testAssetDir}/temp/directoryB`, 0o000);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
chmodSync(`${testAssetDir}/temp/directoryB`, mode);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
expect(assets.items.find((asset) => asset.originalPath.includes('directoryA'))).toBeDefined();
expect(assets.items.find((asset) => asset.originalPath.includes('directoryB'))).not.toBeDefined();
});
it('should reimport a modified file', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,

View File

@@ -195,7 +195,6 @@ describe('/people', () => {
.send({
name: 'New Person',
birthDate: '1990-01-01',
color: '#333',
});
expect(status).toBe(201);
expect(body).toMatchObject({
@@ -274,24 +273,6 @@ describe('/people', () => {
expect(body).toMatchObject({ birthDate: null });
});
it('should set a color', async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ color: '#555' });
expect(status).toBe(200);
expect(body).toMatchObject({ color: '#555' });
});
it('should clear a color', async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ color: null });
expect(status).toBe(200);
expect(body.color).toBeUndefined();
});
it('should mark a person as favorite', async () => {
const person = await utils.createPerson(admin.accessToken, {
name: 'visible_person',

View File

@@ -150,30 +150,6 @@ describe('/shared-links', () => {
);
});
it('should filter on albumId', async () => {
const { status, body } = await request(app)
.get(`/shared-links?albumId=${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: linkWithAlbum.id }),
expect.objectContaining({ id: linkWithPassword.id }),
]),
);
});
it('should find 0 albums', async () => {
const { status, body } = await request(app)
.get(`/shared-links?albumId=${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(0);
});
it('should not get shared links created by other users', async () => {
const { status, body } = await request(app)
.get('/shared-links')

View File

@@ -436,7 +436,6 @@
"back_close_deselect": "Back, close, or deselect",
"backward": "Backward",
"birthdate_saved": "Date of birth saved successfully",
"show_shared_links": "Show shared links",
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
"blurred_background": "Blurred background",
"bugs_and_feature_requests": "Bugs & Feature Requests",
@@ -769,10 +768,8 @@
"go_to_search": "Go to search",
"go_to_folder": "Go to folder",
"group_albums_by": "Group albums by...",
"group_country": "Group by country",
"group_no": "No grouping",
"group_owner": "Group by owner",
"group_places_by": "Group places by...",
"group_year": "Group by year",
"has_quota": "Has quota",
"hi_user": "Hi {name} ({email})",
@@ -805,7 +802,6 @@
"include_shared_albums": "Include shared albums",
"include_shared_partner_assets": "Include shared partner assets",
"individual_share": "Individual share",
"individual_shares": "Individual shares",
"info": "Info",
"interval": {
"day_at_onepm": "Every day at 1pm",
@@ -991,7 +987,6 @@
"pick_a_location": "Pick a location",
"place": "Place",
"places": "Places",
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
"play": "Play",
"play_memories": "Play memories",
"play_motion_photo": "Play Motion Photo",
@@ -1174,7 +1169,6 @@
"shared_from_partner": "Photos from {partner}",
"shared_link_options": "Shared link options",
"shared_links": "Shared links",
"shared_links_description": "Share photos and videos with a link",
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
"shared_with_partner": "Shared with {partner}",
"sharing": "Sharing",
@@ -1284,7 +1278,6 @@
"unfavorite": "Unfavorite",
"unhide_person": "Unhide person",
"unknown": "Unknown",
"unknown_country": "Unknown Country",
"unknown_year": "Unknown Year",
"unlimited": "Unlimited",
"unlink_motion_video": "Unlink motion video",

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.126.0"
version = "1.125.7"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 183,
"android.injected.version.name" => "1.126.0",
"android.injected.version.code" => 182,
"android.injected.version.name" => "1.125.7",
}
)
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')

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
version_number: "1.126.0"
version_number: "1.125.7"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.126.0
- API version: 1.125.7
- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -408,8 +408,6 @@ Class | Method | HTTP request | Description
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
- [SharedLinkType](doc//SharedLinkType.md)
- [SharedLinksResponse](doc//SharedLinksResponse.md)
- [SharedLinksUpdate](doc//SharedLinksUpdate.md)
- [SignUpDto](doc//SignUpDto.md)
- [SmartSearchDto](doc//SmartSearchDto.md)
- [SourceType](doc//SourceType.md)

View File

@@ -221,8 +221,6 @@ part 'model/shared_link_create_dto.dart';
part 'model/shared_link_edit_dto.dart';
part 'model/shared_link_response_dto.dart';
part 'model/shared_link_type.dart';
part 'model/shared_links_response.dart';
part 'model/shared_links_update.dart';
part 'model/sign_up_dto.dart';
part 'model/smart_search_dto.dart';
part 'model/source_type.dart';

View File

@@ -127,10 +127,7 @@ class SharedLinksApi {
}
/// Performs an HTTP 'GET /shared-links' operation and returns the [Response].
/// Parameters:
///
/// * [String] albumId:
Future<Response> getAllSharedLinksWithHttpInfo({ String? albumId, }) async {
Future<Response> getAllSharedLinksWithHttpInfo() async {
// ignore: prefer_const_declarations
final path = r'/shared-links';
@@ -141,10 +138,6 @@ class SharedLinksApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (albumId != null) {
queryParams.addAll(_queryParams('', 'albumId', albumId));
}
const contentTypes = <String>[];
@@ -159,11 +152,8 @@ class SharedLinksApi {
);
}
/// Parameters:
///
/// * [String] albumId:
Future<List<SharedLinkResponseDto>?> getAllSharedLinks({ String? albumId, }) async {
final response = await getAllSharedLinksWithHttpInfo( albumId: albumId, );
Future<List<SharedLinkResponseDto>?> getAllSharedLinks() async {
final response = await getAllSharedLinksWithHttpInfo();
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View File

@@ -496,10 +496,6 @@ class ApiClient {
return SharedLinkResponseDto.fromJson(value);
case 'SharedLinkType':
return SharedLinkTypeTypeTransformer().decode(value);
case 'SharedLinksResponse':
return SharedLinksResponse.fromJson(value);
case 'SharedLinksUpdate':
return SharedLinksUpdate.fromJson(value);
case 'SignUpDto':
return SignUpDto.fromJson(value);
case 'SmartSearchDto':

View File

@@ -14,7 +14,6 @@ class PeopleUpdateItem {
/// Returns a new [PeopleUpdateItem] instance.
PeopleUpdateItem({
this.birthDate,
this.color,
this.featureFaceAssetId,
required this.id,
this.isFavorite,
@@ -25,8 +24,6 @@ class PeopleUpdateItem {
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate;
String? color;
/// Asset is used to get the feature face thumbnail.
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -68,7 +65,6 @@ class PeopleUpdateItem {
@override
bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateItem &&
other.birthDate == birthDate &&
other.color == color &&
other.featureFaceAssetId == featureFaceAssetId &&
other.id == id &&
other.isFavorite == isFavorite &&
@@ -79,7 +75,6 @@ class PeopleUpdateItem {
int get hashCode =>
// ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
(id.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
@@ -87,7 +82,7 @@ class PeopleUpdateItem {
(name == null ? 0 : name!.hashCode);
@override
String toString() => 'PeopleUpdateItem[birthDate=$birthDate, color=$color, featureFaceAssetId=$featureFaceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
String toString() => 'PeopleUpdateItem[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -96,11 +91,6 @@ class PeopleUpdateItem {
} else {
// json[r'birthDate'] = null;
}
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
}
if (this.featureFaceAssetId != null) {
json[r'featureFaceAssetId'] = this.featureFaceAssetId;
} else {
@@ -135,7 +125,6 @@ class PeopleUpdateItem {
return PeopleUpdateItem(
birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),

View File

@@ -14,7 +14,6 @@ class PersonCreateDto {
/// Returns a new [PersonCreateDto] instance.
PersonCreateDto({
this.birthDate,
this.color,
this.isFavorite,
this.isHidden,
this.name,
@@ -23,8 +22,6 @@ class PersonCreateDto {
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate;
String? color;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@@ -54,7 +51,6 @@ class PersonCreateDto {
@override
bool operator ==(Object other) => identical(this, other) || other is PersonCreateDto &&
other.birthDate == birthDate &&
other.color == color &&
other.isFavorite == isFavorite &&
other.isHidden == isHidden &&
other.name == name;
@@ -63,13 +59,12 @@ class PersonCreateDto {
int get hashCode =>
// ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isHidden == null ? 0 : isHidden!.hashCode) +
(name == null ? 0 : name!.hashCode);
@override
String toString() => 'PersonCreateDto[birthDate=$birthDate, color=$color, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
String toString() => 'PersonCreateDto[birthDate=$birthDate, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -78,11 +73,6 @@ class PersonCreateDto {
} else {
// json[r'birthDate'] = null;
}
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
}
if (this.isFavorite != null) {
json[r'isFavorite'] = this.isFavorite;
} else {
@@ -111,7 +101,6 @@ class PersonCreateDto {
return PersonCreateDto(
birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden'),
name: mapValueOfType<String>(json, r'name'),

View File

@@ -14,7 +14,6 @@ class PersonResponseDto {
/// Returns a new [PersonResponseDto] instance.
PersonResponseDto({
required this.birthDate,
this.color,
required this.id,
this.isFavorite,
required this.isHidden,
@@ -25,15 +24,6 @@ class PersonResponseDto {
DateTime? birthDate;
/// This property was added in v1.126.0
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? color;
String id;
/// This property was added in v1.126.0
@@ -63,7 +53,6 @@ class PersonResponseDto {
@override
bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto &&
other.birthDate == birthDate &&
other.color == color &&
other.id == id &&
other.isFavorite == isFavorite &&
other.isHidden == isHidden &&
@@ -75,7 +64,6 @@ class PersonResponseDto {
int get hashCode =>
// ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(id.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isHidden.hashCode) +
@@ -84,7 +72,7 @@ class PersonResponseDto {
(updatedAt == null ? 0 : updatedAt!.hashCode);
@override
String toString() => 'PersonResponseDto[birthDate=$birthDate, color=$color, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
String toString() => 'PersonResponseDto[birthDate=$birthDate, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -92,11 +80,6 @@ class PersonResponseDto {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
} else {
// json[r'birthDate'] = null;
}
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
}
json[r'id'] = this.id;
if (this.isFavorite != null) {
@@ -125,7 +108,6 @@ class PersonResponseDto {
return PersonResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden')!,

View File

@@ -14,7 +14,6 @@ class PersonUpdateDto {
/// Returns a new [PersonUpdateDto] instance.
PersonUpdateDto({
this.birthDate,
this.color,
this.featureFaceAssetId,
this.isFavorite,
this.isHidden,
@@ -24,8 +23,6 @@ class PersonUpdateDto {
/// Person date of birth. Note: the mobile app cannot currently set the birth date to null.
DateTime? birthDate;
String? color;
/// Asset is used to get the feature face thumbnail.
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -64,7 +61,6 @@ class PersonUpdateDto {
@override
bool operator ==(Object other) => identical(this, other) || other is PersonUpdateDto &&
other.birthDate == birthDate &&
other.color == color &&
other.featureFaceAssetId == featureFaceAssetId &&
other.isFavorite == isFavorite &&
other.isHidden == isHidden &&
@@ -74,14 +70,13 @@ class PersonUpdateDto {
int get hashCode =>
// ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isHidden == null ? 0 : isHidden!.hashCode) +
(name == null ? 0 : name!.hashCode);
@override
String toString() => 'PersonUpdateDto[birthDate=$birthDate, color=$color, featureFaceAssetId=$featureFaceAssetId, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
String toString() => 'PersonUpdateDto[birthDate=$birthDate, featureFaceAssetId=$featureFaceAssetId, isFavorite=$isFavorite, isHidden=$isHidden, name=$name]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -90,11 +85,6 @@ class PersonUpdateDto {
} else {
// json[r'birthDate'] = null;
}
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
}
if (this.featureFaceAssetId != null) {
json[r'featureFaceAssetId'] = this.featureFaceAssetId;
} else {
@@ -128,7 +118,6 @@ class PersonUpdateDto {
return PersonUpdateDto(
birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isHidden: mapValueOfType<bool>(json, r'isHidden'),

View File

@@ -14,7 +14,6 @@ class PersonWithFacesResponseDto {
/// Returns a new [PersonWithFacesResponseDto] instance.
PersonWithFacesResponseDto({
required this.birthDate,
this.color,
this.faces = const [],
required this.id,
this.isFavorite,
@@ -26,15 +25,6 @@ class PersonWithFacesResponseDto {
DateTime? birthDate;
/// This property was added in v1.126.0
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
String? color;
List<AssetFaceWithoutPersonResponseDto> faces;
String id;
@@ -66,7 +56,6 @@ class PersonWithFacesResponseDto {
@override
bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
other.birthDate == birthDate &&
other.color == color &&
_deepEquality.equals(other.faces, faces) &&
other.id == id &&
other.isFavorite == isFavorite &&
@@ -79,7 +68,6 @@ class PersonWithFacesResponseDto {
int get hashCode =>
// ignore: unnecessary_parenthesis
(birthDate == null ? 0 : birthDate!.hashCode) +
(color == null ? 0 : color!.hashCode) +
(faces.hashCode) +
(id.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
@@ -89,7 +77,7 @@ class PersonWithFacesResponseDto {
(updatedAt == null ? 0 : updatedAt!.hashCode);
@override
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, color=$color, faces=$faces, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isFavorite=$isFavorite, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -97,11 +85,6 @@ class PersonWithFacesResponseDto {
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
} else {
// json[r'birthDate'] = null;
}
if (this.color != null) {
json[r'color'] = this.color;
} else {
// json[r'color'] = null;
}
json[r'faces'] = this.faces;
json[r'id'] = this.id;
@@ -131,7 +114,6 @@ class PersonWithFacesResponseDto {
return PersonWithFacesResponseDto(
birthDate: mapDateTime(json, r'birthDate', r''),
color: mapValueOfType<String>(json, r'color'),
faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),

View File

@@ -1,107 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SharedLinksResponse {
/// Returns a new [SharedLinksResponse] instance.
SharedLinksResponse({
this.enabled = true,
this.sidebarWeb = false,
});
bool enabled;
bool sidebarWeb;
@override
bool operator ==(Object other) => identical(this, other) || other is SharedLinksResponse &&
other.enabled == enabled &&
other.sidebarWeb == sidebarWeb;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(enabled.hashCode) +
(sidebarWeb.hashCode);
@override
String toString() => 'SharedLinksResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'enabled'] = this.enabled;
json[r'sidebarWeb'] = this.sidebarWeb;
return json;
}
/// Returns a new [SharedLinksResponse] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SharedLinksResponse? fromJson(dynamic value) {
upgradeDto(value, "SharedLinksResponse");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SharedLinksResponse(
enabled: mapValueOfType<bool>(json, r'enabled')!,
sidebarWeb: mapValueOfType<bool>(json, r'sidebarWeb')!,
);
}
return null;
}
static List<SharedLinksResponse> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SharedLinksResponse>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SharedLinksResponse.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SharedLinksResponse> mapFromJson(dynamic json) {
final map = <String, SharedLinksResponse>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SharedLinksResponse.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SharedLinksResponse-objects as value to a dart map
static Map<String, List<SharedLinksResponse>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SharedLinksResponse>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SharedLinksResponse.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'enabled',
'sidebarWeb',
};
}

View File

@@ -1,125 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SharedLinksUpdate {
/// Returns a new [SharedLinksUpdate] instance.
SharedLinksUpdate({
this.enabled,
this.sidebarWeb,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? enabled;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? sidebarWeb;
@override
bool operator ==(Object other) => identical(this, other) || other is SharedLinksUpdate &&
other.enabled == enabled &&
other.sidebarWeb == sidebarWeb;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(enabled == null ? 0 : enabled!.hashCode) +
(sidebarWeb == null ? 0 : sidebarWeb!.hashCode);
@override
String toString() => 'SharedLinksUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.enabled != null) {
json[r'enabled'] = this.enabled;
} else {
// json[r'enabled'] = null;
}
if (this.sidebarWeb != null) {
json[r'sidebarWeb'] = this.sidebarWeb;
} else {
// json[r'sidebarWeb'] = null;
}
return json;
}
/// Returns a new [SharedLinksUpdate] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SharedLinksUpdate? fromJson(dynamic value) {
upgradeDto(value, "SharedLinksUpdate");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SharedLinksUpdate(
enabled: mapValueOfType<bool>(json, r'enabled'),
sidebarWeb: mapValueOfType<bool>(json, r'sidebarWeb'),
);
}
return null;
}
static List<SharedLinksUpdate> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SharedLinksUpdate>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SharedLinksUpdate.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SharedLinksUpdate> mapFromJson(dynamic json) {
final map = <String, SharedLinksUpdate>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SharedLinksUpdate.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SharedLinksUpdate-objects as value to a dart map
static Map<String, List<SharedLinksUpdate>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SharedLinksUpdate>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SharedLinksUpdate.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -21,7 +21,6 @@ class UserPreferencesResponseDto {
required this.people,
required this.purchase,
required this.ratings,
required this.sharedLinks,
required this.tags,
});
@@ -41,8 +40,6 @@ class UserPreferencesResponseDto {
RatingsResponse ratings;
SharedLinksResponse sharedLinks;
TagsResponse tags;
@override
@@ -55,7 +52,6 @@ class UserPreferencesResponseDto {
other.people == people &&
other.purchase == purchase &&
other.ratings == ratings &&
other.sharedLinks == sharedLinks &&
other.tags == tags;
@override
@@ -69,11 +65,10 @@ class UserPreferencesResponseDto {
(people.hashCode) +
(purchase.hashCode) +
(ratings.hashCode) +
(sharedLinks.hashCode) +
(tags.hashCode);
@override
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -85,7 +80,6 @@ class UserPreferencesResponseDto {
json[r'people'] = this.people;
json[r'purchase'] = this.purchase;
json[r'ratings'] = this.ratings;
json[r'sharedLinks'] = this.sharedLinks;
json[r'tags'] = this.tags;
return json;
}
@@ -107,7 +101,6 @@ class UserPreferencesResponseDto {
people: PeopleResponse.fromJson(json[r'people'])!,
purchase: PurchaseResponse.fromJson(json[r'purchase'])!,
ratings: RatingsResponse.fromJson(json[r'ratings'])!,
sharedLinks: SharedLinksResponse.fromJson(json[r'sharedLinks'])!,
tags: TagsResponse.fromJson(json[r'tags'])!,
);
}
@@ -164,7 +157,6 @@ class UserPreferencesResponseDto {
'people',
'purchase',
'ratings',
'sharedLinks',
'tags',
};
}

View File

@@ -21,7 +21,6 @@ class UserPreferencesUpdateDto {
this.people,
this.purchase,
this.ratings,
this.sharedLinks,
this.tags,
});
@@ -89,14 +88,6 @@ class UserPreferencesUpdateDto {
///
RatingsUpdate? ratings;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
SharedLinksUpdate? sharedLinks;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@@ -115,7 +106,6 @@ class UserPreferencesUpdateDto {
other.people == people &&
other.purchase == purchase &&
other.ratings == ratings &&
other.sharedLinks == sharedLinks &&
other.tags == tags;
@override
@@ -129,11 +119,10 @@ class UserPreferencesUpdateDto {
(people == null ? 0 : people!.hashCode) +
(purchase == null ? 0 : purchase!.hashCode) +
(ratings == null ? 0 : ratings!.hashCode) +
(sharedLinks == null ? 0 : sharedLinks!.hashCode) +
(tags == null ? 0 : tags!.hashCode);
@override
String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]';
String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -177,11 +166,6 @@ class UserPreferencesUpdateDto {
} else {
// json[r'ratings'] = null;
}
if (this.sharedLinks != null) {
json[r'sharedLinks'] = this.sharedLinks;
} else {
// json[r'sharedLinks'] = null;
}
if (this.tags != null) {
json[r'tags'] = this.tags;
} else {
@@ -207,7 +191,6 @@ class UserPreferencesUpdateDto {
people: PeopleUpdate.fromJson(json[r'people']),
purchase: PurchaseUpdate.fromJson(json[r'purchase']),
ratings: RatingsUpdate.fromJson(json[r'ratings']),
sharedLinks: SharedLinksUpdate.fromJson(json[r'sharedLinks']),
tags: TagsUpdate.fromJson(json[r'tags']),
);
}

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.126.0+183
version: 1.125.7+182
environment:
sdk: '>=3.3.0 <4.0.0'

View File

@@ -5230,17 +5230,7 @@
"/shared-links": {
"get": {
"operationId": "getAllSharedLinks",
"parameters": [
{
"name": "albumId",
"required": false,
"in": "query",
"schema": {
"format": "uuid",
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"content": {
@@ -7468,7 +7458,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.126.0",
"version": "1.125.7",
"contact": {}
},
"tags": [],
@@ -10296,10 +10286,6 @@
"nullable": true,
"type": "string"
},
"color": {
"nullable": true,
"type": "string"
},
"featureFaceAssetId": {
"description": "Asset is used to get the feature face thumbnail.",
"type": "string"
@@ -10416,10 +10402,6 @@
"nullable": true,
"type": "string"
},
"color": {
"nullable": true,
"type": "string"
},
"isFavorite": {
"type": "boolean"
},
@@ -10441,10 +10423,6 @@
"nullable": true,
"type": "string"
},
"color": {
"description": "This property was added in v1.126.0",
"type": "string"
},
"id": {
"type": "string"
},
@@ -10495,10 +10473,6 @@
"nullable": true,
"type": "string"
},
"color": {
"nullable": true,
"type": "string"
},
"featureFaceAssetId": {
"description": "Asset is used to get the feature face thumbnail.",
"type": "string"
@@ -10524,10 +10498,6 @@
"nullable": true,
"type": "string"
},
"color": {
"description": "This property was added in v1.126.0",
"type": "string"
},
"faces": {
"items": {
"$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto"
@@ -11524,34 +11494,6 @@
],
"type": "string"
},
"SharedLinksResponse": {
"properties": {
"enabled": {
"default": true,
"type": "boolean"
},
"sidebarWeb": {
"default": false,
"type": "boolean"
}
},
"required": [
"enabled",
"sidebarWeb"
],
"type": "object"
},
"SharedLinksUpdate": {
"properties": {
"enabled": {
"type": "boolean"
},
"sidebarWeb": {
"type": "boolean"
}
},
"type": "object"
},
"SignUpDto": {
"properties": {
"email": {
@@ -12669,6 +12611,7 @@
"properties": {
"color": {
"nullable": true,
"pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
"type": "string"
}
},
@@ -13198,9 +13141,6 @@
"ratings": {
"$ref": "#/components/schemas/RatingsResponse"
},
"sharedLinks": {
"$ref": "#/components/schemas/SharedLinksResponse"
},
"tags": {
"$ref": "#/components/schemas/TagsResponse"
}
@@ -13214,7 +13154,6 @@
"people",
"purchase",
"ratings",
"sharedLinks",
"tags"
],
"type": "object"
@@ -13245,9 +13184,6 @@
"ratings": {
"$ref": "#/components/schemas/RatingsUpdate"
},
"sharedLinks": {
"$ref": "#/components/schemas/SharedLinksUpdate"
},
"tags": {
"$ref": "#/components/schemas/TagsUpdate"
}

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.126.0",
"version": "1.125.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.126.0",
"version": "1.125.7",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.126.0",
"version": "1.125.7",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.126.0
* 1.125.7
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
@@ -113,10 +113,6 @@ export type PurchaseResponse = {
export type RatingsResponse = {
enabled: boolean;
};
export type SharedLinksResponse = {
enabled: boolean;
sidebarWeb: boolean;
};
export type TagsResponse = {
enabled: boolean;
sidebarWeb: boolean;
@@ -130,7 +126,6 @@ export type UserPreferencesResponseDto = {
people: PeopleResponse;
purchase: PurchaseResponse;
ratings: RatingsResponse;
sharedLinks: SharedLinksResponse;
tags: TagsResponse;
};
export type AvatarUpdate = {
@@ -163,10 +158,6 @@ export type PurchaseUpdate = {
export type RatingsUpdate = {
enabled?: boolean;
};
export type SharedLinksUpdate = {
enabled?: boolean;
sidebarWeb?: boolean;
};
export type TagsUpdate = {
enabled?: boolean;
sidebarWeb?: boolean;
@@ -180,7 +171,6 @@ export type UserPreferencesUpdateDto = {
people?: PeopleUpdate;
purchase?: PurchaseUpdate;
ratings?: RatingsUpdate;
sharedLinks?: SharedLinksUpdate;
tags?: TagsUpdate;
};
export type AlbumUserResponseDto = {
@@ -223,8 +213,6 @@ export type AssetFaceWithoutPersonResponseDto = {
};
export type PersonWithFacesResponseDto = {
birthDate: string | null;
/** This property was added in v1.126.0 */
color?: string;
faces: AssetFaceWithoutPersonResponseDto[];
id: string;
/** This property was added in v1.126.0 */
@@ -505,8 +493,6 @@ export type DuplicateResponseDto = {
};
export type PersonResponseDto = {
birthDate: string | null;
/** This property was added in v1.126.0 */
color?: string;
id: string;
/** This property was added in v1.126.0 */
isFavorite?: boolean;
@@ -707,7 +693,6 @@ export type PersonCreateDto = {
/** Person date of birth.
Note: the mobile app cannot currently set the birth date to null. */
birthDate?: string | null;
color?: string | null;
isFavorite?: boolean;
/** Person visibility */
isHidden?: boolean;
@@ -718,7 +703,6 @@ export type PeopleUpdateItem = {
/** Person date of birth.
Note: the mobile app cannot currently set the birth date to null. */
birthDate?: string | null;
color?: string | null;
/** Asset is used to get the feature face thumbnail. */
featureFaceAssetId?: string;
/** Person id. */
@@ -736,7 +720,6 @@ export type PersonUpdateDto = {
/** Person date of birth.
Note: the mobile app cannot currently set the birth date to null. */
birthDate?: string | null;
color?: string | null;
/** Asset is used to get the feature face thumbnail. */
featureFaceAssetId?: string;
isFavorite?: boolean;
@@ -2762,15 +2745,11 @@ export function deleteSession({ id }: {
method: "DELETE"
}));
}
export function getAllSharedLinks({ albumId }: {
albumId?: string;
}, opts?: Oazapfts.RequestOpts) {
export function getAllSharedLinks(opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: SharedLinkResponseDto[];
}>(`/shared-links${QS.query(QS.explode({
albumId
}))}`, {
}>("/shared-links", {
...opts
}));
}

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20250204@sha256:8b203f19f4d5cf4619b60ee5f50d6d4b5ea3745747f5e5170d1b7404ebeb0792 AS dev
FROM ghcr.io/immich-app/base-server-dev:20250123@sha256:04eba5cd87d61bc3d20a3915b2302f04d08fbc329c55ee0cde103c502f59f412 AS dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@@ -42,7 +42,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20250204@sha256:2af3da713d5ab3ccca23b216b747557ea6016117e72deac101e35069ccaf9b5e
FROM ghcr.io/immich-app/base-server-prod:20250123@sha256:591739983913f82672d8191258f3a1a24c123db0d619ff91fca8fef431ee1338
WORKDIR /usr/src/app
ENV NODE_ENV=production \

2042
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.126.0",
"version": "1.125.7",
"description": "",
"author": "",
"private": true,
@@ -35,16 +35,16 @@
"email:dev": "email dev -p 3050 --dir src/emails"
},
"dependencies": {
"@nestjs/bullmq": "^11.0.1",
"@nestjs/common": "^11.0.4",
"@nestjs/core": "^11.0.4",
"@nestjs/bullmq": "^11.0.0",
"@nestjs/common": "^10.2.2",
"@nestjs/core": "^10.2.2",
"@nestjs/event-emitter": "^3.0.0",
"@nestjs/platform-express": "^11.0.4",
"@nestjs/platform-socket.io": "^11.0.4",
"@nestjs/platform-express": "^10.2.2",
"@nestjs/platform-socket.io": "^10.2.2",
"@nestjs/schedule": "^5.0.0",
"@nestjs/swagger": "^11.0.2",
"@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.0.4",
"@nestjs/swagger": "^8.0.0",
"@nestjs/typeorm": "^10.0.0",
"@nestjs/websockets": "^10.2.2",
"@opentelemetry/auto-instrumentations-node": "^0.55.0",
"@opentelemetry/context-async-hooks": "^1.24.0",
"@opentelemetry/exporter-prometheus": "^0.57.0",
@@ -60,7 +60,7 @@
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"exiftool-vendored": "^28.3.1",
"fast-glob": "^3.3.2",
"fast-glob": "github:etnoy/fast-glob#built",
"fluent-ffmpeg": "^2.1.2",
"geo-tz": "^8.0.0",
"handlebars": "^4.7.8",
@@ -72,9 +72,9 @@
"kysely-postgres-js": "^2.0.0",
"lodash": "^4.17.21",
"luxon": "^3.4.2",
"nest-commander": "^3.16.0",
"nestjs-cls": "^5.0.0",
"nestjs-kysely": "^1.1.0",
"nest-commander": "^3.11.1",
"nestjs-cls": "^4.3.0",
"nestjs-kysely": "^1.0.0",
"nestjs-otel": "^6.0.0",
"nodemailer": "^6.9.13",
"openid-client": "^5.4.3",
@@ -97,9 +97,9 @@
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0",
"@nestjs/cli": "^11.0.2",
"@nestjs/schematics": "^11.0.0",
"@nestjs/testing": "^11.0.4",
"@nestjs/cli": "^10.1.16",
"@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^10.2.2",
"@swc/core": "^1.4.14",
"@testcontainers/postgresql": "^10.2.1",
"@types/archiver": "^6.0.0",

View File

@@ -9,7 +9,6 @@ import {
SharedLinkEditDto,
SharedLinkPasswordDto,
SharedLinkResponseDto,
SharedLinkSearchDto,
} from 'src/dtos/shared-link.dto';
import { ImmichCookie, Permission } from 'src/enum';
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
@@ -25,8 +24,8 @@ export class SharedLinkController {
@Get()
@Authenticated({ permission: Permission.SHARED_LINK_READ })
getAllSharedLinks(@Auth() auth: AuthDto, @Query() dto: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
return this.service.getAll(auth, dto);
getAllSharedLinks(@Auth() auth: AuthDto): Promise<SharedLinkResponseDto[]> {
return this.service.getAll(auth);
}
@Get('me')

View File

@@ -9,7 +9,8 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { IConfigRepository, ILoggingRepository, ISystemMetadataRepository } from 'src/types';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IConfigRepository, ILoggingRepository } from 'src/types';
import { getAssetFiles } from 'src/utils/asset.util';
import { getConfig } from 'src/utils/config';

1
server/src/db.d.ts vendored
View File

@@ -276,7 +276,6 @@ export interface Partners {
export interface Person {
birthDate: Timestamp | null;
color: string | null;
createdAt: Generated<Timestamp>;
faceAssetId: string | null;
id: Generated<string>;

View File

@@ -7,14 +7,7 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { SourceType } from 'src/enum';
import {
IsDateStringFormat,
MaxDateString,
Optional,
ValidateBoolean,
ValidateHexColor,
ValidateUUID,
} from 'src/validation';
import { IsDateStringFormat, MaxDateString, Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
export class PersonCreateDto {
/**
@@ -42,10 +35,6 @@ export class PersonCreateDto {
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@Optional({ emptyToNull: true, nullable: true })
@ValidateHexColor()
color?: string | null;
}
export class PersonUpdateDto extends PersonCreateDto {
@@ -113,8 +102,6 @@ export class PersonResponseDto {
updatedAt?: Date;
@PropertyLifecycle({ addedAt: 'v1.126.0' })
isFavorite?: boolean;
@PropertyLifecycle({ addedAt: 'v1.126.0' })
color?: string;
}
export class PersonWithFacesResponseDto extends PersonResponseDto {
@@ -189,7 +176,6 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
thumbnailPath: person.thumbnailPath,
isHidden: person.isHidden,
isFavorite: person.isFavorite,
color: person.color ?? undefined,
updatedAt: person.updatedAt,
};
}

View File

@@ -1,4 +1,4 @@
import { SessionItem } from 'src/types';
import { SessionEntity } from 'src/entities/session.entity';
export class SessionResponseDto {
id!: string;
@@ -9,7 +9,7 @@ export class SessionResponseDto {
deviceOS!: string;
}
export const mapSession = (entity: SessionItem, currentId?: string): SessionResponseDto => ({
export const mapSession = (entity: SessionEntity, currentId?: string): SessionResponseDto => ({
id: entity.id,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),

View File

@@ -7,11 +7,6 @@ import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { SharedLinkType } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
export class SharedLinkSearchDto {
@ValidateUUID({ optional: true })
albumId?: string;
}
export class SharedLinkCreateDto {
@IsEnum(SharedLinkType)
@ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' })

View File

@@ -1,7 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
import { TagEntity } from 'src/entities/tag.entity';
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
import { Optional, ValidateUUID } from 'src/validation';
export class TagCreateDto {
@IsString()
@@ -17,8 +18,9 @@ export class TagCreateDto {
}
export class TagUpdateDto {
@Optional({ emptyToNull: true, nullable: true })
@ValidateHexColor()
@Optional({ nullable: true, emptyToNull: true })
@IsHexColor()
@Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value))
color?: string | null;
}

View File

@@ -38,14 +38,6 @@ class PeopleUpdate {
sidebarWeb?: boolean;
}
class SharedLinksUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
@ValidateBoolean({ optional: true })
sidebarWeb?: boolean;
}
class TagsUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
@@ -106,11 +98,6 @@ export class UserPreferencesUpdateDto {
@Type(() => RatingsUpdate)
ratings?: RatingsUpdate;
@Optional()
@ValidateNested()
@Type(() => SharedLinksUpdate)
sharedLinks?: SharedLinksUpdate;
@Optional()
@ValidateNested()
@Type(() => TagsUpdate)
@@ -165,11 +152,6 @@ class TagsResponse {
sidebarWeb: boolean = true;
}
class SharedLinksResponse {
enabled: boolean = true;
sidebarWeb: boolean = false;
}
class EmailNotificationsResponse {
enabled!: boolean;
albumInvite!: boolean;
@@ -193,7 +175,6 @@ export class UserPreferencesResponseDto implements UserPreferences {
memories!: MemoriesResponse;
people!: PeopleResponse;
ratings!: RatingsResponse;
sharedLinks!: SharedLinksResponse;
tags!: TagsResponse;
avatar!: AvatarResponse;
emailNotifications!: EmailNotificationsResponse;

View File

@@ -52,7 +52,4 @@ export class PersonEntity {
@Column({ default: false })
isFavorite!: boolean;
@Column({ type: 'varchar', nullable: true, default: null })
color?: string | null;
}

View File

@@ -34,10 +34,6 @@ export interface UserPreferences {
ratings: {
enabled: boolean;
};
sharedLinks: {
enabled: boolean;
sidebarWeb: boolean;
};
tags: {
enabled: boolean;
sidebarWeb: boolean;
@@ -78,10 +74,6 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences
enabled: true,
sidebarWeb: false,
},
sharedLinks: {
enabled: true,
sidebarWeb: false,
},
ratings: {
enabled: false,
},

View File

@@ -0,0 +1,25 @@
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'node:child_process';
import { Readable } from 'node:stream';
export interface ImmichReadStream {
stream: Readable;
type?: string;
length?: number;
}
export interface ImmichZipStream extends ImmichReadStream {
addFile: (inputPath: string, filename: string) => void;
finalize: () => Promise<void>;
}
export interface DiskUsage {
available: number;
free: number;
total: number;
}
export const IProcessRepository = 'IProcessRepository';
export interface IProcessRepository {
spawn(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams;
}

View File

@@ -0,0 +1,17 @@
import { Insertable, Updateable } from 'kysely';
import { Sessions } from 'src/db';
import { SessionEntity } from 'src/entities/session.entity';
export const ISessionRepository = 'ISessionRepository';
type E = SessionEntity;
export type SessionSearchOptions = { updatedBefore: Date };
export interface ISessionRepository {
search(options: SessionSearchOptions): Promise<SessionEntity[]>;
create(dto: Insertable<Sessions>): Promise<SessionEntity>;
update(id: string, dto: Updateable<Sessions>): Promise<SessionEntity>;
delete(id: string): Promise<void>;
getByToken(token: string): Promise<E | undefined>;
getByUserId(userId: string): Promise<E[]>;
}

View File

@@ -4,13 +4,8 @@ import { SharedLinkEntity } from 'src/entities/shared-link.entity';
export const ISharedLinkRepository = 'ISharedLinkRepository';
export type SharedLinkSearchOptions = {
userId: string;
albumId?: string;
};
export interface ISharedLinkRepository {
getAll(options: SharedLinkSearchOptions): Promise<SharedLinkEntity[]>;
getAll(userId: string): Promise<SharedLinkEntity[]>;
get(userId: string, id: string): Promise<SharedLinkEntity | undefined>;
getByKey(key: Buffer): Promise<SharedLinkEntity | undefined>;
create(entity: Insertable<SharedLinks> & { assetIds?: string[] }): Promise<SharedLinkEntity>;

View File

@@ -0,0 +1,10 @@
import { SystemMetadata } from 'src/entities/system-metadata.entity';
export const ISystemMetadataRepository = 'ISystemMetadataRepository';
export interface ISystemMetadataRepository {
get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>;
set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void>;
delete<T extends keyof SystemMetadata>(key: T): Promise<void>;
readFile(filename: string): Promise<string>;
}

View File

@@ -1,14 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddPersonColor1738889177573 implements MigrationInterface {
name = 'AddPersonColor1738889177573'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "person" ADD "color" character varying`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "color"`);
}
}

View File

@@ -9,10 +9,13 @@ import { IMachineLearningRepository } from 'src/interfaces/machine-learning.inte
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IProcessRepository } from 'src/interfaces/process.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AccessRepository } from 'src/repositories/access.repository';
@@ -70,10 +73,7 @@ export const repositories = [
MetadataRepository,
NotificationRepository,
OAuthRepository,
ProcessRepository,
SessionRepository,
ServerInfoRepository,
SystemMetadataRepository,
TelemetryRepository,
TrashRepository,
ViewRepository,
@@ -92,10 +92,13 @@ export const providers = [
{ provide: IMoveRepository, useClass: MoveRepository },
{ provide: IPartnerRepository, useClass: PartnerRepository },
{ provide: IPersonRepository, useClass: PersonRepository },
{ provide: IProcessRepository, useClass: ProcessRepository },
{ provide: ISearchRepository, useClass: SearchRepository },
{ provide: ISessionRepository, useClass: SessionRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: IStackRepository, useClass: StackRepository },
{ provide: IStorageRepository, useClass: StorageRepository },
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
{ provide: ITagRepository, useClass: TagRepository },
{ provide: IUserRepository, useClass: UserRepository },
];

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { getName } from 'i18n-iso-countries';
import { Expression, Kysely, sql, SqlBool } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
@@ -11,9 +11,9 @@ import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
import { LogLevel, SystemMetadataKey } from 'src/enum';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
export interface MapMarkerSearchOptions {
isArchived?: boolean;
@@ -48,7 +48,7 @@ interface MapDB extends DB {
export class MapRepository {
constructor(
private configRepository: ConfigRepository,
private metadataRepository: SystemMetadataRepository,
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,
private logger: LoggingRepository,
@InjectKysely() private db: Kysely<MapDB>,
) {

View File

@@ -43,12 +43,7 @@ export class OAuthRepository {
const params = client.callbackParams(url);
try {
const tokens = await client.callback(redirectUrl, params, { state: params.state });
const profile = await client.userinfo<OAuthProfile>(tokens.access_token || '');
if (!profile.sub) {
throw new Error('Unexpected profile response, no `sub`');
}
return profile;
return await client.userinfo<OAuthProfile>(tokens.access_token || '');
} catch (error: Error | any) {
if (error.message.includes('unexpected JWT alg received')) {
this.logger.warn(

View File

@@ -1,11 +1,13 @@
import { Injectable } from '@nestjs/common';
import { ChildProcessWithoutNullStreams, spawn, SpawnOptionsWithoutStdio } from 'node:child_process';
import { IProcessRepository } from 'src/interfaces/process.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { StorageRepository } from 'src/repositories/storage.repository';
@Injectable()
export class ProcessRepository {
export class ProcessRepository implements IProcessRepository {
constructor(private logger: LoggingRepository) {
this.logger.setContext(ProcessRepository.name);
this.logger.setContext(StorageRepository.name);
}
spawn(command: string, args: readonly string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams {

View File

@@ -3,37 +3,36 @@ import { Insertable, Kysely, Updateable } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB, Sessions } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { withUser } from 'src/entities/session.entity';
import { SessionEntity, withUser } from 'src/entities/session.entity';
import { ISessionRepository, SessionSearchOptions } from 'src/interfaces/session.interface';
import { asUuid } from 'src/utils/database';
export type SessionSearchOptions = { updatedBefore: Date };
@Injectable()
export class SessionRepository {
export class SessionRepository implements ISessionRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [{ updatedBefore: DummyValue.DATE }] })
search(options: SessionSearchOptions) {
search(options: SessionSearchOptions): Promise<SessionEntity[]> {
return this.db
.selectFrom('sessions')
.selectAll()
.where('sessions.updatedAt', '<=', options.updatedBefore)
.execute();
.execute() as Promise<SessionEntity[]>;
}
@GenerateSql({ params: [DummyValue.STRING] })
getByToken(token: string) {
getByToken(token: string): Promise<SessionEntity | undefined> {
return this.db
.selectFrom('sessions')
.innerJoinLateral(withUser, (join) => join.onTrue())
.selectAll('sessions')
.select((eb) => eb.fn.toJson('user').as('user'))
.where('sessions.token', '=', token)
.executeTakeFirst();
.executeTakeFirst() as Promise<SessionEntity | undefined>;
}
@GenerateSql({ params: [DummyValue.UUID] })
getByUserId(userId: string) {
getByUserId(userId: string): Promise<SessionEntity[]> {
return this.db
.selectFrom('sessions')
.innerJoinLateral(withUser, (join) => join.onTrue())
@@ -42,24 +41,30 @@ export class SessionRepository {
.where('sessions.userId', '=', userId)
.orderBy('sessions.updatedAt', 'desc')
.orderBy('sessions.createdAt', 'desc')
.execute();
.execute() as unknown as Promise<SessionEntity[]>;
}
create(dto: Insertable<Sessions>) {
return this.db.insertInto('sessions').values(dto).returningAll().executeTakeFirstOrThrow();
async create(dto: Insertable<Sessions>): Promise<SessionEntity> {
const { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } = await this.db
.insertInto('sessions')
.values(dto)
.returningAll()
.executeTakeFirstOrThrow();
return { id, token, userId, createdAt, updatedAt, deviceType, deviceOS } as SessionEntity;
}
update(id: string, dto: Updateable<Sessions>) {
update(id: string, dto: Updateable<Sessions>): Promise<SessionEntity> {
return this.db
.updateTable('sessions')
.set(dto)
.where('sessions.id', '=', asUuid(id))
.returningAll()
.executeTakeFirstOrThrow();
.executeTakeFirstOrThrow() as Promise<SessionEntity>;
}
@GenerateSql({ params: [DummyValue.UUID] })
async delete(id: string) {
async delete(id: string): Promise<void> {
await this.db.deleteFrom('sessions').where('id', '=', asUuid(id)).execute();
}
}

View File

@@ -7,7 +7,7 @@ import { DB, SharedLinks } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { SharedLinkType } from 'src/enum';
import { ISharedLinkRepository, SharedLinkSearchOptions } from 'src/interfaces/shared-link.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
@Injectable()
export class SharedLinkRepository implements ISharedLinkRepository {
@@ -93,7 +93,7 @@ export class SharedLinkRepository implements ISharedLinkRepository {
}
@GenerateSql({ params: [DummyValue.UUID] })
getAll({ userId, albumId }: SharedLinkSearchOptions): Promise<SharedLinkEntity[]> {
getAll(userId: string): Promise<SharedLinkEntity[]> {
return this.db
.selectFrom('shared_links')
.selectAll('shared_links')
@@ -149,7 +149,6 @@ export class SharedLinkRepository implements ISharedLinkRepository {
)
.select((eb) => eb.fn.toJson('album').as('album'))
.where((eb) => eb.or([eb('shared_links.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)]))
.$if(!!albumId, (eb) => eb.where('shared_links.albumId', '=', albumId!))
.orderBy('shared_links.createdAt', 'desc')
.distinctOn(['shared_links.createdAt'])
.execute() as unknown as Promise<SharedLinkEntity[]>;

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import archiver from 'archiver';
import chokidar, { WatchOptions } from 'chokidar';
import { escapePath, glob, globStream } from 'fast-glob';
import { ErrnoException } from 'fast-glob/out/types';
import { constants, createReadStream, createWriteStream, existsSync, mkdirSync } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
@@ -170,6 +171,19 @@ export class StorageRepository implements IStorageRepository {
});
}
private errorHandler = (error: ErrnoException) => {
if (error.code === 'ENOENT') {
this.logger.warn(`Path ${error.path} does not exist, ignoring.`);
return true;
} else if (error.code === 'EACCES') {
this.logger.warn(`Permission denied for path ${error.path}, ignoring.`);
return true;
}
this.logger.error(`Error while walking path ${error.path}: ${error.message}`);
return false;
};
async *walk(walkOptions: WalkOptionsDto): AsyncGenerator<string[]> {
const { pathsToCrawl, exclusionPatterns, includeHidden } = walkOptions;
if (pathsToCrawl.length === 0) {
@@ -185,6 +199,7 @@ export class StorageRepository implements IStorageRepository {
onlyFiles: true,
dot: includeHidden,
ignore: exclusionPatterns,
errorHandler: this.errorHandler,
});
let batch: string[] = [];

View File

@@ -5,11 +5,12 @@ import { readFile } from 'node:fs/promises';
import { DB, SystemMetadata as DbSystemMetadata } from 'src/db';
import { GenerateSql } from 'src/decorators';
import { SystemMetadata } from 'src/entities/system-metadata.entity';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
type Upsert = Insertable<DbSystemMetadata>;
@Injectable()
export class SystemMetadataRepository {
export class SystemMetadataRepository implements ISystemMetadataRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: ['metadata_key'] })

View File

@@ -9,9 +9,9 @@ import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AssetService } from 'src/services/asset.service';
import { ISystemMetadataRepository } from 'src/types';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { faceStub } from 'test/fixtures/face.stub';

View File

@@ -5,10 +5,12 @@ import { UserEntity } from 'src/entities/user.entity';
import { AuthType, Permission } from 'src/enum';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AuthService } from 'src/services/auth.service';
import { IApiKeyRepository, IOAuthRepository, ISessionRepository, ISystemMetadataRepository } from 'src/types';
import { IApiKeyRepository, IOAuthRepository } from 'src/types';
import { keyStub } from 'test/fixtures/api-key.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub';
@@ -256,7 +258,7 @@ describe('AuthService', () => {
it('should validate using authorization header', async () => {
userMock.get.mockResolvedValue(userStub.user1);
sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
await expect(
sut.authenticate({
headers: { authorization: 'Bearer auth_token' },
@@ -361,7 +363,7 @@ describe('AuthService', () => {
});
it('should return an auth dto', async () => {
sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
await expect(
sut.authenticate({
headers: { cookie: 'immich_access_token=auth_token' },
@@ -375,7 +377,7 @@ describe('AuthService', () => {
});
it('should throw if admin route and not an admin', async () => {
sessionMock.getByToken.mockResolvedValue(sessionStub.valid as any);
sessionMock.getByToken.mockResolvedValue(sessionStub.valid);
await expect(
sut.authenticate({
headers: { cookie: 'immich_access_token=auth_token' },
@@ -386,7 +388,7 @@ describe('AuthService', () => {
});
it('should update when access time exceeds an hour', async () => {
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive as any);
sessionMock.getByToken.mockResolvedValue(sessionStub.inactive);
sessionMock.update.mockResolvedValue(sessionStub.valid);
await expect(
sut.authenticate({

View File

@@ -17,7 +17,6 @@ import {
mapLoginResponse,
} from 'src/dtos/auth.dto';
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
import { SessionEntity } from 'src/entities/session.entity';
import { UserEntity } from 'src/entities/user.entity';
import { AuthType, ImmichCookie, ImmichHeader, ImmichQuery, Permission } from 'src/enum';
import { OAuthProfile } from 'src/repositories/oauth.repository';
@@ -339,7 +338,7 @@ export class AuthService extends BaseService {
await this.sessionRepository.update(session.id, { id: session.id, updatedAt: new Date() });
}
return { user: session.user as unknown as UserEntity, session: session as unknown as SessionEntity };
return { user: session.user, session };
}
throw new UnauthorizedException('Invalid user token');

View File

@@ -4,9 +4,11 @@ import { StorageCore } from 'src/cores/storage.core';
import { ImmichWorker, StorageFolder } from 'src/enum';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { JobStatus } from 'src/interfaces/job.interface';
import { IProcessRepository } from 'src/interfaces/process.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { BackupService } from 'src/services/backup.service';
import { IConfigRepository, ICronRepository, IProcessRepository, ISystemMetadataRepository } from 'src/types';
import { IConfigRepository, ICronRepository } from 'src/types';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { mockSpawn, newTestService } from 'test/utils';
import { describe, Mocked } from 'vitest';

View File

@@ -17,10 +17,13 @@ import { IMachineLearningRepository } from 'src/interfaces/machine-learning.inte
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IProcessRepository } from 'src/interfaces/process.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { AccessRepository } from 'src/repositories/access.repository';
@@ -37,10 +40,7 @@ import { MemoryRepository } from 'src/repositories/memory.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { NotificationRepository } from 'src/repositories/notification.repository';
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ProcessRepository } from 'src/repositories/process.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
@@ -77,14 +77,14 @@ export class BaseService {
protected oauthRepository: OAuthRepository,
@Inject(IPartnerRepository) protected partnerRepository: IPartnerRepository,
@Inject(IPersonRepository) protected personRepository: IPersonRepository,
protected processRepository: ProcessRepository,
@Inject(IProcessRepository) protected processRepository: IProcessRepository,
@Inject(ISearchRepository) protected searchRepository: ISearchRepository,
protected serverInfoRepository: ServerInfoRepository,
protected sessionRepository: SessionRepository,
@Inject(ISessionRepository) protected sessionRepository: ISessionRepository,
@Inject(ISharedLinkRepository) protected sharedLinkRepository: ISharedLinkRepository,
@Inject(IStackRepository) protected stackRepository: IStackRepository,
@Inject(IStorageRepository) protected storageRepository: IStorageRepository,
protected systemMetadataRepository: SystemMetadataRepository,
@Inject(ISystemMetadataRepository) protected systemMetadataRepository: ISystemMetadataRepository,
@Inject(ITagRepository) protected tagRepository: ITagRepository,
protected telemetryRepository: TelemetryRepository,
protected trashRepository: TrashRepository,

View File

@@ -1,6 +1,6 @@
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { CliService } from 'src/services/cli.service';
import { ISystemMetadataRepository } from 'src/types';
import { userStub } from 'test/fixtures/user.stub';
import { newTestService } from 'test/utils';
import { Mocked, describe, it } from 'vitest';

View File

@@ -1,9 +1,10 @@
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { DuplicateService } from 'src/services/duplicate.service';
import { SearchService } from 'src/services/search.service';
import { ILoggingRepository, ISystemMetadataRepository } from 'src/types';
import { ILoggingRepository } from 'src/types';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { newTestService } from 'test/utils';

View File

@@ -18,8 +18,9 @@ import { IJobRepository, JobCounts, JobName, JobStatus } from 'src/interfaces/jo
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { MediaService } from 'src/services/media.service';
import { ILoggingRepository, IMediaRepository, ISystemMetadataRepository, RawImageInfo } from 'src/types';
import { ILoggingRepository, IMediaRepository, RawImageInfo } from 'src/types';
import { assetStub } from 'test/fixtures/asset.stub';
import { faceStub } from 'test/fixtures/face.stub';
import { probeStub } from 'test/fixtures/media.stub';

View File

@@ -12,17 +12,12 @@ import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { ImmichTags } from 'src/repositories/metadata.repository';
import { MetadataService } from 'src/services/metadata.service';
import {
IConfigRepository,
IMapRepository,
IMediaRepository,
IMetadataRepository,
ISystemMetadataRepository,
} from 'src/types';
import { IConfigRepository, IMapRepository, IMediaRepository, IMetadataRepository } from 'src/types';
import { assetStub } from 'test/fixtures/asset.stub';
import { fileStub } from 'test/fixtures/file.stub';
import { probeStub } from 'test/fixtures/media.stub';

View File

@@ -8,10 +8,11 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, INotifyAlbumUpdateJob, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { EmailTemplate } from 'src/repositories/notification.repository';
import { NotificationService } from 'src/services/notification.service';
import { INotificationRepository, ISystemMetadataRepository } from 'src/types';
import { INotificationRepository } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub';

View File

@@ -10,8 +10,9 @@ import { DetectedFaces, IMachineLearningRepository } from 'src/interfaces/machin
import { IPersonRepository } from 'src/interfaces/person.interface';
import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { PersonService } from 'src/services/person.service';
import { IMediaRepository, ISystemMetadataRepository } from 'src/types';
import { IMediaRepository } from 'src/types';
import { ImmichFileResponse } from 'src/utils/file';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
@@ -354,7 +355,7 @@ describe(PersonService.name, () => {
sut.reassignFaces(authStub.admin, personStub.noName.id, {
data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }],
}),
).resolves.toBeDefined();
).resolves.toEqual([personStub.noName]);
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
@@ -447,7 +448,7 @@ describe(PersonService.name, () => {
it('should create a new person', async () => {
personMock.create.mockResolvedValue(personStub.primaryPerson);
await expect(sut.create(authStub.admin, {})).resolves.toBeDefined();
await expect(sut.create(authStub.admin, {})).resolves.toBe(personStub.primaryPerson);
expect(personMock.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id });
});

View File

@@ -104,7 +104,7 @@ export class PersonService extends BaseService {
await this.personRepository.reassignFace(face.id, personId);
}
result.push(mapPerson(person));
result.push(person);
}
if (changeFeaturePhoto.length > 0) {
// Remove duplicates
@@ -178,23 +178,20 @@ export class PersonService extends BaseService {
});
}
async create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
const person = await this.personRepository.create({
create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
return this.personRepository.create({
ownerId: auth.user.id,
name: dto.name,
birthDate: dto.birthDate,
isHidden: dto.isHidden,
isFavorite: dto.isFavorite,
color: dto.color,
});
return mapPerson(person);
}
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
await this.requireAccess({ auth, permission: Permission.PERSON_UPDATE, ids: [id] });
const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto;
const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite } = dto;
// TODO: set by faceId directly
let faceId: string | undefined = undefined;
if (assetId) {
@@ -214,7 +211,6 @@ export class PersonService extends BaseService {
birthDate,
isHidden,
isFavorite,
color,
});
if (assetId) {

View File

@@ -31,8 +31,6 @@ describe(SearchService.name, () => {
it('should pass options to search', async () => {
const { name } = personStub.withName;
personMock.getByName.mockResolvedValue([]);
await sut.searchPerson(authStub.user1, { name, withHidden: false });
expect(personMock.getByName).toHaveBeenCalledWith(authStub.user1.user.id, name, { withHidden: false });

View File

@@ -1,9 +1,8 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { AssetMapOptions, AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
import { PersonResponseDto } from 'src/dtos/person.dto';
import {
mapPlaces,
MetadataSearchDto,
PlacesResponseDto,
RandomSearchDto,
@@ -13,6 +12,7 @@ import {
SearchSuggestionRequestDto,
SearchSuggestionType,
SmartSearchDto,
mapPlaces,
} from 'src/dtos/search.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetOrder } from 'src/enum';
@@ -24,8 +24,7 @@ import { isSmartSearchEnabled } from 'src/utils/misc';
@Injectable()
export class SearchService extends BaseService {
async searchPerson(auth: AuthDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
const people = await this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
return people.map((person) => mapPerson(person));
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
}
async searchPlaces(dto: SearchPlacesDto): Promise<PlacesResponseDto[]> {

View File

@@ -1,8 +1,8 @@
import { SystemMetadataKey } from 'src/enum';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { ServerService } from 'src/services/server.service';
import { ISystemMetadataRepository } from 'src/types';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';

View File

@@ -1,6 +1,7 @@
import { UserEntity } from 'src/entities/user.entity';
import { JobStatus } from 'src/interfaces/job.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { SessionService } from 'src/services/session.service';
import { ISessionRepository } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub';
import { sessionStub } from 'test/fixtures/session.stub';
import { IAccessRepositoryMock } from 'test/repositories/access.repository.mock';
@@ -37,6 +38,7 @@ describe('SessionService', () => {
deviceType: '',
id: '123',
token: '420',
user: {} as UserEntity,
userId: '42',
},
]);
@@ -48,7 +50,7 @@ describe('SessionService', () => {
describe('getAll', () => {
it('should get the devices', async () => {
sessionMock.getByUserId.mockResolvedValue([sessionStub.valid as any, sessionStub.inactive]);
sessionMock.getByUserId.mockResolvedValue([sessionStub.valid, sessionStub.inactive]);
await expect(sut.getAll(authStub.user1)).resolves.toEqual([
{
createdAt: '2021-01-01T00:00:00.000Z',
@@ -74,7 +76,7 @@ describe('SessionService', () => {
describe('logoutDevices', () => {
it('should logout all devices', async () => {
sessionMock.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid] as any[]);
sessionMock.getByUserId.mockResolvedValue([sessionStub.inactive, sessionStub.valid]);
await sut.deleteAll(authStub.user1);

View File

@@ -29,11 +29,11 @@ describe(SharedLinkService.name, () => {
describe('getAll', () => {
it('should return all shared links for a user', async () => {
sharedLinkMock.getAll.mockResolvedValue([sharedLinkStub.expired, sharedLinkStub.valid]);
await expect(sut.getAll(authStub.user1, {})).resolves.toEqual([
await expect(sut.getAll(authStub.user1)).resolves.toEqual([
sharedLinkResponseStub.expired,
sharedLinkResponseStub.valid,
]);
expect(sharedLinkMock.getAll).toHaveBeenCalledWith({ userId: authStub.user1.user.id });
expect(sharedLinkMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
});
});

View File

@@ -9,7 +9,6 @@ import {
SharedLinkEditDto,
SharedLinkPasswordDto,
SharedLinkResponseDto,
SharedLinkSearchDto,
} from 'src/dtos/shared-link.dto';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { Permission, SharedLinkType } from 'src/enum';
@@ -18,10 +17,8 @@ import { getExternalDomain, OpenGraphTags } from 'src/utils/misc';
@Injectable()
export class SharedLinkService extends BaseService {
async getAll(auth: AuthDto, { albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
return this.sharedLinkRepository
.getAll({ userId: auth.user.id, albumId })
.then((links) => links.map((link) => mapSharedLink(link)));
async getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
return this.sharedLinkRepository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link)));
}
async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {

View File

@@ -5,8 +5,9 @@ import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SmartInfoService } from 'src/services/smart-info.service';
import { IConfigRepository, ISystemMetadataRepository } from 'src/types';
import { IConfigRepository } from 'src/types';
import { getCLIPModelInfo } from 'src/utils/misc';
import { assetStub } from 'test/fixtures/asset.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';

View File

@@ -8,9 +8,9 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { JobStatus } from 'src/interfaces/job.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { StorageTemplateService } from 'src/services/storage-template.service';
import { ISystemMetadataRepository } from 'src/types';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub';

View File

@@ -1,7 +1,8 @@
import { SystemMetadataKey } from 'src/enum';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { StorageService } from 'src/services/storage.service';
import { IConfigRepository, ILoggingRepository, ISystemMetadataRepository } from 'src/types';
import { IConfigRepository, ILoggingRepository } from 'src/types';
import { ImmichStartupError } from 'src/utils/misc';
import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newTestService } from 'test/utils';

View File

@@ -14,8 +14,9 @@ import {
} from 'src/enum';
import { IEventRepository } from 'src/interfaces/event.interface';
import { QueueName } from 'src/interfaces/job.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SystemConfigService } from 'src/services/system-config.service';
import { DeepPartial, IConfigRepository, ILoggingRepository, ISystemMetadataRepository } from 'src/types';
import { DeepPartial, IConfigRepository, ILoggingRepository } from 'src/types';
import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';

View File

@@ -1,6 +1,6 @@
import { SystemMetadataKey } from 'src/enum';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { SystemMetadataService } from 'src/services/system-metadata.service';
import { ISystemMetadataRepository } from 'src/types';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';

View File

@@ -4,9 +4,9 @@ import { CacheControl, UserMetadataKey } from 'src/enum';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { UserService } from 'src/services/user.service';
import { ISystemMetadataRepository } from 'src/types';
import { ImmichFileResponse } from 'src/utils/file';
import { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';

View File

@@ -4,14 +4,9 @@ import { serverVersion } from 'src/constants';
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { VersionService } from 'src/services/version.service';
import {
IConfigRepository,
ILoggingRepository,
IServerInfoRepository,
ISystemMetadataRepository,
IVersionHistoryRepository,
} from 'src/types';
import { IConfigRepository, ILoggingRepository, IServerInfoRepository, IVersionHistoryRepository } from 'src/types';
import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';

View File

@@ -14,10 +14,7 @@ import { MemoryRepository } from 'src/repositories/memory.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { NotificationRepository } from 'src/repositories/notification.repository';
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ProcessRepository } from 'src/repositories/process.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { MetricGroupRepository, TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
@@ -61,10 +58,7 @@ export type IMetadataRepository = RepositoryInterface<MetadataRepository>;
export type IMetricGroupRepository = RepositoryInterface<MetricGroupRepository>;
export type INotificationRepository = RepositoryInterface<NotificationRepository>;
export type IOAuthRepository = RepositoryInterface<OAuthRepository>;
export type IProcessRepository = RepositoryInterface<ProcessRepository>;
export type ISessionRepository = RepositoryInterface<SessionRepository>;
export type IServerInfoRepository = RepositoryInterface<ServerInfoRepository>;
export type ISystemMetadataRepository = RepositoryInterface<SystemMetadataRepository>;
export type ITelemetryRepository = RepositoryInterface<TelemetryRepository>;
export type ITrashRepository = RepositoryInterface<TrashRepository>;
export type IViewRepository = RepositoryInterface<ViewRepository>;
@@ -83,8 +77,6 @@ export type MemoryItem =
| Awaited<ReturnType<IMemoryRepository['create']>>
| Awaited<ReturnType<IMemoryRepository['search']>>[0];
export type SessionItem = Awaited<ReturnType<ISessionRepository['getByUserId']>>[0];
export interface CropOptions {
top: number;
left: number;

View File

@@ -7,7 +7,8 @@ import { SystemConfig, defaults } from 'src/config';
import { SystemConfigDto } from 'src/dtos/system-config.dto';
import { SystemMetadataKey } from 'src/enum';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { DeepPartial, IConfigRepository, ILoggingRepository, ISystemMetadataRepository } from 'src/types';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { DeepPartial, IConfigRepository, ILoggingRepository } from 'src/types';
import { getKeysDeep, unsetDeep } from 'src/utils/misc';
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;

View File

@@ -12,7 +12,6 @@ import {
IsArray,
IsBoolean,
IsDate,
IsHexColor,
IsNotEmpty,
IsOptional,
IsString,
@@ -98,15 +97,6 @@ export function Optional({ nullable, emptyToNull, ...validationOptions }: Option
return applyDecorators(...decorators);
}
export const ValidateHexColor = () => {
const decorators = [
IsHexColor(),
Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value)),
];
return applyDecorators(...decorators);
};
type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean };
export const ValidateUUID = (options?: UUIDOptions) => {
const { optional, each, nullable } = { optional: false, each: false, nullable: false, ...options };

View File

@@ -1,4 +1,4 @@
import { IProcessRepository } from 'src/types';
import { IProcessRepository } from 'src/interfaces/process.interface';
import { Mocked, vitest } from 'vitest';
export const newProcessRepositoryMock = (): Mocked<IProcessRepository> => {

View File

@@ -1,4 +1,4 @@
import { ISessionRepository } from 'src/types';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { Mocked, vitest } from 'vitest';
export const newSessionRepositoryMock = (): Mocked<ISessionRepository> => {

View File

@@ -1,4 +1,4 @@
import { ISystemMetadataRepository } from 'src/types';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { clearConfigCache } from 'src/utils/config';
import { Mocked, vitest } from 'vitest';

View File

@@ -15,10 +15,7 @@ import { MemoryRepository } from 'src/repositories/memory.repository';
import { MetadataRepository } from 'src/repositories/metadata.repository';
import { NotificationRepository } from 'src/repositories/notification.repository';
import { OAuthRepository } from 'src/repositories/oauth.repository';
import { ProcessRepository } from 'src/repositories/process.repository';
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
import { SessionRepository } from 'src/repositories/session.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
import { TelemetryRepository } from 'src/repositories/telemetry.repository';
import { TrashRepository } from 'src/repositories/trash.repository';
import { VersionHistoryRepository } from 'src/repositories/version-history.repository';
@@ -38,10 +35,7 @@ import {
IMetadataRepository,
INotificationRepository,
IOAuthRepository,
IProcessRepository,
IServerInfoRepository,
ISessionRepository,
ISystemMetadataRepository,
ITrashRepository,
IVersionHistoryRepository,
IViewRepository,
@@ -169,14 +163,14 @@ export const newTestService = <T extends BaseService>(
oauthMock as IOAuthRepository as OAuthRepository,
partnerMock,
personMock,
processMock as IProcessRepository as ProcessRepository,
processMock,
searchMock,
serverInfoMock as IServerInfoRepository as ServerInfoRepository,
sessionMock as ISessionRepository as SessionRepository,
sessionMock,
sharedLinkMock,
stackMock,
storageMock,
systemMock as ISystemMetadataRepository as SystemMetadataRepository,
systemMock,
tagMock,
telemetryMock as unknown as TelemetryRepository,
trashMock as ITrashRepository as TrashRepository,

6
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.126.0",
"version": "1.125.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.126.0",
"version": "1.125.7",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.9.8",
@@ -77,7 +77,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.126.0",
"version": "1.125.7",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.126.0",
"version": "1.125.7",
"license": "GNU Affero General Public License version 3",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",

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