Compare commits

..

5 Commits

Author SHA1 Message Date
Alex Tran ad996d1b88 wip: fav button 2026-04-26 22:05:56 -05:00
Alex Tran 08a73c0978 fix: tests 2026-04-26 13:06:17 -05:00
Alex 976ce39fe2 Merge branch 'main' into feat/favorite-albums 2026-04-26 09:18:12 -05:00
Alex a162230a75 chore: regenerate mobile openapi sdk 2026-04-23 04:15:14 +00:00
Alex 166f36e5bf feat(server,web): favorite albums per user 2026-04-23 03:33:32 +00:00
466 changed files with 4051 additions and 6649 deletions
-2
View File
@@ -392,8 +392,6 @@ jobs:
node-version-file: './server/.nvmrc' node-version-file: './server/.nvmrc'
cache: 'pnpm' cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml' cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
- name: Run pnpm install - name: Run pnpm install
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- name: Run medium tests - name: Run medium tests
+1 -2
View File
@@ -26,8 +26,7 @@
}, },
"[svelte]": { "[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode", "editor.defaultFormatter": "svelte.svelte-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
+1 -1
View File
@@ -1,5 +1,5 @@
[tools] [tools]
terragrunt = "1.0.2" terragrunt = "1.0.1"
opentofu = "1.11.6" opentofu = "1.11.6"
[tasks."tg:fmt"] [tasks."tg:fmt"]
+1 -1
View File
@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3 image: prom/prometheus@sha256:5550dc63da361dc30f6fe02ac0e4dfc736ededfef3c8d12a634db04a67824d78
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus
@@ -81,7 +81,7 @@ VectorChord is the successor extension to pgvecto.rs, allowing for higher perfor
### Migrating from pgvecto.rs ### Migrating from pgvecto.rs
Support for pgvecto.rs has been dropped as of 3.0, hence all users currently using pgvecto.rs should migrate to VectorChord. There are two primary approaches to do so. Support for pgvecto.rs will be dropped in a later release, hence we recommend all users currently using pgvecto.rs to migrate to VectorChord at their convenience. There are two primary approaches to do so.
The easiest option is to have both extensions installed during the migration: The easiest option is to have both extensions installed during the migration:
+1 -1
View File
@@ -39,7 +39,7 @@ You can learn how to set up Tailscale together with Immich with the [tutorial vi
### Cons ### Cons
- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022. - The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) suitable for personal use. - Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices.
- Tailscale needs to be installed and running on both server-side and client-side. - Tailscale needs to be installed and running on both server-side and client-side.
## Option 3: Reverse Proxy ## Option 3: Reverse Proxy
+1 -1
View File
@@ -81,7 +81,7 @@ Information on the current workers can be found [here](/administration/jobs-work
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> | | `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> | | `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
| `DB_SSL_MODE` | Database SSL mode | | server | | `DB_SSL_MODE` | Database SSL mode | | server |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`]) | | server | | `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server | | `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | database | | `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | database |
+4
View File
@@ -130,3 +130,7 @@ These storage mediums have different performance characteristics. As a result, t
#### Can I use the new database image as a general PostgreSQL image outside of Immich? #### Can I use the new database image as a general PostgreSQL image outside of Immich?
Its a standard PostgreSQL container image that additionally contains the VectorChord, pgvector, and (optionally) pgvecto.rs extensions. If you were using the previous pgvecto.rs image for other purposes, you can similarly do so with this image. Its a standard PostgreSQL container image that additionally contains the VectorChord, pgvector, and (optionally) pgvecto.rs extensions. If you were using the previous pgvecto.rs image for other purposes, you can similarly do so with this image.
#### If pgvecto.rs and pgvector still work, why should I switch to VectorChord?
VectorChord is faster, more stable, uses less RAM, and (with the settings Immich uses) offers higher-quality results than pgvector and pgvecto.rs. This translates to better search and facial recognition experiences. In addition, pgvecto.rs support will be dropped in the future, so changing it sooner will avoid disruption.
+42 -4
View File
@@ -2,44 +2,82 @@ import { expect } from 'vitest';
export const errorDto = { export const errorDto = {
unauthorized: { unauthorized: {
error: 'Unauthorized',
statusCode: 401,
message: 'Authentication required', message: 'Authentication required',
correlationId: expect.any(String),
}, },
unauthorizedWithMessage: (message: string) => ({ unauthorizedWithMessage: (message: string) => ({
error: 'Unauthorized',
statusCode: 401,
message, message,
correlationId: expect.any(String),
}), }),
forbidden: { forbidden: {
error: 'Forbidden',
statusCode: 403,
message: expect.any(String), message: expect.any(String),
correlationId: expect.any(String),
}, },
missingPermission: (permission: string) => ({ missingPermission: (permission: string) => ({
error: 'Forbidden',
statusCode: 403,
message: `Missing required permission: ${permission}`, message: `Missing required permission: ${permission}`,
correlationId: expect.any(String),
}), }),
wrongPassword: { wrongPassword: {
error: 'Bad Request',
statusCode: 400,
message: 'Wrong password', message: 'Wrong password',
correlationId: expect.any(String),
}, },
invalidToken: { invalidToken: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid user token', message: 'Invalid user token',
correlationId: expect.any(String),
}, },
invalidShareKey: { invalidShareKey: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid share key', message: 'Invalid share key',
correlationId: expect.any(String),
}, },
passwordRequired: { passwordRequired: {
error: 'Unauthorized',
statusCode: 401,
message: 'Password required', message: 'Password required',
correlationId: expect.any(String),
}, },
badRequest: (message: any = null) => ({ badRequest: (message: any = null) => ({
error: 'Bad Request',
statusCode: 400,
message: message ?? expect.anything(), message: message ?? expect.anything(),
}), correlationId: expect.any(String),
validationError: (errors?: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>) => ({
message: 'Validation failed',
errors: errors ? expect.arrayContaining(errors.map((e) => expect.objectContaining(e))) : expect.any(Array),
}), }),
noPermission: { noPermission: {
error: 'Bad Request',
statusCode: 400,
message: expect.stringContaining('Not found or no'), message: expect.stringContaining('Not found or no'),
correlationId: expect.any(String),
}, },
incorrectLogin: { incorrectLogin: {
error: 'Unauthorized',
statusCode: 401,
message: 'Incorrect email or password', message: 'Incorrect email or password',
correlationId: expect.any(String),
}, },
alreadyHasAdmin: { alreadyHasAdmin: {
error: 'Bad Request',
statusCode: 400,
message: 'The server already has an admin', message: 'The server already has an admin',
correlationId: expect.any(String),
},
invalidEmail: {
error: 'Bad Request',
statusCode: 400,
message: ['email must be an email'],
correlationId: expect.any(String),
}, },
}; };
+7 -21
View File
@@ -110,9 +110,7 @@ describe('/libraries', () => {
}); });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items']));
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
);
}); });
it('should not create an external library with duplicate exclusion patterns', async () => { it('should not create an external library with duplicate exclusion patterns', async () => {
@@ -127,9 +125,7 @@ describe('/libraries', () => {
}); });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items']));
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
);
}); });
}); });
@@ -161,9 +157,7 @@ describe('/libraries', () => {
.send({ name: '' }); .send({ name: '' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[name] Too small: expected string to have >=1 characters']));
errorDto.validationError([{ path: ['name'], message: 'Too small: expected string to have >=1 characters' }]),
);
}); });
it('should change the import paths', async () => { it('should change the import paths', async () => {
@@ -187,9 +181,7 @@ describe('/libraries', () => {
.send({ importPaths: [''] }); .send({ importPaths: [''] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[importPaths] Array items must not be empty']));
errorDto.validationError([{ path: ['importPaths'], message: 'Array items must not be empty' }]),
);
}); });
it('should reject duplicate import paths', async () => { it('should reject duplicate import paths', async () => {
@@ -199,9 +191,7 @@ describe('/libraries', () => {
.send({ importPaths: ['/path', '/path'] }); .send({ importPaths: ['/path', '/path'] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items']));
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
);
}); });
it('should change the exclusion pattern', async () => { it('should change the exclusion pattern', async () => {
@@ -225,9 +215,7 @@ describe('/libraries', () => {
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] }); .send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items']));
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
);
}); });
it('should reject an empty exclusion pattern', async () => { it('should reject an empty exclusion pattern', async () => {
@@ -237,9 +225,7 @@ describe('/libraries', () => {
.send({ exclusionPatterns: [''] }); .send({ exclusionPatterns: [''] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array items must not be empty']));
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array items must not be empty' }]),
);
}); });
}); });
+4 -12
View File
@@ -109,9 +109,7 @@ describe('/map', () => {
.get('/map/reverse-geocode?lon=123') .get('/map/reverse-geocode?lon=123')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN']));
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
);
}); });
it('should throw an error if a lat is not a number', async () => { it('should throw an error if a lat is not a number', async () => {
@@ -119,9 +117,7 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=abc&lon=123.456') .get('/map/reverse-geocode?lat=abc&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN']));
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
);
}); });
it('should throw an error if a lat is out of range', async () => { it('should throw an error if a lat is out of range', async () => {
@@ -129,9 +125,7 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=91&lon=123.456') .get('/map/reverse-geocode?lat=91&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[lat] Too big: expected number to be <=90']));
errorDto.validationError([{ path: ['lat'], message: 'Too big: expected number to be <=90' }]),
);
}); });
it('should throw an error if a lon is not provided', async () => { it('should throw an error if a lon is not provided', async () => {
@@ -139,9 +133,7 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=75') .get('/map/reverse-geocode?lat=75')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[lon] Invalid input: expected number, received NaN']));
errorDto.validationError([{ path: ['lon'], message: 'Invalid input: expected number, received NaN' }]),
);
}); });
const reverseGeocodeTestCases = [ const reverseGeocodeTestCases = [
+9 -18
View File
@@ -105,11 +105,7 @@ describe(`/oauth`, () => {
it(`should throw an error if a redirect uri is not provided`, async () => { it(`should throw an error if a redirect uri is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/authorize').send({}); const { status, body } = await request(app).post('/oauth/authorize').send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[redirectUri] Invalid input: expected string, received undefined']));
errorDto.validationError([
{ path: ['redirectUri'], message: 'Invalid input: expected string, received undefined' },
]),
);
}); });
it('should return a redirect uri', async () => { it('should return a redirect uri', async () => {
@@ -168,17 +164,13 @@ describe(`/oauth`, () => {
it(`should throw an error if a url is not provided`, async () => { it(`should throw an error if a url is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({}); const { status, body } = await request(app).post('/oauth/callback').send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[url] Invalid input: expected string, received undefined']));
errorDto.validationError([{ path: ['url'], message: 'Invalid input: expected string, received undefined' }]),
);
}); });
it(`should throw an error if the url is empty`, async () => { it(`should throw an error if the url is empty`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' }); const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[url] Too small: expected string to have >=1 characters']));
errorDto.validationError([{ path: ['url'], message: 'Too small: expected string to have >=1 characters' }]),
);
}); });
it(`should throw an error if the state is not provided`, async () => { it(`should throw an error if the state is not provided`, async () => {
@@ -340,7 +332,9 @@ describe(`/oauth`, () => {
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(500); expect(status).toBe(500);
expect(body).toMatchObject({ expect(body).toMatchObject({
error: 'Internal Server Error',
message: 'Failed to finish oauth', message: 'Failed to finish oauth',
statusCode: 500,
}); });
}); });
@@ -359,7 +353,7 @@ describe(`/oauth`, () => {
const callbackParams = await loginWithOAuth('oauth-no-auto-register'); const callbackParams = await loginWithOAuth('oauth-no-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams); const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('OAuth authentication failed')); expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.'));
}); });
it('should link to an existing user by email', async () => { it('should link to an existing user by email', async () => {
@@ -383,11 +377,7 @@ describe(`/oauth`, () => {
it(`should throw an error if the logout_token is not provided`, async () => { it(`should throw an error if the logout_token is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({}); const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[logout_token] Invalid input: expected string, received undefined']));
errorDto.validationError([
{ path: ['logout_token'], message: 'Invalid input: expected string, received undefined' },
]),
);
}); });
it(`should throw an error if an invalid logout token is provided`, async () => { it(`should throw an error if an invalid logout token is provided`, async () => {
@@ -505,10 +495,11 @@ describe(`/oauth`, () => {
}); });
it('should reject OAuth discovery over HTTP', async () => { it('should reject OAuth discovery over HTTP', async () => {
const { status } = await request(app) const { status, body } = await request(app)
.post('/oauth/authorize') .post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' }); .send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(500); expect(status).toBe(500);
expect(body).toMatchObject({ statusCode: 500 });
}); });
}); });
}); });
@@ -341,9 +341,7 @@ describe('/shared-links', () => {
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest());
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
);
}); });
it('should require an asset/album id', async () => { it('should require an asset/album id', async () => {
+2 -9
View File
@@ -41,9 +41,7 @@ describe('/stacks', () => {
.send({ assetIds: [asset.id] }); .send({ assetIds: [asset.id] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest());
errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]),
);
}); });
it('should require a valid id', async () => { it('should require a valid id', async () => {
@@ -53,12 +51,7 @@ describe('/stacks', () => {
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] }); .send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest());
errorDto.validationError([
{ path: ['assetIds', 0], message: 'Invalid UUID' },
{ path: ['assetIds', 1], message: 'Invalid UUID' },
]),
);
}); });
it('should require access', async () => { it('should require access', async () => {
+2 -2
View File
@@ -309,7 +309,7 @@ describe('/tags', () => {
.get(`/tags/${uuidDto.invalid}`) .get(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
}); });
it('should get tag details', async () => { it('should get tag details', async () => {
@@ -427,7 +427,7 @@ describe('/tags', () => {
.delete(`/tags/${uuidDto.invalid}`) .delete(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
}); });
it('should delete a tag', async () => { it('should delete a tag', async () => {
@@ -108,20 +108,14 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden); expect(body).toEqual(errorDto.forbidden);
}); });
for (const [key, message] of [ for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) {
['password', 'Invalid input: expected string, received null'],
['email', 'Invalid input: expected email, received object'],
['name', 'Invalid input: expected string, received null'],
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
['notify', 'Invalid input: expected boolean, received null'],
] as const) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/admin/users`) .post(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null }); .send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); expect(body).toEqual(errorDto.badRequest());
}); });
} }
@@ -159,19 +153,14 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden); expect(body).toEqual(errorDto.forbidden);
}); });
for (const [key, message] of [ for (const key of ['password', 'email', 'name', 'shouldChangePassword']) {
['password', 'Invalid input: expected string, received null'],
['email', 'Invalid input: expected email, received object'],
['name', 'Invalid input: expected string, received null'],
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
] as const) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`) .put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null }); .send({ [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: [key], message }])); expect(body).toEqual(errorDto.badRequest());
}); });
} }
+3 -7
View File
@@ -120,7 +120,7 @@ describe('/users', () => {
.set('Authorization', `Bearer ${nonAdmin.accessToken}`); .set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toMatchObject(errorDto.badRequest('Email is not available')); expect(body).toMatchObject(errorDto.badRequest('Email already in use by another account'));
}); });
it('should update my email', async () => { it('should update my email', async () => {
@@ -179,9 +179,7 @@ describe('/users', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
errorDto.validationError([ errorDto.badRequest(['[download.archiveSize] Invalid input: expected int, received number']),
{ path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' },
]),
); );
}); });
@@ -209,9 +207,7 @@ describe('/users', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
errorDto.validationError([ errorDto.badRequest(['[download.includeEmbeddedVideos] Invalid input: expected boolean, received number']),
{ path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' },
]),
); );
}); });
@@ -32,12 +32,8 @@ export function generateThumbhash(rng: SeededRandom): string {
return Array.from({ length: 10 }, () => rng.nextInt(0, 256).toString(16).padStart(2, '0')).join(''); return Array.from({ length: 10 }, () => rng.nextInt(0, 256).toString(16).padStart(2, '0')).join('');
} }
export function generateDuration(rng: SeededRandom): number { export function generateDuration(rng: SeededRandom): string {
return ( return `${rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS)}.${rng.nextInt(0, 1000).toString().padStart(3, '0')}`;
rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS) *
1000 +
rng.nextInt(0, 1000)
);
} }
export function generateUUID(): string { export function generateUUID(): string {
@@ -43,7 +43,7 @@ export type MockTimelineAsset = {
isTrashed: boolean; isTrashed: boolean;
isVideo: boolean; isVideo: boolean;
isImage: boolean; isImage: boolean;
duration: number | null; duration: string | null;
projectionType: string | null; projectionType: string | null;
livePhotoVideoId: string | null; livePhotoVideoId: string | null;
city: string | null; city: string | null;
@@ -304,7 +304,7 @@ test.describe('Timeline', () => {
await page.keyboard.down('Shift'); await page.keyboard.down('Shift');
await thumbnailUtils.withAssetId(page, assets[2].id).hover(); await thumbnailUtils.withAssetId(page, assets[2].id).hover();
await expect( await expect(
thumbnailUtils.locator(page).locator('.absolute.top-0.size-full.bg-immich-primary.opacity-40'), thumbnailUtils.locator(page).locator('.absolute.top-0.h-full.w-full.bg-immich-primary.opacity-40'),
).toHaveCount(3); ).toHaveCount(3);
await thumbnailUtils.selectButton(page, assets[2].id).click(); await thumbnailUtils.selectButton(page, assets[2].id).click();
await page.keyboard.up('Shift'); await page.keyboard.up('Shift');
-2
View File
@@ -1761,7 +1761,6 @@
"play_original_video": "Play original video", "play_original_video": "Play original video",
"play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.", "play_original_video_setting_description": "Prefer playback of original videos rather than transcoded videos. If original asset is not compatible it may not playback correctly.",
"play_transcoded_video": "Play transcoded video", "play_transcoded_video": "Play transcoded video",
"playback_speed": "Playback speed",
"please_auth_to_access": "Please authenticate to access", "please_auth_to_access": "Please authenticate to access",
"port": "Port", "port": "Port",
"preferences_settings_subtitle": "Manage the app's preferences", "preferences_settings_subtitle": "Manage the app's preferences",
@@ -2437,7 +2436,6 @@
"workflows": "Workflows", "workflows": "Workflows",
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters", "workflows_help_text": "Workflows automate actions on your assets based on triggers and filters",
"wrong_pin_code": "Wrong PIN code", "wrong_pin_code": "Wrong PIN code",
"x_of_total": "{x}/{total}",
"year": "Year", "year": "Year",
"years_ago": "{years, plural, one {# year} other {# years}} ago", "years_ago": "{years, plural, one {# year} other {# years}} ago",
"yes": "Yes", "yes": "Yes",
+2 -2
View File
@@ -68,7 +68,7 @@ ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
RUN apt-get update && \ RUN apt-get update && \
# Pascal support was dropped in 9.11 # Pascal support was dropped in 9.11
apt-get install --no-install-recommends -yqq libcudnn9-cuda-12=9.10.2.21-1 tzdata && \ apt-get install --no-install-recommends -yqq libcudnn9-cuda-12=9.10.2.21-1 && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
@@ -112,7 +112,7 @@ ARG RKNN_TOOLKIT_VERSION="v2.3.0"
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \ ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
MACHINE_LEARNING_MODEL_ARENA=false MACHINE_LEARNING_MODEL_ARENA=false
ADD --chmod=644 --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/ ADD --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/
FROM prod-${DEVICE} AS prod FROM prod-${DEVICE} AS prod
+1 -4
View File
@@ -183,10 +183,7 @@ async def predict(
text: str | None = Form(default=None), text: str | None = Form(default=None),
) -> Any: ) -> Any:
if image is not None: if image is not None:
decoded = await run(lambda: decode_pil(image)) inputs: Image | str = await run(lambda: decode_pil(image))
if decoded.width == 0 or decoded.height == 0:
raise HTTPException(400, "Image has zero width or height")
inputs: Image | str = decoded
elif text is not None: elif text is not None:
inputs = text inputs = text
else: else:
-13
View File
@@ -1198,19 +1198,6 @@ class TestLoad:
mock_model.model_format = ModelFormat.ONNX mock_model.model_format = ModelFormat.ONNX
@pytest.mark.parametrize("size", [(0, 100), (100, 0), (0, 0)])
def test_predict_rejects_empty_image(size: tuple[int, int], deployed_app: TestClient) -> None:
with mock.patch("immich_ml.main.decode_pil", return_value=Image.new("RGB", size)):
response = deployed_app.post(
"http://localhost:3003/predict",
data={"entries": json.dumps({"clip": {"visual": {"modelName": "ViT-B-32__openai"}}})},
files={"image": b"fake image bytes"},
)
assert response.status_code == 400
assert "zero" in response.json()["detail"].lower()
def test_root_endpoint(deployed_app: TestClient) -> None: def test_root_endpoint(deployed_app: TestClient) -> None:
response = deployed_app.get("http://localhost:3003") response = deployed_app.get("http://localhost:3003")
+4 -13
View File
@@ -15,26 +15,17 @@ config_roots = [
[tools] [tools]
node = "24.15.0" node = "24.15.0"
flutter = "3.41.7" flutter = "3.41.6"
pnpm = "10.33.1" pnpm = "10.33.0"
terragrunt = "1.0.2" terragrunt = "1.0.1"
opentofu = "1.11.6" opentofu = "1.11.6"
java = "21.0.2" java = "21.0.2"
[tools."github:CQLabs/homebrew-dcm"] [tools."github:CQLabs/homebrew-dcm"]
version = "1.37.0" version = "1.35.1"
bin = "dcm" bin = "dcm"
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm" postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
[tools."github:jellyfin/jellyfin-ffmpeg"]
version = "7.1.3-6"
[tools."github:jellyfin/jellyfin-ffmpeg".platforms]
linux-x64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" }
linux-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" }
macos-x64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz" }
macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" }
[settings] [settings]
experimental = true experimental = true
pin = true pin = true
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"dart.flutterSdkPath": ".fvm/versions/3.41.9", "dart.flutterSdkPath": ".fvm/versions/3.41.7",
"dart.lineLength": 120, "dart.lineLength": 120,
"[dart]": { "[dart]": {
"editor.rulers": [ "editor.rulers": [
+1 -1
View File
@@ -1 +1 @@
version: '>=1.29.0 <=1.37.0' version: '>=1.29.0 <=1.36.0'
+1 -1
View File
@@ -1,5 +1,5 @@
app_identifier "app.alextran.immich" # The bundle identifier of your app app_identifier "app.alextran.immich" # The bundle identifier of your app
apple_id "altran@futo.org" # Your Apple email address apple_id "alex.tran1502@gmail.com" # Your Apple email address
# For more information about the Appfile, see: # For more information about the Appfile, see:
+25 -25
View File
@@ -17,10 +17,9 @@ default_platform(:ios)
platform :ios do platform :ios do
# Constants # Constants
TEAM_ID = "2W7AC6T8T5" TEAM_ID = "2F67MQ8R79"
CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})" CODE_SIGN_IDENTITY = "Apple Distribution: Hau Tran (#{TEAM_ID})"
BASE_BUNDLE_ID = "app.alextran.immich" BASE_BUNDLE_ID = "app.alextran.immich"
DEV_BUNDLE_ID = "tech.futo.immich.testflight"
# Helper method to get App Store Connect API key # Helper method to get App Store Connect API key
def get_api_key def get_api_key
@@ -45,14 +44,16 @@ def get_version_from_pubspec
end end
# Helper method to configure code signing for all targets # Helper method to configure code signing for all targets
def configure_code_signing(base_bundle_id:, profile_name_main:, profile_name_share:, profile_name_widget:) def configure_code_signing(bundle_id_suffix: "", profile_name_main:, profile_name_share:, profile_name_widget:)
bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
# Runner (main app) # Runner (main app)
update_code_signing_settings( update_code_signing_settings(
use_automatic_signing: false, use_automatic_signing: false,
path: "./Runner.xcodeproj", path: "./Runner.xcodeproj",
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
code_sign_identity: CODE_SIGN_IDENTITY, code_sign_identity: CODE_SIGN_IDENTITY,
bundle_identifier: base_bundle_id, bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}",
profile_name: profile_name_main, profile_name: profile_name_main,
targets: ["Runner"] targets: ["Runner"]
) )
@@ -63,7 +64,7 @@ end
path: "./Runner.xcodeproj", path: "./Runner.xcodeproj",
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
code_sign_identity: CODE_SIGN_IDENTITY, code_sign_identity: CODE_SIGN_IDENTITY,
bundle_identifier: "#{base_bundle_id}.ShareExtension", bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension",
profile_name: profile_name_share, profile_name: profile_name_share,
targets: ["ShareExtension"] targets: ["ShareExtension"]
) )
@@ -74,7 +75,7 @@ end
path: "./Runner.xcodeproj", path: "./Runner.xcodeproj",
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID, team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
code_sign_identity: CODE_SIGN_IDENTITY, code_sign_identity: CODE_SIGN_IDENTITY,
bundle_identifier: "#{base_bundle_id}.Widget", bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget",
profile_name: profile_name_widget, profile_name: profile_name_widget,
targets: ["WidgetExtension"] targets: ["WidgetExtension"]
) )
@@ -83,7 +84,7 @@ end
# Helper method to build and upload to TestFlight # Helper method to build and upload to TestFlight
def build_and_upload( def build_and_upload(
api_key:, api_key:,
base_bundle_id:, bundle_id_suffix: "",
configuration: "Release", configuration: "Release",
distribute_external: true, distribute_external: true,
version_number: nil, version_number: nil,
@@ -91,7 +92,8 @@ end
profile_name_share:, profile_name_share:,
profile_name_widget: profile_name_widget:
) )
app_identifier = base_bundle_id bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
app_identifier = "#{BASE_BUNDLE_ID}#{bundle_suffix}"
# Set version number if provided # Set version number if provided
if version_number if version_number
@@ -140,18 +142,18 @@ end
# Download and install provisioning profiles from App Store Connect # Download and install provisioning profiles from App Store Connect
# Certificate is imported by GHA workflow into build.keychain # Certificate is imported by GHA workflow into build.keychain
# Capture profile names after each sigh call # Capture profile names after each sigh call
sigh(api_key: api_key, app_identifier: DEV_BUNDLE_ID, force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
main_profile_name = lane_context[SharedValues::SIGH_NAME] main_profile_name = lane_context[SharedValues::SIGH_NAME]
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.ShareExtension", force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
share_profile_name = lane_context[SharedValues::SIGH_NAME] share_profile_name = lane_context[SharedValues::SIGH_NAME]
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.Widget", force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
widget_profile_name = lane_context[SharedValues::SIGH_NAME] widget_profile_name = lane_context[SharedValues::SIGH_NAME]
# Configure code signing for dev bundle IDs using the downloaded profile names # Configure code signing for dev bundle IDs using the downloaded profile names
configure_code_signing( configure_code_signing(
base_bundle_id: DEV_BUNDLE_ID, bundle_id_suffix: "development",
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
profile_name_share: share_profile_name, profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name profile_name_widget: widget_profile_name
@@ -160,7 +162,7 @@ end
# Build and upload # Build and upload
build_and_upload( build_and_upload(
api_key: api_key, api_key: api_key,
base_bundle_id: DEV_BUNDLE_ID, bundle_id_suffix: "development",
configuration: "Profile", configuration: "Profile",
distribute_external: false, distribute_external: false,
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
@@ -187,7 +189,6 @@ end
# Configure code signing for production bundle IDs # Configure code signing for production bundle IDs
configure_code_signing( configure_code_signing(
base_bundle_id: BASE_BUNDLE_ID,
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
profile_name_share: share_profile_name, profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name profile_name_widget: widget_profile_name
@@ -196,7 +197,6 @@ end
# Build and upload with version number # Build and upload with version number
build_and_upload( build_and_upload(
api_key: api_key, api_key: api_key,
base_bundle_id: BASE_BUNDLE_ID,
version_number: get_version_from_pubspec, version_number: get_version_from_pubspec,
distribute_external: false, distribute_external: false,
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
@@ -243,25 +243,25 @@ end
desc "iOS Build Only (no TestFlight upload)" desc "iOS Build Only (no TestFlight upload)"
lane :gha_build_only do lane :gha_build_only do
# Use the same build process as the dev TestFlight lane, just skip the upload # Use the same build process as production, just skip the upload
# This ensures PR builds validate the same way as dev TestFlight builds # This ensures PR builds validate the same way as production builds
api_key = get_api_key api_key = get_api_key
# Download and install provisioning profiles from App Store Connect # Download and install provisioning profiles from App Store Connect
# Certificate is imported by GHA workflow into build.keychain # Certificate is imported by GHA workflow into build.keychain
sigh(api_key: api_key, app_identifier: DEV_BUNDLE_ID, force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
main_profile_name = lane_context[SharedValues::SIGH_NAME] main_profile_name = lane_context[SharedValues::SIGH_NAME]
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.ShareExtension", force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
share_profile_name = lane_context[SharedValues::SIGH_NAME] share_profile_name = lane_context[SharedValues::SIGH_NAME]
sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.Widget", force: true) sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
widget_profile_name = lane_context[SharedValues::SIGH_NAME] widget_profile_name = lane_context[SharedValues::SIGH_NAME]
# Configure code signing for dev bundle IDs # Configure code signing for dev bundle IDs
configure_code_signing( configure_code_signing(
base_bundle_id: DEV_BUNDLE_ID, bundle_id_suffix: "development",
profile_name_main: main_profile_name, profile_name_main: main_profile_name,
profile_name_share: share_profile_name, profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name profile_name_widget: widget_profile_name
@@ -277,9 +277,9 @@ end
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual", xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
export_options: { export_options: {
provisioningProfiles: { provisioningProfiles: {
DEV_BUNDLE_ID => main_profile_name, "#{BASE_BUNDLE_ID}.development" => main_profile_name,
"#{DEV_BUNDLE_ID}.ShareExtension" => share_profile_name, "#{BASE_BUNDLE_ID}.development.ShareExtension" => share_profile_name,
"#{DEV_BUNDLE_ID}.Widget" => widget_profile_name "#{BASE_BUNDLE_ID}.development.Widget" => widget_profile_name
}, },
signingStyle: "manual", signingStyle: "manual",
signingCertificate: CODE_SIGN_IDENTITY signingCertificate: CODE_SIGN_IDENTITY
@@ -3,7 +3,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
@@ -193,22 +192,17 @@ class SyncStreamService {
final remoteSyncAssets = data.cast<SyncAssetV1>(); final remoteSyncAssets = data.cast<SyncAssetV1>();
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets); await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) { if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList()); final hasPermission = await _localFilesManager.hasManageMediaPermission();
if (hasPermission) {
await _handleRemoteTrashed(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum));
await _applyRemoteRestoreToLocal();
} else {
_logger.warning("sync Trashed Assets cannot proceed because MANAGE_MEDIA permission is missing");
} }
return;
case SyncEntityType.assetV2:
final remoteSyncAssets = data.cast<SyncAssetV2>();
await _syncStreamRepository.updateAssetsV2(remoteSyncAssets);
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
} }
return; return;
case SyncEntityType.assetDeleteV1: case SyncEntityType.assetDeleteV1:
final remoteSyncAssets = data.cast<SyncAssetDeleteV1>(); return _syncStreamRepository.deleteAssetsV1(data.cast());
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
await _syncAssetDeletion(remoteSyncAssets.map((e) => e.assetId).toList());
}
return _syncStreamRepository.deleteAssetsV1(remoteSyncAssets);
case SyncEntityType.assetExifV1: case SyncEntityType.assetExifV1:
return _syncStreamRepository.updateAssetsExifV1(data.cast()); return _syncStreamRepository.updateAssetsExifV1(data.cast());
case SyncEntityType.assetEditV1: case SyncEntityType.assetEditV1:
@@ -221,12 +215,8 @@ class SyncStreamService {
return _syncStreamRepository.deleteAssetsMetadataV1(data.cast()); return _syncStreamRepository.deleteAssetsMetadataV1(data.cast());
case SyncEntityType.partnerAssetV1: case SyncEntityType.partnerAssetV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner'); return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner');
case SyncEntityType.partnerAssetV2:
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'partner');
case SyncEntityType.partnerAssetBackfillV1: case SyncEntityType.partnerAssetBackfillV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner backfill'); return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner backfill');
case SyncEntityType.partnerAssetBackfillV2:
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'partner backfill');
case SyncEntityType.partnerAssetDeleteV1: case SyncEntityType.partnerAssetDeleteV1:
return _syncStreamRepository.deleteAssetsV1(data.cast(), debugLabel: "partner"); return _syncStreamRepository.deleteAssetsV1(data.cast(), debugLabel: "partner");
case SyncEntityType.partnerAssetExifV1: case SyncEntityType.partnerAssetExifV1:
@@ -247,16 +237,10 @@ class SyncStreamService {
return _syncStreamRepository.deleteAlbumUsersV1(data.cast()); return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
case SyncEntityType.albumAssetCreateV1: case SyncEntityType.albumAssetCreateV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset create'); return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset create');
case SyncEntityType.albumAssetCreateV2:
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset create');
case SyncEntityType.albumAssetUpdateV1: case SyncEntityType.albumAssetUpdateV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset update'); return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset update');
case SyncEntityType.albumAssetUpdateV2:
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset update');
case SyncEntityType.albumAssetBackfillV1: case SyncEntityType.albumAssetBackfillV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset backfill'); return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset backfill');
case SyncEntityType.albumAssetBackfillV2:
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset backfill');
case SyncEntityType.albumAssetExifCreateV1: case SyncEntityType.albumAssetExifCreateV1:
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif create'); return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif create');
case SyncEntityType.albumAssetExifUpdateV1: case SyncEntityType.albumAssetExifUpdateV1:
@@ -358,47 +342,6 @@ class SyncStreamService {
} }
} }
Future<void> handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) async {
if (batchData.isEmpty) return;
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV2 events');
final List<SyncAssetV2> assets = [];
final List<SyncAssetExifV1> exifs = [];
try {
for (final data in batchData) {
if (data is! Map<String, dynamic>) {
continue;
}
final payload = data;
final assetData = payload['asset'];
final exifData = payload['exif'];
if (assetData == null || exifData == null) {
continue;
}
final asset = SyncAssetV2.fromJson(assetData);
final exif = SyncAssetExifV1.fromJson(exifData);
if (asset != null && exif != null) {
assets.add(asset);
exifs.add(exif);
}
}
if (assets.isNotEmpty && exifs.isNotEmpty) {
await _syncStreamRepository.updateAssetsV2(assets, debugLabel: 'websocket-batch');
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
_logger.info('Successfully processed ${assets.length} assets in batch');
}
} catch (error, stackTrace) {
_logger.severe("Error processing AssetUploadReadyV2 websocket batch events", error, stackTrace);
}
}
Future<void> handleWsAssetEditReadyV1(dynamic data) async { Future<void> handleWsAssetEditReadyV1(dynamic data) async {
_logger.info('Processing AssetEditReadyV1 event'); _logger.info('Processing AssetEditReadyV1 event');
@@ -439,55 +382,12 @@ class SyncStreamService {
} }
} }
Future<void> handleWsAssetEditReadyV2(dynamic data) async { Future<void> _handleRemoteTrashed(Iterable<String> checksums) async {
_logger.info('Processing AssetEditReadyV2 event'); if (checksums.isEmpty) {
try {
if (data is! Map<String, dynamic>) {
throw ArgumentError("Invalid data format for AssetEditReadyV2 event");
}
final payload = data;
if (payload['asset'] == null) {
throw ArgumentError("Missing 'asset' field in AssetEditReadyV2 event data");
}
final asset = SyncAssetV2.fromJson(payload['asset']);
if (asset == null) {
throw ArgumentError("Failed to parse 'asset' field in AssetEditReadyV2 event data");
}
final assetEdits = (payload['edit'] as List<dynamic>)
.map((e) => SyncAssetEditV1.fromJson(e))
.whereType<SyncAssetEditV1>()
.toList();
await _syncStreamRepository.updateAssetsV2([asset], debugLabel: 'websocket-edit');
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
_logger.info(
'Successfully processed AssetEditReadyV2 event for asset ${asset.id} with ${assetEdits.length} edits',
);
} catch (error, stackTrace) {
_logger.severe("Error processing AssetEditReadyV2 websocket event", error, stackTrace);
}
}
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
if (remoteIds.isEmpty) {
return Future.value(); return Future.value();
} else { } else {
final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(remoteIds); final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums);
if (localAssetsToTrash.isNotEmpty) { if (localAssetsToTrash.isNotEmpty) {
await _trashLocalAssets(localAssetsToTrash);
} else {
_logger.info("No assets found in backup-enabled albums for remote assets: $remoteIds");
}
}
}
Future<void> _trashLocalAssets(Map<String, List<LocalAsset>> localAssetsToTrash) async {
final mediaUrls = await Future.wait( final mediaUrls = await Future.wait(
localAssetsToTrash.values localAssetsToTrash.values
.expand((e) => e) .expand((e) => e)
@@ -498,6 +398,10 @@ class SyncStreamService {
if (result) { if (result) {
await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash); await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
} }
} else {
_logger.info("No assets found in backup-enabled albums for assets: $checksums");
}
}
} }
Future<void> _applyRemoteRestoreToLocal() async { Future<void> _applyRemoteRestoreToLocal() async {
@@ -509,23 +413,4 @@ class SyncStreamService {
_logger.info("No remote assets found for restoration"); _logger.info("No remote assets found for restoration");
} }
} }
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
if (!(await _localFilesManager.hasManageMediaPermission())) {
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
return;
}
await _handleRemoteDeleted(remoteIds);
await _applyRemoteRestoreToLocal();
}
Future<void> _syncAssetDeletion(List<String> remoteIds) async {
if (!(await _localFilesManager.hasManageMediaPermission())) {
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
return;
}
await _handleRemoteDeleted(remoteIds);
}
} }
+2 -32
View File
@@ -186,7 +186,7 @@ class BackgroundSyncManager {
}); });
} }
Future<void> syncWebsocketBatchV1(List<dynamic> batchData) { Future<void> syncWebsocketBatch(List<dynamic> batchData) {
if (_syncWebsocketTask != null) { if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future; return _syncWebsocketTask!.future;
} }
@@ -196,17 +196,7 @@ class BackgroundSyncManager {
}); });
} }
Future<void> syncWebsocketBatchV2(List<dynamic> batchData) { Future<void> syncWebsocketEdit(dynamic data) {
if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future;
}
_syncWebsocketTask = _handleWsAssetUploadReadyV2Batch(batchData);
return _syncWebsocketTask!.whenComplete(() {
_syncWebsocketTask = null;
});
}
Future<void> syncWebsocketEditV1(dynamic data) {
if (_syncWebsocketTask != null) { if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future; return _syncWebsocketTask!.future;
} }
@@ -216,16 +206,6 @@ class BackgroundSyncManager {
}); });
} }
Future<void> syncWebsocketEditV2(dynamic data) {
if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future;
}
_syncWebsocketTask = _handleWsAssetEditReadyV2(data);
return _syncWebsocketTask!.whenComplete(() {
_syncWebsocketTask = null;
});
}
Future<void> syncLinkedAlbum() { Future<void> syncLinkedAlbum() {
if (_linkedAlbumSyncTask != null) { if (_linkedAlbumSyncTask != null) {
return _linkedAlbumSyncTask!.future; return _linkedAlbumSyncTask!.future;
@@ -262,17 +242,7 @@ Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => ru
debugLabel: 'websocket-batch', debugLabel: 'websocket-batch',
); );
Cancelable<void> _handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) => runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV2Batch(batchData),
debugLabel: 'websocket-batch',
);
Cancelable<void> _handleWsAssetEditReadyV1(dynamic data) => runInIsolateGentle( Cancelable<void> _handleWsAssetEditReadyV1(dynamic data) => runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1(data), computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1(data),
debugLabel: 'websocket-edit', debugLabel: 'websocket-edit',
); );
Cancelable<void> _handleWsAssetEditReadyV2(dynamic data) => runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV2(data),
debugLabel: 'websocket-edit',
);
+3 -2
View File
@@ -1,5 +1,6 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart'; import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:openapi/api.dart' as api; import 'package:openapi/api.dart' as api;
@@ -13,7 +14,7 @@ extension DTOToAsset on api.AssetResponseDto {
updatedAt: updatedAt, updatedAt: updatedAt,
ownerId: ownerId, ownerId: ownerId,
visibility: visibility.toAssetVisibility(), visibility: visibility.toAssetVisibility(),
durationMs: duration, durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
height: height?.toInt(), height: height?.toInt(),
width: width?.toInt(), width: width?.toInt(),
isFavorite: isFavorite, isFavorite: isFavorite,
@@ -35,7 +36,7 @@ extension DTOToAsset on api.AssetResponseDto {
updatedAt: updatedAt, updatedAt: updatedAt,
ownerId: ownerId, ownerId: ownerId,
visibility: visibility.toAssetVisibility(), visibility: visibility.toAssetVisibility(),
durationMs: duration, durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
height: height?.toInt(), height: height?.toInt(),
width: width?.toInt(), width: width?.toInt(),
isFavorite: isFavorite, isFavorite: isFavorite,
@@ -109,40 +109,31 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
return query.map((localAlbum) => localAlbum.toDto()).get(); return query.map((localAlbum) => localAlbum.toDto()).get();
} }
Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> remoteIds) async { Future<Map<String, List<LocalAsset>>> getAssetsFromBackupAlbums(Iterable<String> checksums) async {
if (remoteIds.isEmpty) { if (checksums.isEmpty) {
return {}; return {};
} }
final result = <String, List<LocalAsset>>{}; final result = <String, List<LocalAsset>>{};
for (final slice in remoteIds.toSet().slices(kDriftMaxChunk)) { for (final slice in checksums.toSet().slices(kDriftMaxChunk)) {
final rows = final rows =
await (_db.select(_db.localAlbumAssetEntity).join([ await (_db.select(_db.localAlbumAssetEntity).join([
innerJoin( innerJoin(_db.localAlbumEntity, _db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id)),
_db.localAlbumEntity,
_db.localAlbumAssetEntity.albumId.equalsExp(_db.localAlbumEntity.id),
useColumns: false,
),
innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)), innerJoin(_db.localAssetEntity, _db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id)),
innerJoin(
_db.remoteAssetEntity,
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
useColumns: false,
),
])..where( ])..where(
_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) & _db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected) &
_db.remoteAssetEntity.id.isIn(slice), _db.localAssetEntity.checksum.isIn(slice),
)) ))
.get(); .get();
for (final row in rows) { for (final row in rows) {
final albumId = row.readTable(_db.localAlbumAssetEntity).albumId; final albumId = row.readTable(_db.localAlbumAssetEntity).albumId;
final asset = row.readTable(_db.localAssetEntity).toDto(); final assetData = row.readTable(_db.localAssetEntity);
final asset = assetData.toDto();
(result[albumId] ??= <LocalAsset>[]).add(asset); (result[albumId] ??= <LocalAsset>[]).add(asset);
} }
} }
return result; return result;
} }
@@ -46,25 +46,19 @@ class SyncApiRepository {
types: [ types: [
SyncRequestType.authUsersV1, SyncRequestType.authUsersV1,
SyncRequestType.usersV1, SyncRequestType.usersV1,
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0) SyncRequestType.assetsV1,
? SyncRequestType.assetsV2
: SyncRequestType.assetsV1,
SyncRequestType.assetExifsV1, SyncRequestType.assetExifsV1,
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetEditsV1, if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetEditsV1,
SyncRequestType.assetMetadataV1, SyncRequestType.assetMetadataV1,
SyncRequestType.partnersV1, SyncRequestType.partnersV1,
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0) SyncRequestType.partnerAssetsV1,
? SyncRequestType.partnerAssetsV2
: SyncRequestType.partnerAssetsV1,
SyncRequestType.partnerAssetExifsV1, SyncRequestType.partnerAssetExifsV1,
if (serverVersion < const SemVer(major: 3, minor: 0, patch: 0)) if (serverVersion < const SemVer(major: 3, minor: 0, patch: 0))
SyncRequestType.albumsV1 SyncRequestType.albumsV1
else else
SyncRequestType.albumsV2, SyncRequestType.albumsV2,
SyncRequestType.albumUsersV1, SyncRequestType.albumUsersV1,
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0) SyncRequestType.albumAssetsV1,
? SyncRequestType.albumAssetsV2
: SyncRequestType.albumAssetsV1,
SyncRequestType.albumAssetExifsV1, SyncRequestType.albumAssetExifsV1,
SyncRequestType.albumToAssetsV1, SyncRequestType.albumToAssetsV1,
SyncRequestType.memoriesV1, SyncRequestType.memoriesV1,
@@ -73,9 +67,8 @@ class SyncApiRepository {
SyncRequestType.partnerStacksV1, SyncRequestType.partnerStacksV1,
SyncRequestType.userMetadataV1, SyncRequestType.userMetadataV1,
SyncRequestType.peopleV1, SyncRequestType.peopleV1,
serverVersion >= const SemVer(major: 2, minor: 6, patch: 0) if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1,
? SyncRequestType.assetFacesV2 if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2,
: SyncRequestType.assetFacesV1,
], ],
reset: shouldReset, reset: shouldReset,
).toJson(), ).toJson(),
@@ -160,7 +153,6 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.partnerV1: SyncPartnerV1.fromJson, SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson, SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson,
SyncEntityType.assetV1: SyncAssetV1.fromJson, SyncEntityType.assetV1: SyncAssetV1.fromJson,
SyncEntityType.assetV2: SyncAssetV2.fromJson,
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson, SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
SyncEntityType.assetEditV1: SyncAssetEditV1.fromJson, SyncEntityType.assetEditV1: SyncAssetEditV1.fromJson,
@@ -168,9 +160,7 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson, SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson,
SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson, SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson,
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
SyncEntityType.partnerAssetV2: SyncAssetV2.fromJson,
SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson,
SyncEntityType.partnerAssetBackfillV2: SyncAssetV2.fromJson,
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson, SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson, SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson,
@@ -181,11 +171,8 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson, SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson, SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
SyncEntityType.albumAssetCreateV1: SyncAssetV1.fromJson, SyncEntityType.albumAssetCreateV1: SyncAssetV1.fromJson,
SyncEntityType.albumAssetCreateV2: SyncAssetV2.fromJson,
SyncEntityType.albumAssetUpdateV1: SyncAssetV1.fromJson, SyncEntityType.albumAssetUpdateV1: SyncAssetV1.fromJson,
SyncEntityType.albumAssetUpdateV2: SyncAssetV2.fromJson,
SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson, SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson,
SyncEntityType.albumAssetBackfillV2: SyncAssetV2.fromJson,
SyncEntityType.albumAssetExifCreateV1: SyncAssetExifV1.fromJson, SyncEntityType.albumAssetExifCreateV1: SyncAssetExifV1.fromJson,
SyncEntityType.albumAssetExifUpdateV1: SyncAssetExifV1.fromJson, SyncEntityType.albumAssetExifUpdateV1: SyncAssetExifV1.fromJson,
SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson, SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson,
@@ -220,44 +220,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
} }
} }
Future<void> updateAssetsV2(Iterable<SyncAssetV2> data, {String debugLabel = 'user'}) async {
try {
await _db.batch((batch) {
for (final asset in data) {
final companion = RemoteAssetEntityCompanion(
name: Value(asset.originalFileName),
type: Value(asset.type.toAssetType()),
createdAt: Value.absentIfNull(asset.fileCreatedAt),
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
durationMs: Value(asset.duration),
checksum: Value(asset.checksum),
isFavorite: Value(asset.isFavorite),
ownerId: Value(asset.ownerId),
localDateTime: Value(asset.localDateTime),
thumbHash: Value(asset.thumbhash),
deletedAt: Value(asset.deletedAt),
visibility: Value(asset.visibility.toAssetVisibility()),
livePhotoVideoId: Value(asset.livePhotoVideoId),
stackId: Value(asset.stackId),
libraryId: Value(asset.libraryId),
width: Value(asset.width),
height: Value(asset.height),
isEdited: Value(asset.isEdited),
);
batch.insert(
_db.remoteAssetEntity,
companion.copyWith(id: Value(asset.id)),
onConflict: DoUpdate((_) => companion),
);
}
});
} catch (error, stack) {
_logger.severe('Error: updateAssetsV2 - $debugLabel', error, stack);
rethrow;
}
}
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data, {String debugLabel = 'user'}) async { Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data, {String debugLabel = 'user'}) async {
try { try {
await _db.batch((batch) { await _db.batch((batch) {
@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/scroll_extensions.dart'; import 'package:immich_mobile/extensions/scroll_extensions.dart';
@@ -364,8 +363,7 @@ class _AssetPageState extends ConsumerState<AssetPage> {
} }
BaseAsset displayAsset = asset; BaseAsset displayAsset = asset;
final showAssetStack = ref.watch(timelineServiceProvider.select((s) => s.origin != TimelineOrigin.trash)); final stackChildren = ref.watch(stackChildrenNotifier(asset)).valueOrNull;
final stackChildren = showAssetStack ? ref.watch(stackChildrenNotifier(asset)).valueOrNull : null;
if (stackChildren != null && stackChildren.isNotEmpty) { if (stackChildren != null && stackChildren.isNotEmpty) {
displayAsset = stackChildren.elementAt(stackIndex); displayAsset = stackChildren.elementAt(stackIndex);
} }
@@ -1,10 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
class AssetStackRow extends ConsumerWidget { class AssetStackRow extends ConsumerWidget {
final List<RemoteAsset> stack; final List<RemoteAsset> stack;
@@ -17,11 +15,6 @@ class AssetStackRow extends ConsumerWidget {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final hideAssetStack = ref.read(timelineServiceProvider).origin == TimelineOrigin.trash;
if (hideAssetStack) {
return const SizedBox.shrink();
}
final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); final showingControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0); double opacity = ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity)) * (showingControls ? 1 : 0);
@@ -21,7 +21,6 @@ class ThumbnailTile extends ConsumerStatefulWidget {
this.showStorageIndicator = false, this.showStorageIndicator = false,
this.lockSelection = false, this.lockSelection = false,
this.heroOffset, this.heroOffset,
this.showStackIndicator = false,
super.key, super.key,
}); });
@@ -31,7 +30,6 @@ class ThumbnailTile extends ConsumerStatefulWidget {
final bool showStorageIndicator; final bool showStorageIndicator;
final bool lockSelection; final bool lockSelection;
final int? heroOffset; final int? heroOffset;
final bool showStackIndicator;
@override @override
ConsumerState<ThumbnailTile> createState() => _ThumbnailTileState(); ConsumerState<ThumbnailTile> createState() => _ThumbnailTileState();
@@ -141,14 +139,7 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
duration: Durations.short4, duration: Durations.short4,
child: Align( child: Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: Column( child: _AssetTypeIcons(asset: asset),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
_AssetTypeIcons(asset: asset),
if (widget.showStackIndicator) _StackIndicator(asset: asset),
],
),
), ),
), ),
if (storageIndicator && asset != null) if (storageIndicator && asset != null)
@@ -295,8 +286,8 @@ class _AssetTypeIcons extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final remoteAsset = asset is RemoteAsset ? asset as RemoteAsset : null; final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
final isLivePhoto = remoteAsset?.livePhotoVideoId != null; final isLivePhoto = asset is RemoteAsset && asset.livePhotoVideoId != null;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -304,6 +295,11 @@ class _AssetTypeIcons extends StatelessWidget {
children: [ children: [
if (asset.isVideo) if (asset.isVideo)
Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)), Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)),
if (hasStack)
const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0),
child: _TileOverlayIcon(Icons.burst_mode_rounded),
),
if (isLivePhoto) if (isLivePhoto)
const Padding( const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0), padding: EdgeInsets.only(right: 10.0, top: 6.0),
@@ -316,24 +312,6 @@ class _AssetTypeIcons extends StatelessWidget {
} }
} }
class _StackIndicator extends StatelessWidget {
final BaseAsset asset;
const _StackIndicator({required this.asset});
@override
Widget build(BuildContext context) {
if (asset is! RemoteAsset || (asset as RemoteAsset).stackId == null) {
return const SizedBox.shrink();
}
return const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0),
child: _TileOverlayIcon(Icons.burst_mode_rounded),
);
}
}
class _UploadProgressOverlay extends StatelessWidget { class _UploadProgressOverlay extends StatelessWidget {
final double progress; final double progress;
@@ -244,7 +244,6 @@ class _AssetTileWidget extends ConsumerWidget {
final lockSelection = _getLockSelectionStatus(ref); final lockSelection = _getLockSelectionStatus(ref);
final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator)); final showStorageIndicator = ref.watch(timelineArgsProvider.select((args) => args.showStorageIndicator));
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final showStackIndicator = ref.read(timelineServiceProvider).origin != TimelineOrigin.trash;
return RepaintBoundary( return RepaintBoundary(
child: GestureDetector( child: GestureDetector(
@@ -254,7 +253,6 @@ class _AssetTileWidget extends ConsumerWidget {
asset, asset,
lockSelection: lockSelection, lockSelection: lockSelection,
showStorageIndicator: showStorageIndicator, showStorageIndicator: showStorageIndicator,
showStackIndicator: showStackIndicator,
heroOffset: heroOffset, heroOffset: heroOffset,
), ),
), ),
+8 -40
View File
@@ -94,10 +94,8 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
state = const WebsocketState(isConnected: false, socket: null); state = const WebsocketState(isConnected: false, socket: null);
}); });
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReadyV1); socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
socket.on('AssetUploadReadyV2', _handleSyncAssetUploadReadyV2); socket.on('AssetEditReadyV1', _handleSyncAssetEditReady);
socket.on('AssetEditReadyV1', _handleSyncAssetEditReadyV1);
socket.on('AssetEditReadyV2', _handleSyncAssetEditReadyV2);
socket.on('on_config_update', _handleOnConfigUpdate); socket.on('on_config_update', _handleOnConfigUpdate);
socket.on('on_new_release', _handleReleaseUpdates); socket.on('on_new_release', _handleReleaseUpdates);
} catch (e) { } catch (e) {
@@ -165,25 +163,16 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
_ref.read(serverInfoProvider.notifier).handleReleaseInfo(serverVersion, releaseVersion); _ref.read(serverInfoProvider.notifier).handleReleaseInfo(serverVersion, releaseVersion);
} }
void _handleSyncAssetUploadReadyV1(dynamic data) { void _handleSyncAssetUploadReady(dynamic data) {
_batchedAssetUploadReady.add(data); _batchedAssetUploadReady.add(data);
_batchDebouncer.run(_processBatchedAssetUploadReadyV1); _batchDebouncer.run(_processBatchedAssetUploadReady);
} }
void _handleSyncAssetUploadReadyV2(dynamic data) { void _handleSyncAssetEditReady(dynamic data) {
_batchedAssetUploadReady.add(data); unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEdit(data));
_batchDebouncer.run(_processBatchedAssetUploadReadyV2);
} }
void _handleSyncAssetEditReadyV1(dynamic data) { void _processBatchedAssetUploadReady() {
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data));
}
void _handleSyncAssetEditReadyV2(dynamic data) {
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV2(data));
}
void _processBatchedAssetUploadReadyV1() {
if (_batchedAssetUploadReady.isEmpty) { if (_batchedAssetUploadReady.isEmpty) {
return; return;
} }
@@ -191,7 +180,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false); final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
try { try {
unawaited( unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) { _ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
if (isSyncAlbumEnabled) { if (isSyncAlbumEnabled) {
_ref.read(backgroundSyncProvider).syncLinkedAlbum(); _ref.read(backgroundSyncProvider).syncLinkedAlbum();
} }
@@ -203,27 +192,6 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
_batchedAssetUploadReady.clear(); _batchedAssetUploadReady.clear();
} }
void _processBatchedAssetUploadReadyV2() {
if (_batchedAssetUploadReady.isEmpty) {
return;
}
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
try {
unawaited(
_ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) {
if (isSyncAlbumEnabled) {
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
}
}),
);
} catch (error) {
_log.severe("Error processing batched AssetUploadReadyV2 events: $error");
}
_batchedAssetUploadReady.clear();
}
} }
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebsocketState>((ref) { final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebsocketState>((ref) {
@@ -148,7 +148,6 @@ enum ActionButtonType {
context.selectedCount == 1, context.selectedCount == 1,
ActionButtonType.unstack => ActionButtonType.unstack =>
context.isOwner && // context.isOwner && //
context.timelineOrigin != TimelineOrigin.trash &&
!context.isInLockedView && // !context.isInLockedView && //
context.isStacked, context.isStacked,
ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView, ActionButtonType.openInBrowser => context.asset.hasRemote && !context.isInLockedView,
+1 -1
View File
@@ -1,5 +1,5 @@
[tools] [tools]
flutter = "3.41.9" flutter = "3.41.7"
[tools."github:CQLabs/homebrew-dcm"] [tools."github:CQLabs/homebrew-dcm"]
version = "1.30.0" version = "1.30.0"
+1 -2
View File
@@ -146,7 +146,7 @@ Class | Method | HTTP request | Description
*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information
*DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Dismiss a duplicate group *DuplicatesApi* | [**deleteDuplicate**](doc//DuplicatesApi.md#deleteduplicate) | **DELETE** /duplicates/{id} | Delete a duplicate
*DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates | Delete duplicates *DuplicatesApi* | [**deleteDuplicates**](doc//DuplicatesApi.md#deleteduplicates) | **DELETE** /duplicates | Delete duplicates
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | Retrieve duplicates *DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | Retrieve duplicates
*DuplicatesApi* | [**resolveDuplicates**](doc//DuplicatesApi.md#resolveduplicates) | **POST** /duplicates/resolve | Resolve duplicate groups *DuplicatesApi* | [**resolveDuplicates**](doc//DuplicatesApi.md#resolveduplicates) | **POST** /duplicates/resolve | Resolve duplicate groups
@@ -579,7 +579,6 @@ Class | Method | HTTP request | Description
- [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md) - [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md)
- [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md) - [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md)
- [SyncAssetV1](doc//SyncAssetV1.md) - [SyncAssetV1](doc//SyncAssetV1.md)
- [SyncAssetV2](doc//SyncAssetV2.md)
- [SyncAuthUserV1](doc//SyncAuthUserV1.md) - [SyncAuthUserV1](doc//SyncAuthUserV1.md)
- [SyncEntityType](doc//SyncEntityType.md) - [SyncEntityType](doc//SyncEntityType.md)
- [SyncMemoryAssetDeleteV1](doc//SyncMemoryAssetDeleteV1.md) - [SyncMemoryAssetDeleteV1](doc//SyncMemoryAssetDeleteV1.md)
-1
View File
@@ -327,7 +327,6 @@ part 'model/sync_asset_face_v2.dart';
part 'model/sync_asset_metadata_delete_v1.dart'; part 'model/sync_asset_metadata_delete_v1.dart';
part 'model/sync_asset_metadata_v1.dart'; part 'model/sync_asset_metadata_v1.dart';
part 'model/sync_asset_v1.dart'; part 'model/sync_asset_v1.dart';
part 'model/sync_asset_v2.dart';
part 'model/sync_auth_user_v1.dart'; part 'model/sync_auth_user_v1.dart';
part 'model/sync_entity_type.dart'; part 'model/sync_entity_type.dart';
part 'model/sync_memory_asset_delete_v1.dart'; part 'model/sync_memory_asset_delete_v1.dart';
+12 -3
View File
@@ -508,9 +508,12 @@ class AlbumsApi {
/// * [String] assetId: /// * [String] assetId:
/// Filter albums containing this asset ID (ignores shared parameter) /// Filter albums containing this asset ID (ignores shared parameter)
/// ///
/// * [bool] favorite:
/// Filter to only albums favorited by the authenticated user
///
/// * [bool] shared: /// * [bool] shared:
/// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums /// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
Future<Response> getAllAlbumsWithHttpInfo({ String? assetId, bool? shared, }) async { Future<Response> getAllAlbumsWithHttpInfo({ String? assetId, bool? favorite, bool? shared, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/albums'; final apiPath = r'/albums';
@@ -524,6 +527,9 @@ class AlbumsApi {
if (assetId != null) { if (assetId != null) {
queryParams.addAll(_queryParams('', 'assetId', assetId)); queryParams.addAll(_queryParams('', 'assetId', assetId));
} }
if (favorite != null) {
queryParams.addAll(_queryParams('', 'favorite', favorite));
}
if (shared != null) { if (shared != null) {
queryParams.addAll(_queryParams('', 'shared', shared)); queryParams.addAll(_queryParams('', 'shared', shared));
} }
@@ -551,10 +557,13 @@ class AlbumsApi {
/// * [String] assetId: /// * [String] assetId:
/// Filter albums containing this asset ID (ignores shared parameter) /// Filter albums containing this asset ID (ignores shared parameter)
/// ///
/// * [bool] favorite:
/// Filter to only albums favorited by the authenticated user
///
/// * [bool] shared: /// * [bool] shared:
/// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums /// Filter by shared status: true = only shared, false = not shared, undefined = all owned albums
Future<List<AlbumResponseDto>?> getAllAlbums({ String? assetId, bool? shared, }) async { Future<List<AlbumResponseDto>?> getAllAlbums({ String? assetId, bool? favorite, bool? shared, }) async {
final response = await getAllAlbumsWithHttpInfo( assetId: assetId, shared: shared, ); final response = await getAllAlbumsWithHttpInfo( assetId: assetId, favorite: favorite, shared: shared, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
+6 -6
View File
@@ -1234,8 +1234,8 @@ class AssetsApi {
/// * [String] xImmichChecksum: /// * [String] xImmichChecksum:
/// sha1 checksum that can be used for duplicate detection before the file is uploaded /// sha1 checksum that can be used for duplicate detection before the file is uploaded
/// ///
/// * [int] duration: /// * [String] duration:
/// Duration in milliseconds (for videos) /// Duration (for videos)
/// ///
/// * [String] filename: /// * [String] filename:
/// Filename /// Filename
@@ -1253,7 +1253,7 @@ class AssetsApi {
/// Sidecar file data /// Sidecar file data
/// ///
/// * [AssetVisibility] visibility: /// * [AssetVisibility] visibility:
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/assets'; final apiPath = r'/assets';
@@ -1358,8 +1358,8 @@ class AssetsApi {
/// * [String] xImmichChecksum: /// * [String] xImmichChecksum:
/// sha1 checksum that can be used for duplicate detection before the file is uploaded /// sha1 checksum that can be used for duplicate detection before the file is uploaded
/// ///
/// * [int] duration: /// * [String] duration:
/// Duration in milliseconds (for videos) /// Duration (for videos)
/// ///
/// * [String] filename: /// * [String] filename:
/// Filename /// Filename
@@ -1377,7 +1377,7 @@ class AssetsApi {
/// Sidecar file data /// Sidecar file data
/// ///
/// * [AssetVisibility] visibility: /// * [AssetVisibility] visibility:
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+4 -4
View File
@@ -16,9 +16,9 @@ class DuplicatesApi {
final ApiClient apiClient; final ApiClient apiClient;
/// Dismiss a duplicate group /// Delete a duplicate
/// ///
/// Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them. /// Delete a single duplicate asset specified by its ID.
/// ///
/// Note: This method returns the HTTP [Response]. /// Note: This method returns the HTTP [Response].
/// ///
@@ -51,9 +51,9 @@ class DuplicatesApi {
); );
} }
/// Dismiss a duplicate group /// Delete a duplicate
/// ///
/// Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them. /// Delete a single duplicate asset specified by its ID.
/// ///
/// Parameters: /// Parameters:
/// ///
+6 -6
View File
@@ -183,15 +183,15 @@ class PeopleApi {
/// * [String] closestPersonId: /// * [String] closestPersonId:
/// Closest person ID for similarity search /// Closest person ID for similarity search
/// ///
/// * [int] page: /// * [num] page:
/// Page number for pagination /// Page number for pagination
/// ///
/// * [int] size: /// * [num] size:
/// Number of items per page /// Number of items per page
/// ///
/// * [bool] withHidden: /// * [bool] withHidden:
/// Include hidden people /// Include hidden people
Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async { Future<Response> getAllPeopleWithHttpInfo({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/people'; final apiPath = r'/people';
@@ -244,15 +244,15 @@ class PeopleApi {
/// * [String] closestPersonId: /// * [String] closestPersonId:
/// Closest person ID for similarity search /// Closest person ID for similarity search
/// ///
/// * [int] page: /// * [num] page:
/// Page number for pagination /// Page number for pagination
/// ///
/// * [int] size: /// * [num] size:
/// Number of items per page /// Number of items per page
/// ///
/// * [bool] withHidden: /// * [bool] withHidden:
/// Include hidden people /// Include hidden people
Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, int? page, int? size, bool? withHidden, }) async { Future<PeopleResponseDto?> getAllPeople({ String? closestAssetId, String? closestPersonId, num? page, num? size, bool? withHidden, }) async {
final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, ); final response = await getAllPeopleWithHttpInfo( closestAssetId: closestAssetId, closestPersonId: closestPersonId, page: page, size: size, withHidden: withHidden, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+6 -6
View File
@@ -404,10 +404,10 @@ class SearchApi {
/// * [List<String>] personIds: /// * [List<String>] personIds:
/// Filter by person IDs /// Filter by person IDs
/// ///
/// * [int] rating: /// * [num] rating:
/// Filter by rating [1-5], or null for unrated /// Filter by rating [1-5], or null for unrated
/// ///
/// * [int] size: /// * [num] size:
/// Number of results to return /// Number of results to return
/// ///
/// * [String] state: /// * [String] state:
@@ -443,7 +443,7 @@ class SearchApi {
/// ///
/// * [bool] withExif: /// * [bool] withExif:
/// Include EXIF data in response /// Include EXIF data in response
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final apiPath = r'/search/large-assets'; final apiPath = r'/search/large-assets';
@@ -619,10 +619,10 @@ class SearchApi {
/// * [List<String>] personIds: /// * [List<String>] personIds:
/// Filter by person IDs /// Filter by person IDs
/// ///
/// * [int] rating: /// * [num] rating:
/// Filter by rating [1-5], or null for unrated /// Filter by rating [1-5], or null for unrated
/// ///
/// * [int] size: /// * [num] size:
/// Number of results to return /// Number of results to return
/// ///
/// * [String] state: /// * [String] state:
@@ -658,7 +658,7 @@ class SearchApi {
/// ///
/// * [bool] withExif: /// * [bool] withExif:
/// Include EXIF data in response /// Include EXIF data in response
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async { Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, ); final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
-2
View File
@@ -700,8 +700,6 @@ class ApiClient {
return SyncAssetMetadataV1.fromJson(value); return SyncAssetMetadataV1.fromJson(value);
case 'SyncAssetV1': case 'SyncAssetV1':
return SyncAssetV1.fromJson(value); return SyncAssetV1.fromJson(value);
case 'SyncAssetV2':
return SyncAssetV2.fromJson(value);
case 'SyncAuthUserV1': case 'SyncAuthUserV1':
return SyncAuthUserV1.fromJson(value); return SyncAuthUserV1.fromJson(value);
case 'SyncEntityType': case 'SyncEntityType':
+10 -1
View File
@@ -24,6 +24,7 @@ class AlbumResponseDto {
required this.hasSharedLink, required this.hasSharedLink,
required this.id, required this.id,
required this.isActivityEnabled, required this.isActivityEnabled,
required this.isFavorite,
this.lastModifiedAssetTimestamp, this.lastModifiedAssetTimestamp,
this.order, this.order,
required this.shared, required this.shared,
@@ -72,6 +73,9 @@ class AlbumResponseDto {
/// Activity feed enabled /// Activity feed enabled
bool isActivityEnabled; bool isActivityEnabled;
/// Whether the authenticated user has favorited this album
bool isFavorite;
/// Last modified asset timestamp /// Last modified asset timestamp
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@@ -117,6 +121,7 @@ class AlbumResponseDto {
other.hasSharedLink == hasSharedLink && other.hasSharedLink == hasSharedLink &&
other.id == id && other.id == id &&
other.isActivityEnabled == isActivityEnabled && other.isActivityEnabled == isActivityEnabled &&
other.isFavorite == isFavorite &&
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp && other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
other.order == order && other.order == order &&
other.shared == shared && other.shared == shared &&
@@ -137,6 +142,7 @@ class AlbumResponseDto {
(hasSharedLink.hashCode) + (hasSharedLink.hashCode) +
(id.hashCode) + (id.hashCode) +
(isActivityEnabled.hashCode) + (isActivityEnabled.hashCode) +
(isFavorite.hashCode) +
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) + (lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
(order == null ? 0 : order!.hashCode) + (order == null ? 0 : order!.hashCode) +
(shared.hashCode) + (shared.hashCode) +
@@ -144,7 +150,7 @@ class AlbumResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]'; String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, albumUsers=$albumUsers, assetCount=$assetCount, contributorCounts=$contributorCounts, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, isFavorite=$isFavorite, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, order=$order, shared=$shared, startDate=$startDate, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -167,6 +173,7 @@ class AlbumResponseDto {
json[r'hasSharedLink'] = this.hasSharedLink; json[r'hasSharedLink'] = this.hasSharedLink;
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isActivityEnabled'] = this.isActivityEnabled; json[r'isActivityEnabled'] = this.isActivityEnabled;
json[r'isFavorite'] = this.isFavorite;
if (this.lastModifiedAssetTimestamp != null) { if (this.lastModifiedAssetTimestamp != null) {
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String(); json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
} else { } else {
@@ -207,6 +214,7 @@ class AlbumResponseDto {
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!, hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!, isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''), lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''),
order: AssetOrder.fromJson(json[r'order']), order: AssetOrder.fromJson(json[r'order']),
shared: mapValueOfType<bool>(json, r'shared')!, shared: mapValueOfType<bool>(json, r'shared')!,
@@ -268,6 +276,7 @@ class AlbumResponseDto {
'hasSharedLink', 'hasSharedLink',
'id', 'id',
'isActivityEnabled', 'isActivityEnabled',
'isFavorite',
'shared', 'shared',
'updatedAt', 'updatedAt',
}; };
+2 -5
View File
@@ -37,15 +37,12 @@ class AssetBulkUpdateDto {
/// Relative time offset in seconds /// Relative time offset in seconds
/// ///
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
///
/// Please note: This property should have been non-nullable! Since the specification file /// 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 /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? dateTimeRelative; num? dateTimeRelative;
/// Asset description /// Asset description
/// ///
@@ -216,7 +213,7 @@ class AssetBulkUpdateDto {
return AssetBulkUpdateDto( return AssetBulkUpdateDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'), dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
dateTimeRelative: mapValueOfType<int>(json, r'dateTimeRelative'), dateTimeRelative: num.parse('${json[r'dateTimeRelative']}'),
description: mapValueOfType<String>(json, r'description'), description: mapValueOfType<String>(json, r'description'),
duplicateId: mapValueOfType<String>(json, r'duplicateId'), duplicateId: mapValueOfType<String>(json, r'duplicateId'),
ids: json[r'ids'] is Iterable ids: json[r'ids'] is Iterable
@@ -24,26 +24,22 @@ class AssetEditActionItemDtoParameters {
/// Height of the crop /// Height of the crop
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num height;
int height;
/// Width of the crop /// Width of the crop
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num width;
int width;
/// Top-Left X coordinate of crop /// Top-Left X coordinate of crop
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num x;
int x;
/// Top-Left Y coordinate of crop /// Top-Left Y coordinate of crop
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num y;
int y;
/// Rotation angle in degrees /// Rotation angle in degrees
num angle; num angle;
@@ -92,10 +88,10 @@ class AssetEditActionItemDtoParameters {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return AssetEditActionItemDtoParameters( return AssetEditActionItemDtoParameters(
height: mapValueOfType<int>(json, r'height')!, height: num.parse('${json[r'height']}'),
width: mapValueOfType<int>(json, r'width')!, width: num.parse('${json[r'width']}'),
x: mapValueOfType<int>(json, r'x')!, x: num.parse('${json[r'x']}'),
y: mapValueOfType<int>(json, r'y')!, y: num.parse('${json[r'y']}'),
angle: num.parse('${json[r'angle']}'), angle: num.parse('${json[r'angle']}'),
axis: MirrorAxis.fromJson(json[r'axis'])!, axis: MirrorAxis.fromJson(json[r'axis'])!,
); );
+11 -12
View File
@@ -57,11 +57,8 @@ class AssetResponseDto {
/// Duplicate group ID /// Duplicate group ID
String? duplicateId; String? duplicateId;
/// Video/gif duration in milliseconds (null for static images) /// Video/gif duration in hh:mm:ss.SSS format (null for static images)
/// String? duration;
/// Minimum value: 0
/// Maximum value: 2147483647
int? duration;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@@ -83,8 +80,7 @@ class AssetResponseDto {
/// Asset height /// Asset height
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? height;
int? height;
/// Asset ID /// Asset ID
String id; String id;
@@ -169,8 +165,7 @@ class AssetResponseDto {
/// Asset width /// Asset width
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? width;
int? width;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
@@ -346,12 +341,14 @@ class AssetResponseDto {
checksum: mapValueOfType<String>(json, r'checksum')!, checksum: mapValueOfType<String>(json, r'checksum')!,
createdAt: mapDateTime(json, r'createdAt', r'')!, createdAt: mapDateTime(json, r'createdAt', r'')!,
duplicateId: mapValueOfType<String>(json, r'duplicateId'), duplicateId: mapValueOfType<String>(json, r'duplicateId'),
duration: mapValueOfType<int>(json, r'duration'), duration: mapValueOfType<String>(json, r'duration'),
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']), exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!, fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!, hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
height: mapValueOfType<int>(json, r'height'), height: json[r'height'] == null
? null
: num.parse('${json[r'height']}'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isArchived: mapValueOfType<bool>(json, r'isArchived')!, isArchived: mapValueOfType<bool>(json, r'isArchived')!,
isEdited: mapValueOfType<bool>(json, r'isEdited')!, isEdited: mapValueOfType<bool>(json, r'isEdited')!,
@@ -375,7 +372,9 @@ class AssetResponseDto {
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']), unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
updatedAt: mapDateTime(json, r'updatedAt', r'')!, updatedAt: mapDateTime(json, r'updatedAt', r'')!,
visibility: AssetVisibility.fromJson(json[r'visibility'])!, visibility: AssetVisibility.fromJson(json[r'visibility'])!,
width: mapValueOfType<int>(json, r'width'), width: json[r'width'] == null
? null
: num.parse('${json[r'width']}'),
); );
} }
return null; return null;
+8 -12
View File
@@ -22,26 +22,22 @@ class CropParameters {
/// Height of the crop /// Height of the crop
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num height;
int height;
/// Width of the crop /// Width of the crop
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num width;
int width;
/// Top-Left X coordinate of crop /// Top-Left X coordinate of crop
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num x;
int x;
/// Top-Left Y coordinate of crop /// Top-Left Y coordinate of crop
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num y;
int y;
@override @override
bool operator ==(Object other) => identical(this, other) || other is CropParameters && bool operator ==(Object other) => identical(this, other) || other is CropParameters &&
@@ -79,10 +75,10 @@ class CropParameters {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return CropParameters( return CropParameters(
height: mapValueOfType<int>(json, r'height')!, height: num.parse('${json[r'height']}'),
width: mapValueOfType<int>(json, r'width')!, width: num.parse('${json[r'width']}'),
x: mapValueOfType<int>(json, r'x')!, x: num.parse('${json[r'x']}'),
y: mapValueOfType<int>(json, r'y')!, y: num.parse('${json[r'y']}'),
); );
} }
return null; return null;
+2 -3
View File
@@ -27,8 +27,7 @@ class DatabaseBackupConfig {
/// Keep last amount /// Keep last amount
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991 num keepLastAmount;
int keepLastAmount;
@override @override
bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig && bool operator ==(Object other) => identical(this, other) || other is DatabaseBackupConfig &&
@@ -65,7 +64,7 @@ class DatabaseBackupConfig {
return DatabaseBackupConfig( return DatabaseBackupConfig(
cronExpression: mapValueOfType<String>(json, r'cronExpression')!, cronExpression: mapValueOfType<String>(json, r'cronExpression')!,
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
keepLastAmount: mapValueOfType<int>(json, r'keepLastAmount')!, keepLastAmount: num.parse('${json[r'keepLastAmount']}'),
); );
} }
return null; return null;
+2 -5
View File
@@ -22,10 +22,7 @@ class DatabaseBackupDto {
String filename; String filename;
/// Backup file size /// Backup file size
/// num filesize;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int filesize;
/// Backup timezone /// Backup timezone
String timezone; String timezone;
@@ -64,7 +61,7 @@ class DatabaseBackupDto {
return DatabaseBackupDto( return DatabaseBackupDto(
filename: mapValueOfType<String>(json, r'filename')!, filename: mapValueOfType<String>(json, r'filename')!,
filesize: mapValueOfType<int>(json, r'filesize')!, filesize: num.parse('${json[r'filesize']}'),
timezone: mapValueOfType<String>(json, r'timezone')!, timezone: mapValueOfType<String>(json, r'timezone')!,
); );
} }
+16 -16
View File
@@ -52,14 +52,12 @@ class ExifResponseDto {
/// Image height in pixels /// Image height in pixels
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? exifImageHeight;
int? exifImageHeight;
/// Image width in pixels /// Image width in pixels
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? exifImageWidth;
int? exifImageWidth;
/// Exposure time /// Exposure time
String? exposureTime; String? exposureTime;
@@ -77,10 +75,7 @@ class ExifResponseDto {
num? focalLength; num? focalLength;
/// ISO sensitivity /// ISO sensitivity
/// num? iso;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int? iso;
/// GPS latitude /// GPS latitude
num? latitude; num? latitude;
@@ -107,10 +102,7 @@ class ExifResponseDto {
String? projectionType; String? projectionType;
/// Rating /// Rating
/// num? rating;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int? rating;
/// State/province name /// State/province name
String? state; String? state;
@@ -300,8 +292,12 @@ class ExifResponseDto {
country: mapValueOfType<String>(json, r'country'), country: mapValueOfType<String>(json, r'country'),
dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''), dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''),
description: mapValueOfType<String>(json, r'description'), description: mapValueOfType<String>(json, r'description'),
exifImageHeight: mapValueOfType<int>(json, r'exifImageHeight'), exifImageHeight: json[r'exifImageHeight'] == null
exifImageWidth: mapValueOfType<int>(json, r'exifImageWidth'), ? null
: num.parse('${json[r'exifImageHeight']}'),
exifImageWidth: json[r'exifImageWidth'] == null
? null
: num.parse('${json[r'exifImageWidth']}'),
exposureTime: mapValueOfType<String>(json, r'exposureTime'), exposureTime: mapValueOfType<String>(json, r'exposureTime'),
fNumber: json[r'fNumber'] == null fNumber: json[r'fNumber'] == null
? null ? null
@@ -310,7 +306,9 @@ class ExifResponseDto {
focalLength: json[r'focalLength'] == null focalLength: json[r'focalLength'] == null
? null ? null
: num.parse('${json[r'focalLength']}'), : num.parse('${json[r'focalLength']}'),
iso: mapValueOfType<int>(json, r'iso'), iso: json[r'iso'] == null
? null
: num.parse('${json[r'iso']}'),
latitude: json[r'latitude'] == null latitude: json[r'latitude'] == null
? null ? null
: num.parse('${json[r'latitude']}'), : num.parse('${json[r'latitude']}'),
@@ -323,7 +321,9 @@ class ExifResponseDto {
modifyDate: mapDateTime(json, r'modifyDate', r''), modifyDate: mapDateTime(json, r'modifyDate', r''),
orientation: mapValueOfType<String>(json, r'orientation'), orientation: mapValueOfType<String>(json, r'orientation'),
projectionType: mapValueOfType<String>(json, r'projectionType'), projectionType: mapValueOfType<String>(json, r'projectionType'),
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
timeZone: mapValueOfType<String>(json, r'timeZone'), timeZone: mapValueOfType<String>(json, r'timeZone'),
); );
@@ -21,13 +21,9 @@ class MachineLearningAvailabilityChecksDto {
/// Enabled /// Enabled
bool enabled; bool enabled;
/// Minimum value: -9007199254740991 num interval;
/// Maximum value: 9007199254740991
int interval;
/// Minimum value: -9007199254740991 num timeout;
/// Maximum value: 9007199254740991
int timeout;
@override @override
bool operator ==(Object other) => identical(this, other) || other is MachineLearningAvailabilityChecksDto && bool operator ==(Object other) => identical(this, other) || other is MachineLearningAvailabilityChecksDto &&
@@ -63,8 +59,8 @@ class MachineLearningAvailabilityChecksDto {
return MachineLearningAvailabilityChecksDto( return MachineLearningAvailabilityChecksDto(
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
interval: mapValueOfType<int>(json, r'interval')!, interval: num.parse('${json[r'interval']}'),
timeout: mapValueOfType<int>(json, r'timeout')!, timeout: num.parse('${json[r'timeout']}'),
); );
} }
return null; return null;
@@ -20,10 +20,7 @@ class MaintenanceDetectInstallStorageFolderDto {
}); });
/// Number of files in the folder /// Number of files in the folder
/// num files;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int files;
StorageFolder folder; StorageFolder folder;
@@ -69,7 +66,7 @@ class MaintenanceDetectInstallStorageFolderDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return MaintenanceDetectInstallStorageFolderDto( return MaintenanceDetectInstallStorageFolderDto(
files: mapValueOfType<int>(json, r'files')!, files: num.parse('${json[r'files']}'),
folder: StorageFolder.fromJson(json[r'folder'])!, folder: StorageFolder.fromJson(json[r'folder'])!,
readable: mapValueOfType<bool>(json, r'readable')!, readable: mapValueOfType<bool>(json, r'readable')!,
writable: mapValueOfType<bool>(json, r'writable')!, writable: mapValueOfType<bool>(json, r'writable')!,
@@ -32,15 +32,13 @@ class MaintenanceStatusResponseDto {
/// ///
String? error; String? error;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// 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 /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? progress; num? progress;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@@ -104,7 +102,7 @@ class MaintenanceStatusResponseDto {
action: MaintenanceAction.fromJson(json[r'action'])!, action: MaintenanceAction.fromJson(json[r'action'])!,
active: mapValueOfType<bool>(json, r'active')!, active: mapValueOfType<bool>(json, r'active')!,
error: mapValueOfType<String>(json, r'error'), error: mapValueOfType<String>(json, r'error'),
progress: mapValueOfType<int>(json, r'progress'), progress: num.parse('${json[r'progress']}'),
task: mapValueOfType<String>(json, r'task'), task: mapValueOfType<String>(json, r'task'),
); );
} }
+8 -7
View File
@@ -215,14 +215,13 @@ class MetadataSearchDto {
/// Page number /// Page number
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// 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 /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? page; num? page;
/// Filter by person IDs /// Filter by person IDs
List<String> personIds; List<String> personIds;
@@ -240,7 +239,7 @@ class MetadataSearchDto {
/// ///
/// Minimum value: -1 /// Minimum value: -1
/// Maximum value: 5 /// Maximum value: 5
int? rating; num? rating;
/// Number of results to return /// Number of results to return
/// ///
@@ -252,7 +251,7 @@ class MetadataSearchDto {
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? size; num? size;
/// Filter by state/province name /// Filter by state/province name
String? state; String? state;
@@ -725,13 +724,15 @@ class MetadataSearchDto {
order: AssetOrder.fromJson(json[r'order']), order: AssetOrder.fromJson(json[r'order']),
originalFileName: mapValueOfType<String>(json, r'originalFileName'), originalFileName: mapValueOfType<String>(json, r'originalFileName'),
originalPath: mapValueOfType<String>(json, r'originalPath'), originalPath: mapValueOfType<String>(json, r'originalPath'),
page: mapValueOfType<int>(json, r'page'), page: num.parse('${json[r'page']}'),
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
previewPath: mapValueOfType<String>(json, r'previewPath'), previewPath: mapValueOfType<String>(json, r'previewPath'),
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
size: mapValueOfType<int>(json, r'size'), ? null
: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
+6 -4
View File
@@ -147,7 +147,7 @@ class RandomSearchDto {
/// ///
/// Minimum value: -1 /// Minimum value: -1
/// Maximum value: 5 /// Maximum value: 5
int? rating; num? rating;
/// Number of results to return /// Number of results to return
/// ///
@@ -159,7 +159,7 @@ class RandomSearchDto {
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? size; num? size;
/// Filter by state/province name /// Filter by state/province name
String? state; String? state;
@@ -549,8 +549,10 @@ class RandomSearchDto {
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
size: mapValueOfType<int>(json, r'size'), ? null
: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
+2 -3
View File
@@ -39,14 +39,13 @@ class SessionCreateDto {
/// Session duration in seconds /// Session duration in seconds
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// 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 /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? duration; num? duration;
@override @override
bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto && bool operator ==(Object other) => identical(this, other) || other is SessionCreateDto &&
@@ -95,7 +94,7 @@ class SessionCreateDto {
return SessionCreateDto( return SessionCreateDto(
deviceOS: mapValueOfType<String>(json, r'deviceOS'), deviceOS: mapValueOfType<String>(json, r'deviceOS'),
deviceType: mapValueOfType<String>(json, r'deviceType'), deviceType: mapValueOfType<String>(json, r'deviceType'),
duration: mapValueOfType<int>(json, r'duration'), duration: num.parse('${json[r'duration']}'),
); );
} }
return null; return null;
+8 -7
View File
@@ -154,14 +154,13 @@ class SmartSearchDto {
/// Page number /// Page number
/// ///
/// Minimum value: 1 /// Minimum value: 1
/// Maximum value: 9007199254740991
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// 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 /// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? page; num? page;
/// Filter by person IDs /// Filter by person IDs
List<String> personIds; List<String> personIds;
@@ -188,7 +187,7 @@ class SmartSearchDto {
/// ///
/// Minimum value: -1 /// Minimum value: -1
/// Maximum value: 5 /// Maximum value: 5
int? rating; num? rating;
/// Number of results to return /// Number of results to return
/// ///
@@ -200,7 +199,7 @@ class SmartSearchDto {
/// source code must fall back to having a nullable type. /// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note. /// Consider adding a "default:" property in the specification file to hide this note.
/// ///
int? size; num? size;
/// Filter by state/province name /// Filter by state/province name
String? state; String? state;
@@ -584,14 +583,16 @@ class SmartSearchDto {
make: mapValueOfType<String>(json, r'make'), make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'), model: mapValueOfType<String>(json, r'model'),
ocr: mapValueOfType<String>(json, r'ocr'), ocr: mapValueOfType<String>(json, r'ocr'),
page: mapValueOfType<int>(json, r'page'), page: num.parse('${json[r'page']}'),
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
query: mapValueOfType<String>(json, r'query'), query: mapValueOfType<String>(json, r'query'),
queryAssetId: mapValueOfType<String>(json, r'queryAssetId'), queryAssetId: mapValueOfType<String>(json, r'queryAssetId'),
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
size: mapValueOfType<int>(json, r'size'), ? null
: num.parse('${json[r'rating']}'),
size: num.parse('${json[r'size']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
+4 -2
View File
@@ -152,7 +152,7 @@ class StatisticsSearchDto {
/// ///
/// Minimum value: -1 /// Minimum value: -1
/// Maximum value: 5 /// Maximum value: 5
int? rating; num? rating;
/// Filter by state/province name /// Filter by state/province name
String? state; String? state;
@@ -479,7 +479,9 @@ class StatisticsSearchDto {
personIds: json[r'personIds'] is Iterable personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
rating: mapValueOfType<int>(json, r'rating'), rating: json[r'rating'] == null
? null
: num.parse('${json[r'rating']}'),
state: mapValueOfType<String>(json, r'state'), state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false) ? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
+10 -1
View File
@@ -14,6 +14,7 @@ class SyncAlbumUserV1 {
/// Returns a new [SyncAlbumUserV1] instance. /// Returns a new [SyncAlbumUserV1] instance.
SyncAlbumUserV1({ SyncAlbumUserV1({
required this.albumId, required this.albumId,
required this.isFavorite,
required this.role, required this.role,
required this.userId, required this.userId,
}); });
@@ -21,6 +22,9 @@ class SyncAlbumUserV1 {
/// Album ID /// Album ID
String albumId; String albumId;
/// Favorite flag
bool isFavorite;
AlbumUserRole role; AlbumUserRole role;
/// User ID /// User ID
@@ -29,6 +33,7 @@ class SyncAlbumUserV1 {
@override @override
bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserV1 && bool operator ==(Object other) => identical(this, other) || other is SyncAlbumUserV1 &&
other.albumId == albumId && other.albumId == albumId &&
other.isFavorite == isFavorite &&
other.role == role && other.role == role &&
other.userId == userId; other.userId == userId;
@@ -36,15 +41,17 @@ class SyncAlbumUserV1 {
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(albumId.hashCode) + (albumId.hashCode) +
(isFavorite.hashCode) +
(role.hashCode) + (role.hashCode) +
(userId.hashCode); (userId.hashCode);
@override @override
String toString() => 'SyncAlbumUserV1[albumId=$albumId, role=$role, userId=$userId]'; String toString() => 'SyncAlbumUserV1[albumId=$albumId, isFavorite=$isFavorite, role=$role, userId=$userId]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'albumId'] = this.albumId; json[r'albumId'] = this.albumId;
json[r'isFavorite'] = this.isFavorite;
json[r'role'] = this.role; json[r'role'] = this.role;
json[r'userId'] = this.userId; json[r'userId'] = this.userId;
return json; return json;
@@ -60,6 +67,7 @@ class SyncAlbumUserV1 {
return SyncAlbumUserV1( return SyncAlbumUserV1(
albumId: mapValueOfType<String>(json, r'albumId')!, albumId: mapValueOfType<String>(json, r'albumId')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
role: AlbumUserRole.fromJson(json[r'role'])!, role: AlbumUserRole.fromJson(json[r'role'])!,
userId: mapValueOfType<String>(json, r'userId')!, userId: mapValueOfType<String>(json, r'userId')!,
); );
@@ -110,6 +118,7 @@ class SyncAlbumUserV1 {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'albumId', 'albumId',
'isFavorite',
'role', 'role',
'userId', 'userId',
}; };
-321
View File
@@ -1,321 +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 SyncAssetV2 {
/// Returns a new [SyncAssetV2] instance.
SyncAssetV2({
required this.checksum,
required this.deletedAt,
required this.duration,
required this.fileCreatedAt,
required this.fileModifiedAt,
required this.height,
required this.id,
required this.isEdited,
required this.isFavorite,
required this.libraryId,
required this.livePhotoVideoId,
required this.localDateTime,
required this.originalFileName,
required this.ownerId,
required this.stackId,
required this.thumbhash,
required this.type,
required this.visibility,
required this.width,
});
/// Checksum
String checksum;
/// Deleted at
DateTime? deletedAt;
/// Duration
///
/// Minimum value: 0
/// Maximum value: 2147483647
int? duration;
/// File created at
DateTime? fileCreatedAt;
/// File modified at
DateTime? fileModifiedAt;
/// Asset height
///
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int? height;
/// Asset ID
String id;
/// Is edited
bool isEdited;
/// Is favorite
bool isFavorite;
/// Library ID
String? libraryId;
/// Live photo video ID
String? livePhotoVideoId;
/// Local date time
DateTime? localDateTime;
/// Original file name
String originalFileName;
/// Owner ID
String ownerId;
/// Stack ID
String? stackId;
/// Thumbhash
String? thumbhash;
AssetTypeEnum type;
AssetVisibility visibility;
/// Asset width
///
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int? width;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV2 &&
other.checksum == checksum &&
other.deletedAt == deletedAt &&
other.duration == duration &&
other.fileCreatedAt == fileCreatedAt &&
other.fileModifiedAt == fileModifiedAt &&
other.height == height &&
other.id == id &&
other.isEdited == isEdited &&
other.isFavorite == isFavorite &&
other.libraryId == libraryId &&
other.livePhotoVideoId == livePhotoVideoId &&
other.localDateTime == localDateTime &&
other.originalFileName == originalFileName &&
other.ownerId == ownerId &&
other.stackId == stackId &&
other.thumbhash == thumbhash &&
other.type == type &&
other.visibility == visibility &&
other.width == width;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(checksum.hashCode) +
(deletedAt == null ? 0 : deletedAt!.hashCode) +
(duration == null ? 0 : duration!.hashCode) +
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
(height == null ? 0 : height!.hashCode) +
(id.hashCode) +
(isEdited.hashCode) +
(isFavorite.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(localDateTime == null ? 0 : localDateTime!.hashCode) +
(originalFileName.hashCode) +
(ownerId.hashCode) +
(stackId == null ? 0 : stackId!.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) +
(type.hashCode) +
(visibility.hashCode) +
(width == null ? 0 : width!.hashCode);
@override
String toString() => 'SyncAssetV2[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isEdited=$isEdited, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'checksum'] = this.checksum;
if (this.deletedAt != null) {
json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.deletedAt!.millisecondsSinceEpoch
: this.deletedAt!.toUtc().toIso8601String();
} else {
// json[r'deletedAt'] = null;
}
if (this.duration != null) {
json[r'duration'] = this.duration;
} else {
// json[r'duration'] = null;
}
if (this.fileCreatedAt != null) {
json[r'fileCreatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.fileCreatedAt!.millisecondsSinceEpoch
: this.fileCreatedAt!.toUtc().toIso8601String();
} else {
// json[r'fileCreatedAt'] = null;
}
if (this.fileModifiedAt != null) {
json[r'fileModifiedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.fileModifiedAt!.millisecondsSinceEpoch
: this.fileModifiedAt!.toUtc().toIso8601String();
} else {
// json[r'fileModifiedAt'] = null;
}
if (this.height != null) {
json[r'height'] = this.height;
} else {
// json[r'height'] = null;
}
json[r'id'] = this.id;
json[r'isEdited'] = this.isEdited;
json[r'isFavorite'] = this.isFavorite;
if (this.libraryId != null) {
json[r'libraryId'] = this.libraryId;
} else {
// json[r'libraryId'] = null;
}
if (this.livePhotoVideoId != null) {
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
} else {
// json[r'livePhotoVideoId'] = null;
}
if (this.localDateTime != null) {
json[r'localDateTime'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
? this.localDateTime!.millisecondsSinceEpoch
: this.localDateTime!.toUtc().toIso8601String();
} else {
// json[r'localDateTime'] = null;
}
json[r'originalFileName'] = this.originalFileName;
json[r'ownerId'] = this.ownerId;
if (this.stackId != null) {
json[r'stackId'] = this.stackId;
} else {
// json[r'stackId'] = null;
}
if (this.thumbhash != null) {
json[r'thumbhash'] = this.thumbhash;
} else {
// json[r'thumbhash'] = null;
}
json[r'type'] = this.type;
json[r'visibility'] = this.visibility;
if (this.width != null) {
json[r'width'] = this.width;
} else {
// json[r'width'] = null;
}
return json;
}
/// Returns a new [SyncAssetV2] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SyncAssetV2? fromJson(dynamic value) {
upgradeDto(value, "SyncAssetV2");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SyncAssetV2(
checksum: mapValueOfType<String>(json, r'checksum')!,
deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
duration: mapValueOfType<int>(json, r'duration'),
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
height: mapValueOfType<int>(json, r'height'),
id: mapValueOfType<String>(json, r'id')!,
isEdited: mapValueOfType<bool>(json, r'isEdited')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
libraryId: mapValueOfType<String>(json, r'libraryId'),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
localDateTime: mapDateTime(json, r'localDateTime', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
ownerId: mapValueOfType<String>(json, r'ownerId')!,
stackId: mapValueOfType<String>(json, r'stackId'),
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: AssetTypeEnum.fromJson(json[r'type'])!,
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
width: mapValueOfType<int>(json, r'width'),
);
}
return null;
}
static List<SyncAssetV2> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAssetV2>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAssetV2.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SyncAssetV2> mapFromJson(dynamic json) {
final map = <String, SyncAssetV2>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SyncAssetV2.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SyncAssetV2-objects as value to a dart map
static Map<String, List<SyncAssetV2>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SyncAssetV2>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SyncAssetV2.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'checksum',
'deletedAt',
'duration',
'fileCreatedAt',
'fileModifiedAt',
'height',
'id',
'isEdited',
'isFavorite',
'libraryId',
'livePhotoVideoId',
'localDateTime',
'originalFileName',
'ownerId',
'stackId',
'thumbhash',
'type',
'visibility',
'width',
};
}
-18
View File
@@ -27,7 +27,6 @@ class SyncEntityType {
static const userV1 = SyncEntityType._(r'UserV1'); static const userV1 = SyncEntityType._(r'UserV1');
static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1'); static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1');
static const assetV1 = SyncEntityType._(r'AssetV1'); static const assetV1 = SyncEntityType._(r'AssetV1');
static const assetV2 = SyncEntityType._(r'AssetV2');
static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1'); static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1');
static const assetExifV1 = SyncEntityType._(r'AssetExifV1'); static const assetExifV1 = SyncEntityType._(r'AssetExifV1');
static const assetEditV1 = SyncEntityType._(r'AssetEditV1'); static const assetEditV1 = SyncEntityType._(r'AssetEditV1');
@@ -37,9 +36,7 @@ class SyncEntityType {
static const partnerV1 = SyncEntityType._(r'PartnerV1'); static const partnerV1 = SyncEntityType._(r'PartnerV1');
static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1');
static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1'); static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1');
static const partnerAssetV2 = SyncEntityType._(r'PartnerAssetV2');
static const partnerAssetBackfillV1 = SyncEntityType._(r'PartnerAssetBackfillV1'); static const partnerAssetBackfillV1 = SyncEntityType._(r'PartnerAssetBackfillV1');
static const partnerAssetBackfillV2 = SyncEntityType._(r'PartnerAssetBackfillV2');
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1'); static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1'); static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1'); static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1');
@@ -53,11 +50,8 @@ class SyncEntityType {
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1'); static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1'); static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
static const albumAssetCreateV1 = SyncEntityType._(r'AlbumAssetCreateV1'); static const albumAssetCreateV1 = SyncEntityType._(r'AlbumAssetCreateV1');
static const albumAssetCreateV2 = SyncEntityType._(r'AlbumAssetCreateV2');
static const albumAssetUpdateV1 = SyncEntityType._(r'AlbumAssetUpdateV1'); static const albumAssetUpdateV1 = SyncEntityType._(r'AlbumAssetUpdateV1');
static const albumAssetUpdateV2 = SyncEntityType._(r'AlbumAssetUpdateV2');
static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1'); static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1');
static const albumAssetBackfillV2 = SyncEntityType._(r'AlbumAssetBackfillV2');
static const albumAssetExifCreateV1 = SyncEntityType._(r'AlbumAssetExifCreateV1'); static const albumAssetExifCreateV1 = SyncEntityType._(r'AlbumAssetExifCreateV1');
static const albumAssetExifUpdateV1 = SyncEntityType._(r'AlbumAssetExifUpdateV1'); static const albumAssetExifUpdateV1 = SyncEntityType._(r'AlbumAssetExifUpdateV1');
static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1'); static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1');
@@ -87,7 +81,6 @@ class SyncEntityType {
userV1, userV1,
userDeleteV1, userDeleteV1,
assetV1, assetV1,
assetV2,
assetDeleteV1, assetDeleteV1,
assetExifV1, assetExifV1,
assetEditV1, assetEditV1,
@@ -97,9 +90,7 @@ class SyncEntityType {
partnerV1, partnerV1,
partnerDeleteV1, partnerDeleteV1,
partnerAssetV1, partnerAssetV1,
partnerAssetV2,
partnerAssetBackfillV1, partnerAssetBackfillV1,
partnerAssetBackfillV2,
partnerAssetDeleteV1, partnerAssetDeleteV1,
partnerAssetExifV1, partnerAssetExifV1,
partnerAssetExifBackfillV1, partnerAssetExifBackfillV1,
@@ -113,11 +104,8 @@ class SyncEntityType {
albumUserBackfillV1, albumUserBackfillV1,
albumUserDeleteV1, albumUserDeleteV1,
albumAssetCreateV1, albumAssetCreateV1,
albumAssetCreateV2,
albumAssetUpdateV1, albumAssetUpdateV1,
albumAssetUpdateV2,
albumAssetBackfillV1, albumAssetBackfillV1,
albumAssetBackfillV2,
albumAssetExifCreateV1, albumAssetExifCreateV1,
albumAssetExifUpdateV1, albumAssetExifUpdateV1,
albumAssetExifBackfillV1, albumAssetExifBackfillV1,
@@ -182,7 +170,6 @@ class SyncEntityTypeTypeTransformer {
case r'UserV1': return SyncEntityType.userV1; case r'UserV1': return SyncEntityType.userV1;
case r'UserDeleteV1': return SyncEntityType.userDeleteV1; case r'UserDeleteV1': return SyncEntityType.userDeleteV1;
case r'AssetV1': return SyncEntityType.assetV1; case r'AssetV1': return SyncEntityType.assetV1;
case r'AssetV2': return SyncEntityType.assetV2;
case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1; case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1;
case r'AssetExifV1': return SyncEntityType.assetExifV1; case r'AssetExifV1': return SyncEntityType.assetExifV1;
case r'AssetEditV1': return SyncEntityType.assetEditV1; case r'AssetEditV1': return SyncEntityType.assetEditV1;
@@ -192,9 +179,7 @@ class SyncEntityTypeTypeTransformer {
case r'PartnerV1': return SyncEntityType.partnerV1; case r'PartnerV1': return SyncEntityType.partnerV1;
case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1;
case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1; case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1;
case r'PartnerAssetV2': return SyncEntityType.partnerAssetV2;
case r'PartnerAssetBackfillV1': return SyncEntityType.partnerAssetBackfillV1; case r'PartnerAssetBackfillV1': return SyncEntityType.partnerAssetBackfillV1;
case r'PartnerAssetBackfillV2': return SyncEntityType.partnerAssetBackfillV2;
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1; case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1; case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1; case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1;
@@ -208,11 +193,8 @@ class SyncEntityTypeTypeTransformer {
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1; case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1; case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
case r'AlbumAssetCreateV1': return SyncEntityType.albumAssetCreateV1; case r'AlbumAssetCreateV1': return SyncEntityType.albumAssetCreateV1;
case r'AlbumAssetCreateV2': return SyncEntityType.albumAssetCreateV2;
case r'AlbumAssetUpdateV1': return SyncEntityType.albumAssetUpdateV1; case r'AlbumAssetUpdateV1': return SyncEntityType.albumAssetUpdateV1;
case r'AlbumAssetUpdateV2': return SyncEntityType.albumAssetUpdateV2;
case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1; case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1;
case r'AlbumAssetBackfillV2': return SyncEntityType.albumAssetBackfillV2;
case r'AlbumAssetExifCreateV1': return SyncEntityType.albumAssetExifCreateV1; case r'AlbumAssetExifCreateV1': return SyncEntityType.albumAssetExifCreateV1;
case r'AlbumAssetExifUpdateV1': return SyncEntityType.albumAssetExifUpdateV1; case r'AlbumAssetExifUpdateV1': return SyncEntityType.albumAssetExifUpdateV1;
case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1; case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1;
-9
View File
@@ -28,10 +28,8 @@ class SyncRequestType {
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1'); static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1'); static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1'); static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
static const albumAssetsV2 = SyncRequestType._(r'AlbumAssetsV2');
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1'); static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
static const assetsV1 = SyncRequestType._(r'AssetsV1'); static const assetsV1 = SyncRequestType._(r'AssetsV1');
static const assetsV2 = SyncRequestType._(r'AssetsV2');
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1'); static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1'); static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1');
static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1'); static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1');
@@ -40,7 +38,6 @@ class SyncRequestType {
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1'); static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
static const partnersV1 = SyncRequestType._(r'PartnersV1'); static const partnersV1 = SyncRequestType._(r'PartnersV1');
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1'); static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
static const partnerAssetsV2 = SyncRequestType._(r'PartnerAssetsV2');
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1'); static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1'); static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1');
static const stacksV1 = SyncRequestType._(r'StacksV1'); static const stacksV1 = SyncRequestType._(r'StacksV1');
@@ -57,10 +54,8 @@ class SyncRequestType {
albumUsersV1, albumUsersV1,
albumToAssetsV1, albumToAssetsV1,
albumAssetsV1, albumAssetsV1,
albumAssetsV2,
albumAssetExifsV1, albumAssetExifsV1,
assetsV1, assetsV1,
assetsV2,
assetExifsV1, assetExifsV1,
assetEditsV1, assetEditsV1,
assetMetadataV1, assetMetadataV1,
@@ -69,7 +64,6 @@ class SyncRequestType {
memoryToAssetsV1, memoryToAssetsV1,
partnersV1, partnersV1,
partnerAssetsV1, partnerAssetsV1,
partnerAssetsV2,
partnerAssetExifsV1, partnerAssetExifsV1,
partnerStacksV1, partnerStacksV1,
stacksV1, stacksV1,
@@ -121,10 +115,8 @@ class SyncRequestTypeTypeTransformer {
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1; case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1; case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1; case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
case r'AlbumAssetsV2': return SyncRequestType.albumAssetsV2;
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1; case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
case r'AssetsV1': return SyncRequestType.assetsV1; case r'AssetsV1': return SyncRequestType.assetsV1;
case r'AssetsV2': return SyncRequestType.assetsV2;
case r'AssetExifsV1': return SyncRequestType.assetExifsV1; case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
case r'AssetEditsV1': return SyncRequestType.assetEditsV1; case r'AssetEditsV1': return SyncRequestType.assetEditsV1;
case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1; case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1;
@@ -133,7 +125,6 @@ class SyncRequestTypeTypeTransformer {
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1; case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
case r'PartnersV1': return SyncRequestType.partnersV1; case r'PartnersV1': return SyncRequestType.partnersV1;
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1; case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
case r'PartnerAssetsV2': return SyncRequestType.partnerAssetsV2;
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1; case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1; case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1;
case r'StacksV1': return SyncRequestType.stacksV1; case r'StacksV1': return SyncRequestType.stacksV1;
+4 -3
View File
@@ -57,8 +57,7 @@ class SystemConfigOAuthDto {
/// Default storage quota /// Default storage quota
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 9007199254740991 num? defaultStorageQuota;
int? defaultStorageQuota;
/// Enabled /// Enabled
bool enabled; bool enabled;
@@ -201,7 +200,9 @@ class SystemConfigOAuthDto {
buttonText: mapValueOfType<String>(json, r'buttonText')!, buttonText: mapValueOfType<String>(json, r'buttonText')!,
clientId: mapValueOfType<String>(json, r'clientId')!, clientId: mapValueOfType<String>(json, r'clientId')!,
clientSecret: mapValueOfType<String>(json, r'clientSecret')!, clientSecret: mapValueOfType<String>(json, r'clientSecret')!,
defaultStorageQuota: mapValueOfType<int>(json, r'defaultStorageQuota'), defaultStorageQuota: json[r'defaultStorageQuota'] == null
? null
: num.parse('${json[r'defaultStorageQuota']}'),
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
endSessionEndpoint: mapValueOfType<String>(json, r'endSessionEndpoint')!, endSessionEndpoint: mapValueOfType<String>(json, r'endSessionEndpoint')!,
issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!, issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
@@ -34,7 +34,7 @@ class SystemConfigSmtpTransportDto {
/// ///
/// Minimum value: 0 /// Minimum value: 0
/// Maximum value: 65535 /// Maximum value: 65535
int port; num port;
/// Whether to use secure connection (TLS/SSL) /// Whether to use secure connection (TLS/SSL)
bool secure; bool secure;
@@ -87,7 +87,7 @@ class SystemConfigSmtpTransportDto {
host: mapValueOfType<String>(json, r'host')!, host: mapValueOfType<String>(json, r'host')!,
ignoreCert: mapValueOfType<bool>(json, r'ignoreCert')!, ignoreCert: mapValueOfType<bool>(json, r'ignoreCert')!,
password: mapValueOfType<String>(json, r'password')!, password: mapValueOfType<String>(json, r'password')!,
port: mapValueOfType<int>(json, r'port')!, port: num.parse('${json[r'port']}'),
secure: mapValueOfType<bool>(json, r'secure')!, secure: mapValueOfType<bool>(json, r'secure')!,
username: mapValueOfType<String>(json, r'username')!, username: mapValueOfType<String>(json, r'username')!,
); );
@@ -39,8 +39,8 @@ class TimeBucketAssetResponseDto {
/// Array of country names extracted from EXIF GPS data /// Array of country names extracted from EXIF GPS data
List<String?> country; List<String?> country;
/// Array of video/gif durations in milliseconds (null for static images) /// Array of video/gif durations in hh:mm:ss.SSS format (null for static images)
List<int?> duration; List<String?> duration;
/// Array of file creation timestamps in UTC /// Array of file creation timestamps in UTC
List<String> fileCreatedAt; List<String> fileCreatedAt;
@@ -172,7 +172,7 @@ class TimeBucketAssetResponseDto {
? (json[r'country'] as Iterable).cast<String>().toList(growable: false) ? (json[r'country'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
duration: json[r'duration'] is Iterable duration: json[r'duration'] is Iterable
? (json[r'duration'] as Iterable).cast<int>().toList(growable: false) ? (json[r'duration'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
fileCreatedAt: json[r'fileCreatedAt'] is Iterable fileCreatedAt: json[r'fileCreatedAt'] is Iterable
? (json[r'fileCreatedAt'] as Iterable).cast<String>().toList(growable: false) ? (json[r'fileCreatedAt'] as Iterable).cast<String>().toList(growable: false)
+33 -6
View File
@@ -13,26 +13,53 @@ part of openapi.api;
class UpdateAlbumUserDto { class UpdateAlbumUserDto {
/// Returns a new [UpdateAlbumUserDto] instance. /// Returns a new [UpdateAlbumUserDto] instance.
UpdateAlbumUserDto({ UpdateAlbumUserDto({
required this.role, this.isFavorite,
this.role,
}); });
AlbumUserRole role; /// Mark album as favorite for the user (only the user themselves can update)
///
/// 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? isFavorite;
///
/// 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.
///
AlbumUserRole? role;
@override @override
bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumUserDto && bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumUserDto &&
other.isFavorite == isFavorite &&
other.role == role; other.role == role;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(role.hashCode); (isFavorite == null ? 0 : isFavorite!.hashCode) +
(role == null ? 0 : role!.hashCode);
@override @override
String toString() => 'UpdateAlbumUserDto[role=$role]'; String toString() => 'UpdateAlbumUserDto[isFavorite=$isFavorite, role=$role]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.isFavorite != null) {
json[r'isFavorite'] = this.isFavorite;
} else {
// json[r'isFavorite'] = null;
}
if (this.role != null) {
json[r'role'] = this.role; json[r'role'] = this.role;
} else {
// json[r'role'] = null;
}
return json; return json;
} }
@@ -45,7 +72,8 @@ class UpdateAlbumUserDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UpdateAlbumUserDto( return UpdateAlbumUserDto(
role: AlbumUserRole.fromJson(json[r'role'])!, isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
role: AlbumUserRole.fromJson(json[r'role']),
); );
} }
return null; return null;
@@ -93,7 +121,6 @@ class UpdateAlbumUserDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'role',
}; };
} }
+2 -5
View File
@@ -26,10 +26,7 @@ class WorkflowActionResponseDto {
String id; String id;
/// Action order /// Action order
/// num order;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int order;
/// Plugin action ID /// Plugin action ID
String pluginActionId; String pluginActionId;
@@ -82,7 +79,7 @@ class WorkflowActionResponseDto {
return WorkflowActionResponseDto( return WorkflowActionResponseDto(
actionConfig: mapCastOfType<String, Object>(json, r'actionConfig'), actionConfig: mapCastOfType<String, Object>(json, r'actionConfig'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
order: mapValueOfType<int>(json, r'order')!, order: num.parse('${json[r'order']}'),
pluginActionId: mapValueOfType<String>(json, r'pluginActionId')!, pluginActionId: mapValueOfType<String>(json, r'pluginActionId')!,
workflowId: mapValueOfType<String>(json, r'workflowId')!, workflowId: mapValueOfType<String>(json, r'workflowId')!,
); );
+2 -5
View File
@@ -26,10 +26,7 @@ class WorkflowFilterResponseDto {
String id; String id;
/// Filter order /// Filter order
/// num order;
/// Minimum value: -9007199254740991
/// Maximum value: 9007199254740991
int order;
/// Plugin filter ID /// Plugin filter ID
String pluginFilterId; String pluginFilterId;
@@ -82,7 +79,7 @@ class WorkflowFilterResponseDto {
return WorkflowFilterResponseDto( return WorkflowFilterResponseDto(
filterConfig: mapCastOfType<String, Object>(json, r'filterConfig'), filterConfig: mapCastOfType<String, Object>(json, r'filterConfig'),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
order: mapValueOfType<int>(json, r'order')!, order: num.parse('${json[r'order']}'),
pluginFilterId: mapValueOfType<String>(json, r'pluginFilterId')!, pluginFilterId: mapValueOfType<String>(json, r'pluginFilterId')!,
workflowId: mapValueOfType<String>(json, r'workflowId')!, workflowId: mapValueOfType<String>(json, r'workflowId')!,
); );
+1 -1
View File
@@ -1989,4 +1989,4 @@ packages:
version: "3.1.3" version: "3.1.3"
sdks: sdks:
dart: ">=3.11.0 <4.0.0" dart: ">=3.11.0 <4.0.0"
flutter: "3.41.9" flutter: "3.41.7"
+1 -1
View File
@@ -6,7 +6,7 @@ version: 2.7.5+3046
environment: environment:
sdk: '>=3.11.0 <4.0.0' sdk: '>=3.11.0 <4.0.0'
flutter: 3.41.9 flutter: 3.41.7
dependencies: dependencies:
async: ^2.13.1 async: ^2.13.1
@@ -419,8 +419,8 @@ void main() {
'album-b': [mergedAsset], 'album-b': [mergedAsset],
}; };
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async { when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async {
final Iterable<String> requestedRemoteIds = invocation.positionalArguments.first as Iterable<String>; final Iterable<String> requestedChecksums = invocation.positionalArguments.first as Iterable<String>;
expect(requestedRemoteIds.toSet(), equals({'remote-1', 'remote-2', 'remote-3'})); expect(requestedChecksums.toSet(), equals({'checksum-local', 'checksum-merged', 'checksum-remote-only'}));
return assetsByAlbum; return assetsByAlbum;
}); });
@@ -482,18 +482,12 @@ void main() {
verifyNever(() => mockTrashedLocalAssetRepo.trashLocalAsset(any())); verifyNever(() => mockTrashedLocalAssetRepo.trashLocalAsset(any()));
}); });
test("requests local deletions lookup by remote ids for permanent remote delete events", () async { test("does not request local deletions for permanent remote delete events", () async {
when(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).thenAnswer((invocation) async {
final Iterable<String> requestedRemoteIds = invocation.positionalArguments.first as Iterable<String>;
expect(requestedRemoteIds.toSet(), equals({'remote-asset'}));
return {};
});
final events = [SyncStreamStub.assetDeleteV1]; final events = [SyncStreamStub.assetDeleteV1];
await simulateEvents(events); await simulateEvents(events);
verify(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any())).called(1); verifyNever(() => mockLocalAssetRepo.getAssetsFromBackupAlbums(any()));
verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any())); verifyNever(() => mockLocalFilesManagerRepo.moveToTrash(any()));
verify(() => mockSyncStreamRepo.deleteAssetsV1(any())).called(1); verify(() => mockSyncStreamRepo.deleteAssetsV1(any())).called(1);
}); });
+66 -224
View File
@@ -1648,6 +1648,15 @@
"type": "string" "type": "string"
} }
}, },
{
"name": "favorite",
"required": false,
"in": "query",
"description": "Filter to only albums favorited by the authenticated user",
"schema": {
"type": "boolean"
}
},
{ {
"name": "shared", "name": "shared",
"required": false, "required": false,
@@ -5172,7 +5181,7 @@
}, },
"/duplicates/{id}": { "/duplicates/{id}": {
"delete": { "delete": {
"description": "Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.", "description": "Delete a single duplicate asset specified by its ID.",
"operationId": "deleteDuplicate", "operationId": "deleteDuplicate",
"parameters": [ "parameters": [
{ {
@@ -5202,7 +5211,7 @@
"api_key": [] "api_key": []
} }
], ],
"summary": "Dismiss a duplicate group", "summary": "Delete a duplicate",
"tags": [ "tags": [
"Duplicates" "Duplicates"
], ],
@@ -7964,9 +7973,8 @@
"description": "Page number for pagination", "description": "Page number for pagination",
"schema": { "schema": {
"minimum": 1, "minimum": 1,
"maximum": 9007199254740991,
"default": 1, "default": 1,
"type": "integer" "type": "number"
} }
}, },
{ {
@@ -7978,7 +7986,7 @@
"minimum": 1, "minimum": 1,
"maximum": 1000, "maximum": 1000,
"default": 500, "default": 500,
"type": "integer" "type": "number"
} }
}, },
{ {
@@ -9373,7 +9381,7 @@
], ],
"x-immich-state": "Stable", "x-immich-state": "Stable",
"schema": { "schema": {
"type": "integer", "type": "number",
"minimum": -1, "minimum": -1,
"maximum": 5, "maximum": 5,
"nullable": true "nullable": true
@@ -9387,7 +9395,7 @@
"schema": { "schema": {
"minimum": 1, "minimum": 1,
"maximum": 1000, "maximum": 1000,
"type": "integer" "type": "number"
} }
}, },
{ {
@@ -15325,6 +15333,10 @@
"description": "Activity feed enabled", "description": "Activity feed enabled",
"type": "boolean" "type": "boolean"
}, },
"isFavorite": {
"description": "Whether the authenticated user has favorited this album",
"type": "boolean"
},
"lastModifiedAssetTimestamp": { "lastModifiedAssetTimestamp": {
"description": "Last modified asset timestamp", "description": "Last modified asset timestamp",
"format": "date-time", "format": "date-time",
@@ -15358,6 +15370,7 @@
"hasSharedLink", "hasSharedLink",
"id", "id",
"isActivityEnabled", "isActivityEnabled",
"isFavorite",
"shared", "shared",
"updatedAt" "updatedAt"
], ],
@@ -15637,9 +15650,7 @@
}, },
"dateTimeRelative": { "dateTimeRelative": {
"description": "Relative time offset in seconds", "description": "Relative time offset in seconds",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"description": { "description": {
"description": "Asset description", "description": "Asset description",
@@ -16261,10 +16272,8 @@
"type": "string" "type": "string"
}, },
"duration": { "duration": {
"description": "Duration in milliseconds (for videos)", "description": "Duration (for videos)",
"maximum": 2147483647, "type": "string"
"minimum": 0,
"type": "integer"
}, },
"fileCreatedAt": { "fileCreatedAt": {
"description": "File creation date", "description": "File creation date",
@@ -16632,11 +16641,9 @@
"type": "string" "type": "string"
}, },
"duration": { "duration": {
"description": "Video/gif duration in milliseconds (null for static images)", "description": "Video/gif duration in hh:mm:ss.SSS format (null for static images)",
"maximum": 2147483647,
"minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "string"
}, },
"exifInfo": { "exifInfo": {
"$ref": "#/components/schemas/ExifResponseDto" "$ref": "#/components/schemas/ExifResponseDto"
@@ -16657,10 +16664,9 @@
}, },
"height": { "height": {
"description": "Asset height", "description": "Asset height",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"id": { "id": {
"description": "Asset ID", "description": "Asset ID",
@@ -16803,10 +16809,9 @@
}, },
"width": { "width": {
"description": "Asset width", "description": "Asset width",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
} }
}, },
"required": [ "required": [
@@ -17223,27 +17228,23 @@
"properties": { "properties": {
"height": { "height": {
"description": "Height of the crop", "description": "Height of the crop",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"width": { "width": {
"description": "Width of the crop", "description": "Width of the crop",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"x": { "x": {
"description": "Top-Left X coordinate of crop", "description": "Top-Left X coordinate of crop",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"type": "integer" "type": "number"
}, },
"y": { "y": {
"description": "Top-Left Y coordinate of crop", "description": "Top-Left Y coordinate of crop",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"type": "integer" "type": "number"
} }
}, },
"required": [ "required": [
@@ -17267,9 +17268,8 @@
}, },
"keepLastAmount": { "keepLastAmount": {
"description": "Keep last amount", "description": "Keep last amount",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
} }
}, },
"required": [ "required": [
@@ -17302,9 +17302,7 @@
}, },
"filesize": { "filesize": {
"description": "Backup file size", "description": "Backup file size",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"timezone": { "timezone": {
"description": "Backup timezone", "description": "Backup timezone",
@@ -17643,18 +17641,16 @@
"exifImageHeight": { "exifImageHeight": {
"default": null, "default": null,
"description": "Image height in pixels", "description": "Image height in pixels",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"exifImageWidth": { "exifImageWidth": {
"default": null, "default": null,
"description": "Image width in pixels", "description": "Image width in pixels",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"exposureTime": { "exposureTime": {
"default": null, "default": null,
@@ -17685,10 +17681,8 @@
"iso": { "iso": {
"default": null, "default": null,
"description": "ISO sensitivity", "description": "ISO sensitivity",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"latitude": { "latitude": {
"default": null, "default": null,
@@ -17742,10 +17736,8 @@
"rating": { "rating": {
"default": null, "default": null,
"description": "Rating", "description": "Rating",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"state": { "state": {
"default": null, "default": null,
@@ -18172,14 +18164,10 @@
"type": "boolean" "type": "boolean"
}, },
"interval": { "interval": {
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"timeout": { "timeout": {
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
} }
}, },
"required": [ "required": [
@@ -18229,9 +18217,7 @@
"properties": { "properties": {
"files": { "files": {
"description": "Number of files in the folder", "description": "Number of files in the folder",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"folder": { "folder": {
"$ref": "#/components/schemas/StorageFolder" "$ref": "#/components/schemas/StorageFolder"
@@ -18274,9 +18260,7 @@
"type": "string" "type": "string"
}, },
"progress": { "progress": {
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"task": { "task": {
"type": "string" "type": "string"
@@ -18753,9 +18737,8 @@
}, },
"page": { "page": {
"description": "Page number", "description": "Page number",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"personIds": { "personIds": {
"description": "Filter by person IDs", "description": "Filter by person IDs",
@@ -18775,7 +18758,7 @@
"maximum": 5, "maximum": 5,
"minimum": -1, "minimum": -1,
"nullable": true, "nullable": true,
"type": "integer", "type": "number",
"x-immich-history": [ "x-immich-history": [
{ {
"version": "v1", "version": "v1",
@@ -18797,7 +18780,7 @@
"description": "Number of results to return", "description": "Number of results to return",
"maximum": 1000, "maximum": 1000,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"state": { "state": {
"description": "Filter by state/province name", "description": "Filter by state/province name",
@@ -20628,7 +20611,7 @@
"maximum": 5, "maximum": 5,
"minimum": -1, "minimum": -1,
"nullable": true, "nullable": true,
"type": "integer", "type": "number",
"x-immich-history": [ "x-immich-history": [
{ {
"version": "v1", "version": "v1",
@@ -20650,7 +20633,7 @@
"description": "Number of results to return", "description": "Number of results to return",
"maximum": 1000, "maximum": 1000,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"state": { "state": {
"description": "Filter by state/province name", "description": "Filter by state/province name",
@@ -21468,9 +21451,8 @@
}, },
"duration": { "duration": {
"description": "Session duration in seconds", "description": "Session duration in seconds",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
} }
}, },
"type": "object" "type": "object"
@@ -21984,9 +21966,8 @@
}, },
"page": { "page": {
"description": "Page number", "description": "Page number",
"maximum": 9007199254740991,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"personIds": { "personIds": {
"description": "Filter by person IDs", "description": "Filter by person IDs",
@@ -22012,7 +21993,7 @@
"maximum": 5, "maximum": 5,
"minimum": -1, "minimum": -1,
"nullable": true, "nullable": true,
"type": "integer", "type": "number",
"x-immich-history": [ "x-immich-history": [
{ {
"version": "v1", "version": "v1",
@@ -22034,7 +22015,7 @@
"description": "Number of results to return", "description": "Number of results to return",
"maximum": 1000, "maximum": 1000,
"minimum": 1, "minimum": 1,
"type": "integer" "type": "number"
}, },
"state": { "state": {
"description": "Filter by state/province name", "description": "Filter by state/province name",
@@ -22272,7 +22253,7 @@
"maximum": 5, "maximum": 5,
"minimum": -1, "minimum": -1,
"nullable": true, "nullable": true,
"type": "integer", "type": "number",
"x-immich-history": [ "x-immich-history": [
{ {
"version": "v1", "version": "v1",
@@ -22485,6 +22466,10 @@
"description": "Album ID", "description": "Album ID",
"type": "string" "type": "string"
}, },
"isFavorite": {
"description": "Favorite flag",
"type": "boolean"
},
"role": { "role": {
"$ref": "#/components/schemas/AlbumUserRole" "$ref": "#/components/schemas/AlbumUserRole"
}, },
@@ -22495,6 +22480,7 @@
}, },
"required": [ "required": [
"albumId", "albumId",
"isFavorite",
"role", "role",
"userId" "userId"
], ],
@@ -23179,135 +23165,6 @@
], ],
"type": "object" "type": "object"
}, },
"SyncAssetV2": {
"properties": {
"checksum": {
"description": "Checksum",
"type": "string"
},
"deletedAt": {
"description": "Deleted at",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time",
"nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string"
},
"duration": {
"description": "Duration",
"maximum": 2147483647,
"minimum": 0,
"nullable": true,
"type": "integer"
},
"fileCreatedAt": {
"description": "File created at",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time",
"nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string"
},
"fileModifiedAt": {
"description": "File modified at",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time",
"nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string"
},
"height": {
"description": "Asset height",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"nullable": true,
"type": "integer"
},
"id": {
"description": "Asset ID",
"type": "string"
},
"isEdited": {
"description": "Is edited",
"type": "boolean"
},
"isFavorite": {
"description": "Is favorite",
"type": "boolean"
},
"libraryId": {
"description": "Library ID",
"nullable": true,
"type": "string"
},
"livePhotoVideoId": {
"description": "Live photo video ID",
"nullable": true,
"type": "string"
},
"localDateTime": {
"description": "Local date time",
"example": "2024-01-01T00:00:00.000Z",
"format": "date-time",
"nullable": true,
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
"type": "string"
},
"originalFileName": {
"description": "Original file name",
"type": "string"
},
"ownerId": {
"description": "Owner ID",
"type": "string"
},
"stackId": {
"description": "Stack ID",
"nullable": true,
"type": "string"
},
"thumbhash": {
"description": "Thumbhash",
"nullable": true,
"type": "string"
},
"type": {
"$ref": "#/components/schemas/AssetTypeEnum"
},
"visibility": {
"$ref": "#/components/schemas/AssetVisibility"
},
"width": {
"description": "Asset width",
"maximum": 9007199254740991,
"minimum": -9007199254740991,
"nullable": true,
"type": "integer"
}
},
"required": [
"checksum",
"deletedAt",
"duration",
"fileCreatedAt",
"fileModifiedAt",
"height",
"id",
"isEdited",
"isFavorite",
"libraryId",
"livePhotoVideoId",
"localDateTime",
"originalFileName",
"ownerId",
"stackId",
"thumbhash",
"type",
"visibility",
"width"
],
"type": "object"
},
"SyncAuthUserV1": { "SyncAuthUserV1": {
"properties": { "properties": {
"avatarColor": { "avatarColor": {
@@ -23408,7 +23265,6 @@
"UserV1", "UserV1",
"UserDeleteV1", "UserDeleteV1",
"AssetV1", "AssetV1",
"AssetV2",
"AssetDeleteV1", "AssetDeleteV1",
"AssetExifV1", "AssetExifV1",
"AssetEditV1", "AssetEditV1",
@@ -23418,9 +23274,7 @@
"PartnerV1", "PartnerV1",
"PartnerDeleteV1", "PartnerDeleteV1",
"PartnerAssetV1", "PartnerAssetV1",
"PartnerAssetV2",
"PartnerAssetBackfillV1", "PartnerAssetBackfillV1",
"PartnerAssetBackfillV2",
"PartnerAssetDeleteV1", "PartnerAssetDeleteV1",
"PartnerAssetExifV1", "PartnerAssetExifV1",
"PartnerAssetExifBackfillV1", "PartnerAssetExifBackfillV1",
@@ -23434,11 +23288,8 @@
"AlbumUserBackfillV1", "AlbumUserBackfillV1",
"AlbumUserDeleteV1", "AlbumUserDeleteV1",
"AlbumAssetCreateV1", "AlbumAssetCreateV1",
"AlbumAssetCreateV2",
"AlbumAssetUpdateV1", "AlbumAssetUpdateV1",
"AlbumAssetUpdateV2",
"AlbumAssetBackfillV1", "AlbumAssetBackfillV1",
"AlbumAssetBackfillV2",
"AlbumAssetExifCreateV1", "AlbumAssetExifCreateV1",
"AlbumAssetExifUpdateV1", "AlbumAssetExifUpdateV1",
"AlbumAssetExifBackfillV1", "AlbumAssetExifBackfillV1",
@@ -23730,10 +23581,8 @@
"AlbumUsersV1", "AlbumUsersV1",
"AlbumToAssetsV1", "AlbumToAssetsV1",
"AlbumAssetsV1", "AlbumAssetsV1",
"AlbumAssetsV2",
"AlbumAssetExifsV1", "AlbumAssetExifsV1",
"AssetsV1", "AssetsV1",
"AssetsV2",
"AssetExifsV1", "AssetExifsV1",
"AssetEditsV1", "AssetEditsV1",
"AssetMetadataV1", "AssetMetadataV1",
@@ -23742,7 +23591,6 @@
"MemoryToAssetsV1", "MemoryToAssetsV1",
"PartnersV1", "PartnersV1",
"PartnerAssetsV1", "PartnerAssetsV1",
"PartnerAssetsV2",
"PartnerAssetExifsV1", "PartnerAssetExifsV1",
"PartnerStacksV1", "PartnerStacksV1",
"StacksV1", "StacksV1",
@@ -24542,10 +24390,9 @@
}, },
"defaultStorageQuota": { "defaultStorageQuota": {
"description": "Default storage quota", "description": "Default storage quota",
"maximum": 9007199254740991,
"minimum": 0, "minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "number"
}, },
"enabled": { "enabled": {
"description": "Enabled", "description": "Enabled",
@@ -24720,7 +24567,7 @@
"description": "SMTP server port", "description": "SMTP server port",
"maximum": 65535, "maximum": 65535,
"minimum": 0, "minimum": 0,
"type": "integer" "type": "number"
}, },
"secure": { "secure": {
"description": "Whether to use secure connection (TLS/SSL)", "description": "Whether to use secure connection (TLS/SSL)",
@@ -25136,12 +24983,10 @@
"type": "array" "type": "array"
}, },
"duration": { "duration": {
"description": "Array of video/gif durations in milliseconds (null for static images)", "description": "Array of video/gif durations in hh:mm:ss.SSS format (null for static images)",
"items": { "items": {
"maximum": 2147483647,
"minimum": 0,
"nullable": true, "nullable": true,
"type": "integer" "type": "string"
}, },
"type": "array" "type": "array"
}, },
@@ -25376,13 +25221,14 @@
}, },
"UpdateAlbumUserDto": { "UpdateAlbumUserDto": {
"properties": { "properties": {
"isFavorite": {
"description": "Mark album as favorite for the user (only the user themselves can update)",
"type": "boolean"
},
"role": { "role": {
"$ref": "#/components/schemas/AlbumUserRole" "$ref": "#/components/schemas/AlbumUserRole"
} }
}, },
"required": [
"role"
],
"type": "object" "type": "object"
}, },
"UpdateAssetDto": { "UpdateAssetDto": {
@@ -26140,9 +25986,7 @@
}, },
"order": { "order": {
"description": "Action order", "description": "Action order",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"pluginActionId": { "pluginActionId": {
"description": "Plugin action ID", "description": "Plugin action ID",
@@ -26241,9 +26085,7 @@
}, },
"order": { "order": {
"description": "Filter order", "description": "Filter order",
"maximum": 9007199254740991, "type": "number"
"minimum": -9007199254740991,
"type": "integer"
}, },
"pluginFilterId": { "pluginFilterId": {
"description": "Plugin filter ID", "description": "Plugin filter ID",
+17 -56
View File
@@ -474,6 +474,8 @@ export type AlbumResponseDto = {
id: string; id: string;
/** Activity feed enabled */ /** Activity feed enabled */
isActivityEnabled: boolean; isActivityEnabled: boolean;
/** Whether the authenticated user has favorited this album */
isFavorite: boolean;
/** Last modified asset timestamp */ /** Last modified asset timestamp */
lastModifiedAssetTimestamp?: string; lastModifiedAssetTimestamp?: string;
order?: AssetOrder; order?: AssetOrder;
@@ -556,7 +558,9 @@ export type MapMarkerResponseDto = {
state: string | null; state: string | null;
}; };
export type UpdateAlbumUserDto = { export type UpdateAlbumUserDto = {
role: AlbumUserRole; /** Mark album as favorite for the user (only the user themselves can update) */
isFavorite?: boolean;
role?: AlbumUserRole;
}; };
export type AlbumUserAddDto = { export type AlbumUserAddDto = {
/** Album user role */ /** Album user role */
@@ -614,8 +618,8 @@ export type AssetMetadataUpsertItemDto = {
export type AssetMediaCreateDto = { export type AssetMediaCreateDto = {
/** Asset file data */ /** Asset file data */
assetData: Blob; assetData: Blob;
/** Duration in milliseconds (for videos) */ /** Duration (for videos) */
duration?: number; duration?: string;
/** File creation date */ /** File creation date */
fileCreatedAt: string; fileCreatedAt: string;
/** File modification date */ /** File modification date */
@@ -854,8 +858,8 @@ export type AssetResponseDto = {
createdAt: string; createdAt: string;
/** Duplicate group ID */ /** Duplicate group ID */
duplicateId?: string | null; duplicateId?: string | null;
/** Video/gif duration in milliseconds (null for static images) */ /** Video/gif duration in hh:mm:ss.SSS format (null for static images) */
duration: number | null; duration: string | null;
exifInfo?: ExifResponseDto; exifInfo?: ExifResponseDto;
/** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */ /** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */
fileCreatedAt: string; fileCreatedAt: string;
@@ -2673,8 +2677,8 @@ export type TimeBucketAssetResponseDto = {
city: (string | null)[]; city: (string | null)[];
/** Array of country names extracted from EXIF GPS data */ /** Array of country names extracted from EXIF GPS data */
country: (string | null)[]; country: (string | null)[];
/** Array of video/gif durations in milliseconds (null for static images) */ /** Array of video/gif durations in hh:mm:ss.SSS format (null for static images) */
duration: (number | null)[]; duration: (string | null)[];
/** Array of file creation timestamps in UTC */ /** Array of file creation timestamps in UTC */
fileCreatedAt: string[]; fileCreatedAt: string[];
/** Array of asset IDs in the time bucket */ /** Array of asset IDs in the time bucket */
@@ -2856,6 +2860,8 @@ export type SyncAlbumUserDeleteV1 = {
export type SyncAlbumUserV1 = { export type SyncAlbumUserV1 = {
/** Album ID */ /** Album ID */
albumId: string; albumId: string;
/** Favorite flag */
isFavorite: boolean;
role: AlbumUserRole; role: AlbumUserRole;
/** User ID */ /** User ID */
userId: string; userId: string;
@@ -3075,44 +3081,6 @@ export type SyncAssetV1 = {
/** Asset width */ /** Asset width */
width: number | null; width: number | null;
}; };
export type SyncAssetV2 = {
/** Checksum */
checksum: string;
/** Deleted at */
deletedAt: string | null;
/** Duration */
duration: number | null;
/** File created at */
fileCreatedAt: string | null;
/** File modified at */
fileModifiedAt: string | null;
/** Asset height */
height: number | null;
/** Asset ID */
id: string;
/** Is edited */
isEdited: boolean;
/** Is favorite */
isFavorite: boolean;
/** Library ID */
libraryId: string | null;
/** Live photo video ID */
livePhotoVideoId: string | null;
/** Local date time */
localDateTime: string | null;
/** Original file name */
originalFileName: string;
/** Owner ID */
ownerId: string;
/** Stack ID */
stackId: string | null;
/** Thumbhash */
thumbhash: string | null;
"type": AssetTypeEnum;
visibility: AssetVisibility;
/** Asset width */
width: number | null;
};
export type SyncAuthUserV1 = { export type SyncAuthUserV1 = {
avatarColor?: (UserAvatarColor) | null; avatarColor?: (UserAvatarColor) | null;
/** User deleted at */ /** User deleted at */
@@ -3657,8 +3625,9 @@ export function getUserStatisticsAdmin({ id, isFavorite, isTrashed, visibility }
/** /**
* List all albums * List all albums
*/ */
export function getAllAlbums({ assetId, shared }: { export function getAllAlbums({ assetId, favorite, shared }: {
assetId?: string; assetId?: string;
favorite?: boolean;
shared?: boolean; shared?: boolean;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
@@ -3666,6 +3635,7 @@ export function getAllAlbums({ assetId, shared }: {
data: AlbumResponseDto[]; data: AlbumResponseDto[];
}>(`/albums${QS.query(QS.explode({ }>(`/albums${QS.query(QS.explode({
assetId, assetId,
favorite,
shared shared
}))}`, { }))}`, {
...opts ...opts
@@ -4480,7 +4450,7 @@ export function resolveDuplicates({ duplicateResolveDto }: {
}))); })));
} }
/** /**
* Dismiss a duplicate group * Delete a duplicate
*/ */
export function deleteDuplicate({ id }: { export function deleteDuplicate({ id }: {
id: string; id: string;
@@ -7147,7 +7117,6 @@ export enum SyncEntityType {
UserV1 = "UserV1", UserV1 = "UserV1",
UserDeleteV1 = "UserDeleteV1", UserDeleteV1 = "UserDeleteV1",
AssetV1 = "AssetV1", AssetV1 = "AssetV1",
AssetV2 = "AssetV2",
AssetDeleteV1 = "AssetDeleteV1", AssetDeleteV1 = "AssetDeleteV1",
AssetExifV1 = "AssetExifV1", AssetExifV1 = "AssetExifV1",
AssetEditV1 = "AssetEditV1", AssetEditV1 = "AssetEditV1",
@@ -7157,9 +7126,7 @@ export enum SyncEntityType {
PartnerV1 = "PartnerV1", PartnerV1 = "PartnerV1",
PartnerDeleteV1 = "PartnerDeleteV1", PartnerDeleteV1 = "PartnerDeleteV1",
PartnerAssetV1 = "PartnerAssetV1", PartnerAssetV1 = "PartnerAssetV1",
PartnerAssetV2 = "PartnerAssetV2",
PartnerAssetBackfillV1 = "PartnerAssetBackfillV1", PartnerAssetBackfillV1 = "PartnerAssetBackfillV1",
PartnerAssetBackfillV2 = "PartnerAssetBackfillV2",
PartnerAssetDeleteV1 = "PartnerAssetDeleteV1", PartnerAssetDeleteV1 = "PartnerAssetDeleteV1",
PartnerAssetExifV1 = "PartnerAssetExifV1", PartnerAssetExifV1 = "PartnerAssetExifV1",
PartnerAssetExifBackfillV1 = "PartnerAssetExifBackfillV1", PartnerAssetExifBackfillV1 = "PartnerAssetExifBackfillV1",
@@ -7173,11 +7140,8 @@ export enum SyncEntityType {
AlbumUserBackfillV1 = "AlbumUserBackfillV1", AlbumUserBackfillV1 = "AlbumUserBackfillV1",
AlbumUserDeleteV1 = "AlbumUserDeleteV1", AlbumUserDeleteV1 = "AlbumUserDeleteV1",
AlbumAssetCreateV1 = "AlbumAssetCreateV1", AlbumAssetCreateV1 = "AlbumAssetCreateV1",
AlbumAssetCreateV2 = "AlbumAssetCreateV2",
AlbumAssetUpdateV1 = "AlbumAssetUpdateV1", AlbumAssetUpdateV1 = "AlbumAssetUpdateV1",
AlbumAssetUpdateV2 = "AlbumAssetUpdateV2",
AlbumAssetBackfillV1 = "AlbumAssetBackfillV1", AlbumAssetBackfillV1 = "AlbumAssetBackfillV1",
AlbumAssetBackfillV2 = "AlbumAssetBackfillV2",
AlbumAssetExifCreateV1 = "AlbumAssetExifCreateV1", AlbumAssetExifCreateV1 = "AlbumAssetExifCreateV1",
AlbumAssetExifUpdateV1 = "AlbumAssetExifUpdateV1", AlbumAssetExifUpdateV1 = "AlbumAssetExifUpdateV1",
AlbumAssetExifBackfillV1 = "AlbumAssetExifBackfillV1", AlbumAssetExifBackfillV1 = "AlbumAssetExifBackfillV1",
@@ -7207,10 +7171,8 @@ export enum SyncRequestType {
AlbumUsersV1 = "AlbumUsersV1", AlbumUsersV1 = "AlbumUsersV1",
AlbumToAssetsV1 = "AlbumToAssetsV1", AlbumToAssetsV1 = "AlbumToAssetsV1",
AlbumAssetsV1 = "AlbumAssetsV1", AlbumAssetsV1 = "AlbumAssetsV1",
AlbumAssetsV2 = "AlbumAssetsV2",
AlbumAssetExifsV1 = "AlbumAssetExifsV1", AlbumAssetExifsV1 = "AlbumAssetExifsV1",
AssetsV1 = "AssetsV1", AssetsV1 = "AssetsV1",
AssetsV2 = "AssetsV2",
AssetExifsV1 = "AssetExifsV1", AssetExifsV1 = "AssetExifsV1",
AssetEditsV1 = "AssetEditsV1", AssetEditsV1 = "AssetEditsV1",
AssetMetadataV1 = "AssetMetadataV1", AssetMetadataV1 = "AssetMetadataV1",
@@ -7219,7 +7181,6 @@ export enum SyncRequestType {
MemoryToAssetsV1 = "MemoryToAssetsV1", MemoryToAssetsV1 = "MemoryToAssetsV1",
PartnersV1 = "PartnersV1", PartnersV1 = "PartnersV1",
PartnerAssetsV1 = "PartnerAssetsV1", PartnerAssetsV1 = "PartnerAssetsV1",
PartnerAssetsV2 = "PartnerAssetsV2",
PartnerAssetExifsV1 = "PartnerAssetExifsV1", PartnerAssetExifsV1 = "PartnerAssetExifsV1",
PartnerStacksV1 = "PartnerStacksV1", PartnerStacksV1 = "PartnerStacksV1",
StacksV1 = "StacksV1", StacksV1 = "StacksV1",
@@ -1,16 +1,9 @@
import { HttpError } from '@oazapfts/runtime'; import { HttpError } from '@oazapfts/runtime';
export interface ApiValidationError {
code: string;
path: (string | number)[];
message: string;
}
export interface ApiExceptionResponse { export interface ApiExceptionResponse {
message: string; message: string;
error?: string; error?: string;
statusCode: number; statusCode: number;
errors?: ApiValidationError[];
} }
export interface ApiHttpError extends HttpError { export interface ApiHttpError extends HttpError {
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "2.7.5", "version": "2.7.5",
"description": "Monorepo for Immich", "description": "Monorepo for Immich",
"private": true, "private": true,
"packageManager": "pnpm@10.33.1+sha512.05ba3c1d5d1c18f68df06470d74055e62d41fc110a0c660db1b2dfb2785327f04cf0f68345d4609bc52089e7fa0343c31593b2f9594e2c5d5da426230acc9820", "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"engines": { "engines": {
"pnpm": ">=10.0.0" "pnpm": ">=10.0.0"
} }
+1000 -1062
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -48,7 +48,6 @@ export default typescriptEslint.config([
'unicorn/import-style': 'off', 'unicorn/import-style': 'off',
'unicorn/prefer-structured-clone': 'off', 'unicorn/prefer-structured-clone': 'off',
'unicorn/no-for-loop': 'off', 'unicorn/no-for-loop': 'off',
'unicorn/no-array-sort': 'off',
'@typescript-eslint/await-thenable': 'error', '@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/no-misused-promises': 'error',
'require-await': 'off', 'require-await': 'off',
+4 -4
View File
@@ -46,15 +46,15 @@
"@nestjs/platform-express": "^11.0.4", "@nestjs/platform-express": "^11.0.4",
"@nestjs/platform-socket.io": "^11.0.4", "@nestjs/platform-socket.io": "^11.0.4",
"@nestjs/schedule": "^6.0.0", "@nestjs/schedule": "^6.0.0",
"@nestjs/swagger": "^11.4.2", "@nestjs/swagger": "11.2.6",
"@nestjs/websockets": "^11.0.4", "@nestjs/websockets": "^11.0.4",
"@opentelemetry/api": "^1.9.0", "@opentelemetry/api": "^1.9.0",
"@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/context-async-hooks": "^2.0.0",
"@opentelemetry/exporter-prometheus": "^0.215.0", "@opentelemetry/exporter-prometheus": "^0.215.0",
"@opentelemetry/instrumentation-http": "^0.215.0", "@opentelemetry/instrumentation-http": "^0.215.0",
"@opentelemetry/instrumentation-ioredis": "^0.63.0", "@opentelemetry/instrumentation-ioredis": "^0.62.0",
"@opentelemetry/instrumentation-nestjs-core": "^0.61.0", "@opentelemetry/instrumentation-nestjs-core": "^0.60.0",
"@opentelemetry/instrumentation-pg": "^0.67.0", "@opentelemetry/instrumentation-pg": "^0.66.0",
"@opentelemetry/resources": "^2.0.1", "@opentelemetry/resources": "^2.0.1",
"@opentelemetry/sdk-metrics": "^2.0.1", "@opentelemetry/sdk-metrics": "^2.0.1",
"@opentelemetry/sdk-node": "^0.215.0", "@opentelemetry/sdk-node": "^0.215.0",
+7 -3
View File
@@ -14,6 +14,7 @@ export const ErrorMessages = {
export const POSTGRES_VERSION_RANGE = '>=14.0.0'; export const POSTGRES_VERSION_RANGE = '>=14.0.0';
export const VECTORCHORD_VERSION_RANGE = '>=0.3 <2'; export const VECTORCHORD_VERSION_RANGE = '>=0.3 <2';
export const VECTORS_VERSION_RANGE = '>=0.2 <0.4';
export const VECTOR_VERSION_RANGE = '>=0.5 <1'; export const VECTOR_VERSION_RANGE = '>=0.5 <1';
export const JOBS_ASSET_PAGINATION_SIZE = 1000; export const JOBS_ASSET_PAGINATION_SIZE = 1000;
@@ -23,10 +24,15 @@ export const EXTENSION_NAMES: Record<DatabaseExtension, string> = {
cube: 'cube', cube: 'cube',
earthdistance: 'earthdistance', earthdistance: 'earthdistance',
vector: 'pgvector', vector: 'pgvector',
vectors: 'pgvecto.rs',
vchord: 'VectorChord', vchord: 'VectorChord',
} as const; } as const;
export const VECTOR_EXTENSIONS = [DatabaseExtension.VectorChord, DatabaseExtension.Vector] as const; export const VECTOR_EXTENSIONS = [
DatabaseExtension.VectorChord,
DatabaseExtension.Vectors,
DatabaseExtension.Vector,
] as const;
export const VECTOR_INDEX_TABLES = { export const VECTOR_INDEX_TABLES = {
[VectorIndex.Clip]: 'smart_search', [VectorIndex.Clip]: 'smart_search',
@@ -36,8 +42,6 @@ export const VECTOR_INDEX_TABLES = {
export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2; export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2;
export const SALT_ROUNDS = 10; export const SALT_ROUNDS = 10;
// Syntactically valid bcrypt hash used in login() preventing timing-based user enumeration.
export const LOGIN_DUMMY_HASH = '$2b$10$abcdefghijklmnopqrstuuABCDEFGHIJKLMNOPQRSTUVWXYZabcde';
export const IWorker = 'IWorker'; export const IWorker = 'IWorker';
@@ -28,16 +28,14 @@ describe(ActivityController.name, () => {
const { status, body } = await request(ctx.getHttpServer()).get('/activities'); const { status, body } = await request(ctx.getHttpServer()).get('/activities');
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(['[albumId] Invalid input: expected string, received undefined']),
{ path: ['albumId'], message: 'Invalid input: expected string, received undefined' },
]),
); );
}); });
it('should reject an invalid albumId', async () => { it('should reject an invalid albumId', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' }); const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['albumId'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID']));
}); });
it('should reject an invalid assetId', async () => { it('should reject an invalid assetId', async () => {
@@ -45,7 +43,7 @@ describe(ActivityController.name, () => {
.get('/activities') .get('/activities')
.query({ albumId: factory.uuid(), assetId: '123' }); .query({ albumId: factory.uuid(), assetId: '123' });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['assetId'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID']));
}); });
}); });
@@ -60,7 +58,7 @@ describe(ActivityController.name, () => {
.post('/activities') .post('/activities')
.send({ albumId: '123', type: 'like' }); .send({ albumId: '123', type: 'like' });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['albumId'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID']));
}); });
it('should require a comment when type is comment', async () => { it('should require a comment when type is comment', async () => {
@@ -68,11 +66,7 @@ describe(ActivityController.name, () => {
.post('/activities') .post('/activities')
.send({ albumId: factory.uuid(), type: 'comment', comment: null }); .send({ albumId: factory.uuid(), type: 'comment', comment: null });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(factory.responses.badRequest(['[comment] Invalid input: expected string, received null']));
factory.responses.validationError([
{ path: ['comment'], message: 'Invalid input: expected string, received null' },
]),
);
}); });
}); });
@@ -85,7 +79,7 @@ describe(ActivityController.name, () => {
it('should require a valid uuid', async () => { it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`); const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
}); });
}); });
}); });
@@ -27,17 +27,13 @@ describe(AlbumController.name, () => {
it('should reject an invalid shared param', async () => { it('should reject an invalid shared param', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid'); const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid');
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(factory.responses.badRequest(['[shared] Invalid option: expected one of "true"|"false"']));
factory.responses.validationError([
{ path: ['shared'], message: 'Invalid option: expected one of "true"|"false"' },
]),
);
}); });
it('should reject an invalid assetId param', async () => { it('should reject an invalid assetId param', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid'); const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid');
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['assetId'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID']));
}); });
}); });
@@ -49,7 +49,7 @@ describe(ApiKeyController.name, () => {
it('should require a valid uuid', async () => { it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`); const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
}); });
}); });
@@ -64,7 +64,7 @@ describe(ApiKeyController.name, () => {
.put(`/api-keys/123`) .put(`/api-keys/123`)
.send({ name: 'new name', permissions: [Permission.All] }); .send({ name: 'new name', permissions: [Permission.All] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
}); });
it('should allow updating just the name', async () => { it('should allow updating just the name', async () => {
@@ -84,7 +84,7 @@ describe(ApiKeyController.name, () => {
it('should require a valid uuid', async () => { it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`); const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
}); });
}); });
}); });
@@ -80,9 +80,7 @@ describe(AssetMediaController.name, () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(['[metadata] Invalid input: expected JSON string, received string']),
{ path: ['metadata'], message: 'Invalid input: expected JSON string, received string' },
]),
); );
}); });
@@ -93,8 +91,8 @@ describe(AssetMediaController.name, () => {
.field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) }); .field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest([
{ path: ['fileCreatedAt'], message: 'Invalid input: expected ISO 8601 datetime string, received undefined' }, '[fileCreatedAt] Invalid input: expected ISO 8601 datetime string, received undefined',
]), ]),
); );
}); });
@@ -106,8 +104,8 @@ describe(AssetMediaController.name, () => {
.field(makeUploadDto({ omit: 'fileModifiedAt' })); .field(makeUploadDto({ omit: 'fileModifiedAt' }));
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest([
{ path: ['fileModifiedAt'], message: 'Invalid input: expected ISO 8601 datetime string, received undefined' }, '[fileModifiedAt] Invalid input: expected ISO 8601 datetime string, received undefined',
]), ]),
); );
}); });
@@ -119,9 +117,7 @@ describe(AssetMediaController.name, () => {
.field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' }); .field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(['[isFavorite] Invalid option: expected one of "true"|"false"']),
{ path: ['isFavorite'], message: 'Invalid option: expected one of "true"|"false"' },
]),
); );
}); });
@@ -132,9 +128,7 @@ describe(AssetMediaController.name, () => {
.field({ ...makeUploadDto(), visibility: 'not-an-option' }); .field({ ...makeUploadDto(), visibility: 'not-an-option' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest([expect.stringContaining('[visibility] Invalid option: expected one of')]),
{ path: ['visibility'], message: expect.stringContaining('Invalid option: expected one of') },
]),
); );
}); });
+44 -80
View File
@@ -31,7 +31,7 @@ describe(AssetController.name, () => {
.send({ ids: ['123'] }); .send({ ids: ['123'] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID']));
}); });
it('should require duplicateId to be a string', async () => { it('should require duplicateId to be a string', async () => {
@@ -42,9 +42,7 @@ describe(AssetController.name, () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(['[duplicateId] Invalid input: expected string, received boolean']),
{ path: ['duplicateId'], message: 'Invalid input: expected string, received boolean' },
]),
); );
}); });
@@ -72,7 +70,7 @@ describe(AssetController.name, () => {
.send({ ids: ['123'] }); .send({ ids: ['123'] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID']));
}); });
}); });
@@ -85,7 +83,7 @@ describe(AssetController.name, () => {
it('should require a valid id', async () => { it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`); const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
}); });
}); });
@@ -99,10 +97,12 @@ describe(AssetController.name, () => {
const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({}); const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(
{ path: ['sourceId'], message: 'Invalid input: expected string, received undefined' }, expect.arrayContaining([
{ path: ['targetId'], message: 'Invalid input: expected string, received undefined' }, '[sourceId] Invalid input: expected string, received undefined',
'[targetId] Invalid input: expected string, received undefined',
]), ]),
),
); );
}); });
@@ -125,9 +125,7 @@ describe(AssetController.name, () => {
.put('/assets/metadata') .put('/assets/metadata')
.send({ items: [{ assetId: '123', key: 'test', value: {} }] }); .send({ items: [{ assetId: '123', key: 'test', value: {} }] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID'])));
factory.responses.validationError([{ path: ['items', 0, 'assetId'], message: 'Invalid UUID' }]),
);
}); });
it('should require a key', async () => { it('should require a key', async () => {
@@ -136,9 +134,9 @@ describe(AssetController.name, () => {
.send({ items: [{ assetId: factory.uuid(), value: {} }] }); .send({ items: [{ assetId: factory.uuid(), value: {} }] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(
{ path: ['items', 0, 'key'], message: 'Invalid input: expected string, received undefined' }, expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']),
]), ),
); );
}); });
@@ -161,9 +159,7 @@ describe(AssetController.name, () => {
.delete('/assets/metadata') .delete('/assets/metadata')
.send({ items: [{ assetId: '123', key: 'test' }] }); .send({ items: [{ assetId: '123', key: 'test' }] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID'])));
factory.responses.validationError([{ path: ['items', 0, 'assetId'], message: 'Invalid UUID' }]),
);
}); });
it('should require a key', async () => { it('should require a key', async () => {
@@ -172,9 +168,9 @@ describe(AssetController.name, () => {
.send({ items: [{ assetId: factory.uuid() }] }); .send({ items: [{ assetId: factory.uuid() }] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(
{ path: ['items', 0, 'key'], message: 'Invalid input: expected string, received undefined' }, expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']),
]), ),
); );
}); });
@@ -195,56 +191,33 @@ describe(AssetController.name, () => {
it('should require a valid id', async () => { it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`); const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(factory.responses.badRequest(['Invalid input: expected object, received undefined']));
factory.responses.validationError([
{ path: [], message: 'Invalid input: expected object, received undefined' },
]),
);
}); });
it('should reject invalid gps coordinates', async () => { it('should reject invalid gps coordinates', async () => {
for (const [test, errors] of [ for (const test of [
[{ latitude: 12 }, [{ path: [], message: 'Latitude and longitude must be provided together' }]], { latitude: 12 },
[{ longitude: 12 }, [{ path: [], message: 'Latitude and longitude must be provided together' }]], { longitude: 12 },
[
{ latitude: 12, longitude: 'abc' }, { latitude: 12, longitude: 'abc' },
[{ path: ['longitude'], message: 'Invalid input: expected number, received string' }],
],
[
{ latitude: 'abc', longitude: 12 }, { latitude: 'abc', longitude: 12 },
[{ path: ['latitude'], message: 'Invalid input: expected number, received string' }],
],
[
{ latitude: null, longitude: 12 }, { latitude: null, longitude: 12 },
[{ path: ['latitude'], message: 'Invalid input: expected number, received null' }],
],
[
{ latitude: 12, longitude: null }, { latitude: 12, longitude: null },
[{ path: ['longitude'], message: 'Invalid input: expected number, received null' }], { latitude: 91, longitude: 12 },
], { latitude: -91, longitude: 12 },
[{ latitude: 91, longitude: 12 }, [{ path: ['latitude'], message: 'Too big: expected number to be <=90' }]],
[{ latitude: -91, longitude: 12 }, [{ path: ['latitude'], message: 'Too small: expected number to be >=-90' }]],
[
{ latitude: 12, longitude: -181 }, { latitude: 12, longitude: -181 },
[{ path: ['longitude'], message: 'Too small: expected number to be >=-180' }], { latitude: 12, longitude: 181 },
], ]) {
[{ latitude: 12, longitude: 181 }, [{ path: ['longitude'], message: 'Too big: expected number to be <=180' }]],
] as const) {
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test); const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError(errors)); expect(body).toEqual(factory.responses.badRequest());
} }
}); });
it('should reject invalid rating', async () => { it('should reject invalid rating', async () => {
for (const [test, errors] of [ for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: -2 }]) {
[{ rating: 7 }, [{ path: ['rating'], message: 'Too big: expected number to be <=5' }]],
[{ rating: 3.5 }, [{ path: ['rating'], message: 'Invalid input: expected int, received number' }]],
[{ rating: -2 }, [{ path: ['rating'], message: 'Too small: expected number to be >=-1' }]],
] as const) {
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test); const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError(errors)); expect(body).toEqual(factory.responses.badRequest());
} }
}); });
@@ -288,17 +261,13 @@ describe(AssetController.name, () => {
it('should require a valid id', async () => { it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] }); const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID'])));
}); });
it('should require items to be an array', async () => { it('should require items to be an array', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({}); const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(factory.responses.badRequest(['[items] Invalid input: expected array, received undefined']));
factory.responses.validationError([
{ path: ['items'], message: 'Invalid input: expected array, received undefined' },
]),
);
}); });
it('should require each item to have a valid key', async () => { it('should require each item to have a valid key', async () => {
@@ -307,9 +276,7 @@ describe(AssetController.name, () => {
.send({ items: [{ value: { some: 'value' } }] }); .send({ items: [{ value: { some: 'value' } }] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(['[items.0.key] Invalid input: expected string, received undefined']),
{ path: ['items', 0, 'key'], message: 'Invalid input: expected string, received undefined' },
]),
); );
}); });
@@ -319,9 +286,9 @@ describe(AssetController.name, () => {
.send({ items: [{ key: 'mobile-app', value: null }] }); .send({ items: [{ key: 'mobile-app', value: null }] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(
{ path: ['items', 0, 'value'], message: 'Invalid input: expected record, received null' }, expect.arrayContaining(['[items.0.value] Invalid input: expected record, received null']),
]), ),
); );
}); });
@@ -359,7 +326,7 @@ describe(AssetController.name, () => {
it('should require a valid id', async () => { it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`); const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID'])));
}); });
}); });
@@ -409,7 +376,7 @@ describe(AssetController.name, () => {
}); });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID'])));
}); });
it('should check the action and parameters discriminator', async () => { it('should check the action and parameters discriminator', async () => {
@@ -431,12 +398,13 @@ describe(AssetController.name, () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
factory.responses.validationError([ factory.responses.badRequest(
{ expect.arrayContaining([
path: ['edits', 0, 'parameters'], expect.stringContaining(
message: expect.stringContaining("Invalid parameters for action 'rotate', expecting keys: angle"), "[edits.0.parameters] Invalid parameters for action 'rotate', expecting keys: angle",
}, ),
]), ]),
),
); );
}); });
@@ -445,11 +413,7 @@ describe(AssetController.name, () => {
.put(`/assets/${factory.uuid()}/edits`) .put(`/assets/${factory.uuid()}/edits`)
.send({ edits: [] }); .send({ edits: [] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(factory.responses.badRequest(['[edits] Too small: expected array to have >=1 items']));
factory.responses.validationError([
{ path: ['edits'], message: 'Too small: expected array to have >=1 items' },
]),
);
}); });
}); });
@@ -462,7 +426,7 @@ describe(AssetController.name, () => {
it('should require a valid id', async () => { it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`); const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
}); });
}); });
}); });
+13 -41
View File
@@ -28,27 +28,19 @@ describe(AuthController.name, () => {
it('should require an email address', async () => { it('should require an email address', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, password }); const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, password });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest());
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received undefined' }]),
);
}); });
it('should require a password', async () => { it('should require a password', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, email }); const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ name, email });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest());
errorDto.validationError([
{ path: ['password'], message: 'Invalid input: expected string, received undefined' },
]),
);
}); });
it('should require a name', async () => { it('should require a name', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ email, password }); const { status, body } = await request(ctx.getHttpServer()).post('/auth/admin-sign-up').send({ email, password });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest());
errorDto.validationError([{ path: ['name'], message: 'Invalid input: expected string, received undefined' }]),
);
}); });
it('should require a valid email', async () => { it('should require a valid email', async () => {
@@ -56,9 +48,7 @@ describe(AuthController.name, () => {
.post('/auth/admin-sign-up') .post('/auth/admin-sign-up')
.send({ name, email: 'immich', password }); .send({ name, email: 'immich', password });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest());
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received string' }]),
);
}); });
it('should transform email to lower case', async () => { it('should transform email to lower case', async () => {
@@ -83,9 +73,9 @@ describe(AuthController.name, () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/login').send({ name: 'admin' }); const { status, body } = await request(ctx.getHttpServer()).post('/auth/login').send({ name: 'admin' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
errorDto.validationError([ errorDto.badRequest([
{ path: ['email'], message: 'Invalid input: expected email, received undefined' }, '[email] Invalid input: expected email, received undefined',
{ path: ['password'], message: 'Invalid input: expected string, received undefined' }, '[password] Invalid input: expected string, received undefined',
]), ]),
); );
}); });
@@ -95,9 +85,7 @@ describe(AuthController.name, () => {
.post('/auth/login') .post('/auth/login')
.send({ name: 'admin', email: null, password: 'password' }); .send({ name: 'admin', email: null, password: 'password' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object']));
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received object' }]),
);
}); });
it(`should not allow null password`, async () => { it(`should not allow null password`, async () => {
@@ -105,9 +93,7 @@ describe(AuthController.name, () => {
.post('/auth/login') .post('/auth/login')
.send({ name: 'admin', email: 'admin@immich.cloud', password: null }); .send({ name: 'admin', email: 'admin@immich.cloud', password: null });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[password] Invalid input: expected string, received null']));
errorDto.validationError([{ path: ['password'], message: 'Invalid input: expected string, received null' }]),
);
}); });
it('should reject an invalid email', async () => { it('should reject an invalid email', async () => {
@@ -118,9 +104,7 @@ describe(AuthController.name, () => {
.send({ name: 'admin', email: [], password: 'password' }); .send({ name: 'admin', email: [], password: 'password' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object']));
errorDto.validationError([{ path: ['email'], message: 'Invalid input: expected email, received object' }]),
);
}); });
it('should transform the email to all lowercase', async () => { it('should transform the email to all lowercase', async () => {
@@ -211,31 +195,19 @@ describe(AuthController.name, () => {
it('should reject 5 digits', async () => { it('should reject 5 digits', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' }); const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`]));
errorDto.validationError([
{ path: ['pinCode'], message: String.raw`Invalid string: must match pattern /^\d{6}$/` },
]),
);
}); });
it('should reject 7 digits', async () => { it('should reject 7 digits', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '1234567' }); const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '1234567' });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`]));
errorDto.validationError([
{ path: ['pinCode'], message: String.raw`Invalid string: must match pattern /^\d{6}$/` },
]),
);
}); });
it('should reject non-numbers', async () => { it('should reject non-numbers', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: 'A12345' }); const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: 'A12345' });
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`]));
errorDto.validationError([
{ path: ['pinCode'], message: String.raw`Invalid string: must match pattern /^\d{6}$/` },
]),
);
}); });
}); });
@@ -41,7 +41,7 @@ describe(DuplicateController.name, () => {
it('should require a valid uuid', async () => { it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).delete(`/duplicates/123`); const { status, body } = await request(ctx.getHttpServer()).delete(`/duplicates/123`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID']));
}); });
}); });
}); });
@@ -41,8 +41,8 @@ export class DuplicateController {
@Authenticated({ permission: Permission.DuplicateDelete }) @Authenticated({ permission: Permission.DuplicateDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Endpoint({ @Endpoint({
summary: 'Dismiss a duplicate group', summary: 'Delete a duplicate',
description: 'Dismiss a duplicate group by its ID, unlinking all assets in the group without deleting them.', description: 'Delete a single duplicate asset specified by its ID.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'), history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
}) })
deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteDuplicate(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
@@ -31,9 +31,7 @@ describe(MaintenanceController.name, () => {
}); });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
errorDto.validationError([ errorDto.badRequest(['[restoreBackupFilename] Backup filename is required when action is restore_database']),
{ path: ['restoreBackupFilename'], message: 'Backup filename is required when action is restore_database' },
]),
); );
expect(ctx.authenticate).toHaveBeenCalled(); expect(ctx.authenticate).toHaveBeenCalled();
}); });
@@ -47,11 +47,7 @@ describe(MemoryController.name, () => {
}); });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[data.year] Invalid input: expected number, received undefined']));
errorDto.validationError([
{ path: ['data', 'year'], message: 'Invalid input: expected number, received undefined' },
]),
);
}); });
it('should accept showAt and hideAt', async () => { it('should accept showAt and hideAt', async () => {
@@ -85,7 +81,7 @@ describe(MemoryController.name, () => {
it('should require a valid id', async () => { it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`); const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
}); });
}); });
@@ -98,15 +94,13 @@ describe(MemoryController.name, () => {
it('should require a valid id', async () => { it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`); const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined']));
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
);
}); });
it('should require at least one field', async () => { it('should require at least one field', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`).send({}); const { status, body } = await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`).send({});
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: [], message: 'At least one field must be provided' }])); expect(body).toEqual(errorDto.badRequest(['At least one field must be provided']));
}); });
}); });
@@ -126,7 +120,7 @@ describe(MemoryController.name, () => {
it('should require a valid id', async () => { it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] }); const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
}); });
it('should require a valid asset id', async () => { it('should require a valid asset id', async () => {
@@ -134,7 +128,7 @@ describe(MemoryController.name, () => {
.put(`/memories/${factory.uuid()}/assets`) .put(`/memories/${factory.uuid()}/assets`)
.send({ ids: ['invalid'] }); .send({ ids: ['invalid'] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID']));
}); });
}); });
@@ -147,7 +141,7 @@ describe(MemoryController.name, () => {
it('should require a valid id', async () => { it('should require a valid id', async () => {
const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`); const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
}); });
it('should require a valid asset id', async () => { it('should require a valid asset id', async () => {
@@ -155,7 +149,7 @@ describe(MemoryController.name, () => {
.delete(`/memories/${factory.uuid()}/assets`) .delete(`/memories/${factory.uuid()}/assets`)
.send({ ids: ['invalid'] }); .send({ ids: ['invalid'] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['ids', 0], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID']));
}); });
}); });
}); });
@@ -31,11 +31,7 @@ describe(NotificationController.name, () => {
.query({ level: 'invalid' }) .query({ level: 'invalid' })
.set('Authorization', `Bearer token`); .set('Authorization', `Bearer token`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[level] Invalid option: expected one of')]));
errorDto.validationError([
{ path: ['level'], message: expect.stringContaining('Invalid option: expected one of') },
]),
);
}); });
}); });
@@ -49,9 +45,7 @@ describe(NotificationController.name, () => {
it('should require a list', async () => { it('should require a list', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true }); const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[ids] Invalid input: expected array, received boolean']));
errorDto.validationError([{ path: ['ids'], message: 'Invalid input: expected array, received boolean' }]),
);
}); });
it('should require uuids', async () => { it('should require uuids', async () => {
@@ -59,9 +53,7 @@ describe(NotificationController.name, () => {
.put(`/notifications`) .put(`/notifications`)
.send({ ids: [true] }); .send({ ids: [true] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid input: expected string, received boolean']));
errorDto.validationError([{ path: ['ids', 0], message: 'Invalid input: expected string, received boolean' }]),
);
}); });
it('should accept valid uuids', async () => { it('should accept valid uuids', async () => {
@@ -83,7 +75,7 @@ describe(NotificationController.name, () => {
it('should require a valid uuid', async () => { it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`); const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
}); });
}); });
@@ -33,9 +33,7 @@ describe(PartnerController.name, () => {
const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`); const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
errorDto.validationError([ errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]),
{ path: ['direction'], message: expect.stringContaining('Invalid option: expected one of') },
]),
); );
}); });
@@ -46,9 +44,7 @@ describe(PartnerController.name, () => {
.set('Authorization', `Bearer token`); .set('Authorization', `Bearer token`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual( expect(body).toEqual(
errorDto.validationError([ errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]),
{ path: ['direction'], message: expect.stringContaining('Invalid option: expected one of') },
]),
); );
}); });
}); });
@@ -65,7 +61,7 @@ describe(PartnerController.name, () => {
.send({ sharedWithId: 'invalid' }) .send({ sharedWithId: 'invalid' })
.set('Authorization', `Bearer token`); .set('Authorization', `Bearer token`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['sharedWithId'], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[sharedWithId] Invalid UUID']));
}); });
}); });
@@ -81,7 +77,7 @@ describe(PartnerController.name, () => {
.send({ inTimeline: true }) .send({ inTimeline: true })
.set('Authorization', `Bearer token`); .set('Authorization', `Bearer token`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
}); });
}); });
@@ -96,7 +92,7 @@ describe(PartnerController.name, () => {
.delete(`/partners/invalid`) .delete(`/partners/invalid`)
.set('Authorization', `Bearer token`); .set('Authorization', `Bearer token`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }])); expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
}); });
}); });
}); });

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