forked from Cutlery/immich
Merge branch 'main' of https://github.com/immich-app/immich into feat/offline-files-job
This commit is contained in:
commit
5515f57c09
12
cli/package-lock.json
generated
12
cli/package-lock.json
generated
@ -47,15 +47,15 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.0",
|
||||||
"typescript": "^5.4.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
@ -4456,9 +4456,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.3.tgz",
|
||||||
"integrity": "sha512-FWZbz0oSdLq5snUI0b6sULbz58iXFXdvkZfZWR/F0ZJuKTSPO7v72QPXt6KqYeMFb0yytNp6kZosxJ96Nr/wDQ==",
|
"integrity": "sha512-+i1oagbvkVIhEy9TnEV+fgXsng13nZM90JQbrcPrf6DvW2mXARlz+DK7DLiDP+qeKoD1FCVx/1SpFL1CLq9Mhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.20.1",
|
"esbuild": "^0.20.1",
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
import { getAssetStatistics, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
|
import { getAssetStatistics, getMyUserInfo, getServerVersion, getSupportedMediaTypes } from '@immich/sdk';
|
||||||
import { BaseOptions, authenticate } from 'src/utils';
|
import { BaseOptions, authenticate } from 'src/utils';
|
||||||
|
|
||||||
export const serverInfo = async (options: BaseOptions) => {
|
export const serverInfo = async (options: BaseOptions) => {
|
||||||
await authenticate(options);
|
const { url } = await authenticate(options);
|
||||||
|
|
||||||
const versionInfo = await getServerVersion();
|
const [versionInfo, mediaTypes, stats, userInfo] = await Promise.all([
|
||||||
const mediaTypes = await getSupportedMediaTypes();
|
getServerVersion(),
|
||||||
const stats = await getAssetStatistics({});
|
getSupportedMediaTypes(),
|
||||||
|
getAssetStatistics({}),
|
||||||
|
getMyUserInfo(),
|
||||||
|
]);
|
||||||
|
|
||||||
console.log(`Server Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
|
console.log(`Server Info (via ${userInfo.email})`);
|
||||||
console.log(`Image Types: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`);
|
console.log(` Url: ${url}`);
|
||||||
console.log(`Video Types: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`);
|
console.log(` Version: ${versionInfo.major}.${versionInfo.minor}.${versionInfo.patch}`);
|
||||||
console.log(`Statistics:\n Images: ${stats.images}\n Videos: ${stats.videos}\n Total: ${stats.total}`);
|
console.log(` Formats:`);
|
||||||
|
console.log(` Images: ${mediaTypes.image.map((extension) => extension.replace('.', ''))}`);
|
||||||
|
console.log(` Videos: ${mediaTypes.video.map((extension) => extension.replace('.', ''))}`);
|
||||||
|
console.log(` Statistics:`);
|
||||||
|
console.log(` Images: ${stats.images}`);
|
||||||
|
console.log(` Videos: ${stats.videos}`);
|
||||||
|
console.log(` Total: ${stats.total}`);
|
||||||
};
|
};
|
||||||
|
@ -15,21 +15,25 @@ export interface BaseOptions {
|
|||||||
export type AuthDto = { url: string; key: string };
|
export type AuthDto = { url: string; key: string };
|
||||||
type OldAuthDto = { instanceUrl: string; apiKey: string };
|
type OldAuthDto = { instanceUrl: string; apiKey: string };
|
||||||
|
|
||||||
export const authenticate = async (options: BaseOptions): Promise<void> => {
|
export const authenticate = async (options: BaseOptions): Promise<AuthDto> => {
|
||||||
const { configDirectory: configDir, url, key } = options;
|
const { configDirectory: configDir, url, key } = options;
|
||||||
|
|
||||||
// provided in command
|
// provided in command
|
||||||
if (url && key) {
|
if (url && key) {
|
||||||
await connect(url, key);
|
return connect(url, key);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback to auth file
|
// fallback to auth file
|
||||||
const config = await readAuthFile(configDir);
|
const config = await readAuthFile(configDir);
|
||||||
await connect(config.url, config.key);
|
const auth = await connect(config.url, config.key);
|
||||||
|
if (auth.url !== config.url) {
|
||||||
|
await writeAuthFile(configDir, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
return auth;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const connect = async (url: string, key: string): Promise<void> => {
|
export const connect = async (url: string, key: string) => {
|
||||||
const wellKnownUrl = new URL('.well-known/immich', url);
|
const wellKnownUrl = new URL('.well-known/immich', url);
|
||||||
try {
|
try {
|
||||||
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
|
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
|
||||||
@ -50,6 +54,8 @@ export const connect = async (url: string, key: string): Promise<void> => {
|
|||||||
logError(error, 'Failed to connect to server');
|
logError(error, 'Failed to connect to server');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return { url, key };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logError = (error: unknown, message: string) => {
|
export const logError = (error: unknown, message: string) => {
|
||||||
|
@ -20,7 +20,7 @@ The recommended way to backup and restore the Immich database is to use the `pg_
|
|||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="Linux system based Backup" label="Linux system based Backup" default>
|
<TabItem value="Linux system based Backup" label="Linux system based Backup" default>
|
||||||
|
|
||||||
```bash title='Bash'
|
```bash title='Backup'
|
||||||
docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/backup/dump.sql.gz"
|
docker exec -t immich_postgres pg_dumpall -c -U postgres | gzip > "/path/to/backup/dump.sql.gz"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
6
e2e/package-lock.json
generated
6
e2e/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
@ -80,7 +80,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
TimeBucketSize,
|
|
||||||
getAllLibraries,
|
getAllLibraries,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
updateAssets,
|
updateAssets,
|
||||||
@ -942,146 +941,6 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /asset/time-buckets', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/asset/time-buckets').query({ size: TimeBucketSize.Month });
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get time buckets by month', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
{ count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
|
|
||||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not allow access for unrelated shared links', async () => {
|
|
||||||
const sharedLink = await utils.createSharedLink(user1.accessToken, {
|
|
||||||
type: SharedLinkType.Individual,
|
|
||||||
assetIds: user1Assets.map(({ id }) => id),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
|
|
||||||
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get time buckets by day', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Day });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([
|
|
||||||
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
|
|
||||||
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
|
|
||||||
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset/time-bucket', () => {
|
|
||||||
it('should require authentication', async () => {
|
|
||||||
const { status, body } = await request(app).get('/asset/time-bucket').query({
|
|
||||||
size: TimeBucketSize.Month,
|
|
||||||
timeBucket: '1900-01-01T00:00:00.000Z',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(status).toBe(401);
|
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle 5 digit years', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-bucket')
|
|
||||||
.query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' })
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO enable date string validation while still accepting 5 digit years
|
|
||||||
// it('should fail if time bucket is invalid', async () => {
|
|
||||||
// const { status, body } = await request(app)
|
|
||||||
// .get('/asset/time-bucket')
|
|
||||||
// .set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
|
|
||||||
|
|
||||||
// expect(status).toBe(400);
|
|
||||||
// expect(body).toEqual(errorDto.badRequest);
|
|
||||||
// });
|
|
||||||
|
|
||||||
it('should return time bucket', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/asset/time-bucket')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
|
||||||
const req1 = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
|
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
|
||||||
|
|
||||||
const req2 = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
|
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and favorite', async () => {
|
|
||||||
const req1 = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
|
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
|
||||||
|
|
||||||
const req2 = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
|
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and trash', async () => {
|
|
||||||
const req = await request(app)
|
|
||||||
.get('/asset/time-buckets')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
|
||||||
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
|
|
||||||
|
|
||||||
expect(req.status).toBe(400);
|
|
||||||
expect(req.body).toEqual(errorDto.badRequest());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /asset', () => {
|
describe('GET /asset', () => {
|
||||||
it('should return stack data', async () => {
|
it('should return stack data', async () => {
|
||||||
const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
|
const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { LoginResponseDto, getConfig } from '@immich/sdk';
|
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, getConfig } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { app, asBearerAuth, utils } from 'src/utils';
|
||||||
@ -10,11 +10,14 @@ const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAu
|
|||||||
describe('/system-config', () => {
|
describe('/system-config', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let nonAdmin: LoginResponseDto;
|
let nonAdmin: LoginResponseDto;
|
||||||
|
let asset: AssetFileUploadResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
|
||||||
|
|
||||||
|
asset = await utils.createAsset(admin.accessToken);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /system-config/map/style.json', () => {
|
describe('GET /system-config/map/style.json', () => {
|
||||||
@ -24,6 +27,19 @@ describe('/system-config', () => {
|
|||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow shared link access', async () => {
|
||||||
|
const sharedLink = await utils.createSharedLink(admin.accessToken, {
|
||||||
|
type: SharedLinkType.Individual,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
});
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/system-config/map/style.json?key=${sharedLink.key}`)
|
||||||
|
.query({ theme: 'dark' });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw an error if a theme is not light or dark', async () => {
|
it('should throw an error if a theme is not light or dark', async () => {
|
||||||
for (const theme of ['dark1', true, 123, '', null, undefined]) {
|
for (const theme of ['dark1', true, 123, '', null, undefined]) {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
|
193
e2e/src/api/specs/timeline.e2e-spec.ts
Normal file
193
e2e/src/api/specs/timeline.e2e-spec.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { createUserDto } from 'src/fixtures';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
|
import { app, utils } from 'src/utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
// TODO this should probably be a test util function
|
||||||
|
const today = DateTime.fromObject({
|
||||||
|
year: 2023,
|
||||||
|
month: 11,
|
||||||
|
day: 3,
|
||||||
|
}) as DateTime<true>;
|
||||||
|
const yesterday = today.minus({ days: 1 });
|
||||||
|
|
||||||
|
describe('/timeline', () => {
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
let user: LoginResponseDto;
|
||||||
|
let timeBucketUser: LoginResponseDto;
|
||||||
|
|
||||||
|
let userAssets: AssetFileUploadResponseDto[];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup({ onboarding: false });
|
||||||
|
[user, timeBucketUser] = await Promise.all([
|
||||||
|
utils.userSetup(admin.accessToken, createUserDto.create('1')),
|
||||||
|
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
userAssets = await Promise.all([
|
||||||
|
utils.createAsset(user.accessToken),
|
||||||
|
utils.createAsset(user.accessToken),
|
||||||
|
utils.createAsset(user.accessToken, {
|
||||||
|
isFavorite: true,
|
||||||
|
isReadOnly: true,
|
||||||
|
fileCreatedAt: yesterday.toISO(),
|
||||||
|
fileModifiedAt: yesterday.toISO(),
|
||||||
|
assetData: { filename: 'example.mp4' },
|
||||||
|
}),
|
||||||
|
utils.createAsset(user.accessToken),
|
||||||
|
utils.createAsset(user.accessToken),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
|
||||||
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
|
||||||
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||||
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /timeline/buckets', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get time buckets by month', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
|
||||||
|
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not allow access for unrelated shared links', async () => {
|
||||||
|
const sharedLink = await utils.createSharedLink(user.accessToken, {
|
||||||
|
type: SharedLinkType.Individual,
|
||||||
|
assetIds: userAssets.map(({ id }) => id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.noPermission);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get time buckets by day', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Day });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([
|
||||||
|
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
|
||||||
|
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
|
||||||
|
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
||||||
|
const req1 = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
|
||||||
|
|
||||||
|
expect(req1.status).toBe(400);
|
||||||
|
expect(req1.body).toEqual(errorDto.badRequest());
|
||||||
|
|
||||||
|
const req2 = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
|
||||||
|
|
||||||
|
expect(req2.status).toBe(400);
|
||||||
|
expect(req2.body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error if time bucket is requested with partners asset and favorite', async () => {
|
||||||
|
const req1 = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
|
||||||
|
|
||||||
|
expect(req1.status).toBe(400);
|
||||||
|
expect(req1.body).toEqual(errorDto.badRequest());
|
||||||
|
|
||||||
|
const req2 = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
|
||||||
|
|
||||||
|
expect(req2.status).toBe(400);
|
||||||
|
expect(req2.body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return error if time bucket is requested with partners asset and trash', async () => {
|
||||||
|
const req = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
|
||||||
|
|
||||||
|
expect(req.status).toBe(400);
|
||||||
|
expect(req.body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /timeline/bucket', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/timeline/bucket').query({
|
||||||
|
size: TimeBucketSize.Month,
|
||||||
|
timeBucket: '1900-01-01T00:00:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 5 digit years', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/bucket')
|
||||||
|
.query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' })
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO enable date string validation while still accepting 5 digit years
|
||||||
|
// it('should fail if time bucket is invalid', async () => {
|
||||||
|
// const { status, body } = await request(app)
|
||||||
|
// .get('/timeline/bucket')
|
||||||
|
// .set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
|
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
|
||||||
|
|
||||||
|
// expect(status).toBe(400);
|
||||||
|
// expect(body).toEqual(errorDto.badRequest);
|
||||||
|
// });
|
||||||
|
|
||||||
|
it('should return time bucket', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/bucket')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -11,9 +11,12 @@ describe(`immich server-info`, () => {
|
|||||||
it('should return the server info', async () => {
|
it('should return the server info', async () => {
|
||||||
const { stderr, stdout, exitCode } = await immichCli(['server-info']);
|
const { stderr, stdout, exitCode } = await immichCli(['server-info']);
|
||||||
expect(stdout.split('\n')).toEqual([
|
expect(stdout.split('\n')).toEqual([
|
||||||
expect.stringContaining('Server Version:'),
|
expect.stringContaining('Server Info (via admin@immich.cloud'),
|
||||||
expect.stringContaining('Image Types:'),
|
' Url: http://127.0.0.1:2283/api',
|
||||||
expect.stringContaining('Video Types:'),
|
expect.stringContaining('Version:'),
|
||||||
|
' Formats:',
|
||||||
|
expect.stringContaining('Images:'),
|
||||||
|
expect.stringContaining('Videos:'),
|
||||||
' Statistics:',
|
' Statistics:',
|
||||||
' Images: 0',
|
' Images: 0',
|
||||||
' Videos: 0',
|
' Videos: 0',
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.99.0"
|
version = "1.100.0"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 129,
|
"android.injected.version.code" => 130,
|
||||||
"android.injected.version.name" => "1.99.0",
|
"android.injected.version.name" => "1.100.0",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00024">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000235">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="81.32752">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="71.774783">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="26.041597">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="32.283066">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||||
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
|
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
|
||||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
|
||||||
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
|
geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461
|
||||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||||
integration_test: 13825b8a9334a850581300559b8839134b124670
|
integration_test: 13825b8a9334a850581300559b8839134b124670
|
||||||
@ -175,4 +175,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.12.1
|
||||||
|
@ -383,7 +383,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 145;
|
CURRENT_PROJECT_VERSION = 146;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -525,7 +525,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 145;
|
CURRENT_PROJECT_VERSION = 146;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -553,7 +553,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 145;
|
CURRENT_PROJECT_VERSION = 146;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
@ -55,11 +55,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.99.0</string>
|
<string>1.100.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>145</string>
|
<string>146</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.99.0"
|
version_number: "1.100.0"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
@ -5,32 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000246">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000226">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.175843">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.183824">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="6.871371">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="3.799845">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.189451">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.185425">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="142.078248">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="111.245268">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="78.774821">
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="72.572736">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
@ -169,6 +169,7 @@ doc/TagTypeEnum.md
|
|||||||
doc/ThumbnailFormat.md
|
doc/ThumbnailFormat.md
|
||||||
doc/TimeBucketResponseDto.md
|
doc/TimeBucketResponseDto.md
|
||||||
doc/TimeBucketSize.md
|
doc/TimeBucketSize.md
|
||||||
|
doc/TimelineApi.md
|
||||||
doc/ToneMapping.md
|
doc/ToneMapping.md
|
||||||
doc/TranscodeHWAccel.md
|
doc/TranscodeHWAccel.md
|
||||||
doc/TranscodePolicy.md
|
doc/TranscodePolicy.md
|
||||||
@ -211,6 +212,7 @@ lib/api/server_info_api.dart
|
|||||||
lib/api/shared_link_api.dart
|
lib/api/shared_link_api.dart
|
||||||
lib/api/system_config_api.dart
|
lib/api/system_config_api.dart
|
||||||
lib/api/tag_api.dart
|
lib/api/tag_api.dart
|
||||||
|
lib/api/timeline_api.dart
|
||||||
lib/api/trash_api.dart
|
lib/api/trash_api.dart
|
||||||
lib/api/user_api.dart
|
lib/api/user_api.dart
|
||||||
lib/api_client.dart
|
lib/api_client.dart
|
||||||
@ -556,6 +558,7 @@ test/tag_type_enum_test.dart
|
|||||||
test/thumbnail_format_test.dart
|
test/thumbnail_format_test.dart
|
||||||
test/time_bucket_response_dto_test.dart
|
test/time_bucket_response_dto_test.dart
|
||||||
test/time_bucket_size_test.dart
|
test/time_bucket_size_test.dart
|
||||||
|
test/timeline_api_test.dart
|
||||||
test/tone_mapping_test.dart
|
test/tone_mapping_test.dart
|
||||||
test/transcode_hw_accel_test.dart
|
test/transcode_hw_accel_test.dart
|
||||||
test/transcode_policy_test.dart
|
test/transcode_policy_test.dart
|
||||||
|
6
mobile/openapi/README.md
generated
6
mobile/openapi/README.md
generated
@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.99.0
|
- API version: 1.100.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@ -105,8 +105,6 @@ Class | Method | HTTP request | Description
|
|||||||
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
*AssetApi* | [**getMapMarkers**](doc//AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
||||||
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
*AssetApi* | [**getMemoryLane**](doc//AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
||||||
*AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random |
|
*AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random |
|
||||||
*AssetApi* | [**getTimeBucket**](doc//AssetApi.md#gettimebucket) | **GET** /asset/time-bucket |
|
|
||||||
*AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
|
|
||||||
*AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs |
|
*AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs |
|
||||||
*AssetApi* | [**searchAssets**](doc//AssetApi.md#searchassets) | **GET** /assets |
|
*AssetApi* | [**searchAssets**](doc//AssetApi.md#searchassets) | **GET** /assets |
|
||||||
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
||||||
@ -199,6 +197,8 @@ Class | Method | HTTP request | Description
|
|||||||
*TagApi* | [**tagAssets**](doc//TagApi.md#tagassets) | **PUT** /tag/{id}/assets |
|
*TagApi* | [**tagAssets**](doc//TagApi.md#tagassets) | **PUT** /tag/{id}/assets |
|
||||||
*TagApi* | [**untagAssets**](doc//TagApi.md#untagassets) | **DELETE** /tag/{id}/assets |
|
*TagApi* | [**untagAssets**](doc//TagApi.md#untagassets) | **DELETE** /tag/{id}/assets |
|
||||||
*TagApi* | [**updateTag**](doc//TagApi.md#updatetag) | **PATCH** /tag/{id} |
|
*TagApi* | [**updateTag**](doc//TagApi.md#updatetag) | **PATCH** /tag/{id} |
|
||||||
|
*TimelineApi* | [**getTimeBucket**](doc//TimelineApi.md#gettimebucket) | **GET** /timeline/bucket |
|
||||||
|
*TimelineApi* | [**getTimeBuckets**](doc//TimelineApi.md#gettimebuckets) | **GET** /timeline/buckets |
|
||||||
*TrashApi* | [**emptyTrash**](doc//TrashApi.md#emptytrash) | **POST** /trash/empty |
|
*TrashApi* | [**emptyTrash**](doc//TrashApi.md#emptytrash) | **POST** /trash/empty |
|
||||||
*TrashApi* | [**restoreAssets**](doc//TrashApi.md#restoreassets) | **POST** /trash/restore/assets |
|
*TrashApi* | [**restoreAssets**](doc//TrashApi.md#restoreassets) | **POST** /trash/restore/assets |
|
||||||
*TrashApi* | [**restoreTrash**](doc//TrashApi.md#restoretrash) | **POST** /trash/restore |
|
*TrashApi* | [**restoreTrash**](doc//TrashApi.md#restoretrash) | **POST** /trash/restore |
|
||||||
|
154
mobile/openapi/doc/AssetApi.md
generated
154
mobile/openapi/doc/AssetApi.md
generated
@ -23,8 +23,6 @@ Method | HTTP request | Description
|
|||||||
[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
[**getMapMarkers**](AssetApi.md#getmapmarkers) | **GET** /asset/map-marker |
|
||||||
[**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
[**getMemoryLane**](AssetApi.md#getmemorylane) | **GET** /asset/memory-lane |
|
||||||
[**getRandom**](AssetApi.md#getrandom) | **GET** /asset/random |
|
[**getRandom**](AssetApi.md#getrandom) | **GET** /asset/random |
|
||||||
[**getTimeBucket**](AssetApi.md#gettimebucket) | **GET** /asset/time-bucket |
|
|
||||||
[**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets |
|
|
||||||
[**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs |
|
[**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs |
|
||||||
[**searchAssets**](AssetApi.md#searchassets) | **GET** /assets |
|
[**searchAssets**](AssetApi.md#searchassets) | **GET** /assets |
|
||||||
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |
|
||||||
@ -833,158 +831,6 @@ Name | Type | Description | Notes
|
|||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **getTimeBucket**
|
|
||||||
> List<AssetResponseDto> getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Example
|
|
||||||
```dart
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
// TODO Configure API key authorization: cookie
|
|
||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
|
||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
|
||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
|
||||||
// TODO Configure API key authorization: api_key
|
|
||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
|
||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
|
||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
|
||||||
// TODO Configure HTTP Bearer authorization: bearer
|
|
||||||
// Case 1. Use String Token
|
|
||||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
|
||||||
// Case 2. Use Function which generate token.
|
|
||||||
// String yourTokenGeneratorFunction() { ... }
|
|
||||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
|
||||||
|
|
||||||
final api_instance = AssetApi();
|
|
||||||
final size = ; // TimeBucketSize |
|
|
||||||
final timeBucket = timeBucket_example; // String |
|
|
||||||
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
|
||||||
final isArchived = true; // bool |
|
|
||||||
final isFavorite = true; // bool |
|
|
||||||
final isTrashed = true; // bool |
|
|
||||||
final key = key_example; // String |
|
|
||||||
final order = ; // AssetOrder |
|
|
||||||
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
|
||||||
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
|
||||||
final withPartners = true; // bool |
|
|
||||||
final withStacked = true; // bool |
|
|
||||||
|
|
||||||
try {
|
|
||||||
final result = api_instance.getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked);
|
|
||||||
print(result);
|
|
||||||
} catch (e) {
|
|
||||||
print('Exception when calling AssetApi->getTimeBucket: $e\n');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Name | Type | Description | Notes
|
|
||||||
------------- | ------------- | ------------- | -------------
|
|
||||||
**size** | [**TimeBucketSize**](.md)| |
|
|
||||||
**timeBucket** | **String**| |
|
|
||||||
**albumId** | **String**| | [optional]
|
|
||||||
**isArchived** | **bool**| | [optional]
|
|
||||||
**isFavorite** | **bool**| | [optional]
|
|
||||||
**isTrashed** | **bool**| | [optional]
|
|
||||||
**key** | **String**| | [optional]
|
|
||||||
**order** | [**AssetOrder**](.md)| | [optional]
|
|
||||||
**personId** | **String**| | [optional]
|
|
||||||
**userId** | **String**| | [optional]
|
|
||||||
**withPartners** | **bool**| | [optional]
|
|
||||||
**withStacked** | **bool**| | [optional]
|
|
||||||
|
|
||||||
### Return type
|
|
||||||
|
|
||||||
[**List<AssetResponseDto>**](AssetResponseDto.md)
|
|
||||||
|
|
||||||
### Authorization
|
|
||||||
|
|
||||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
|
||||||
|
|
||||||
### HTTP request headers
|
|
||||||
|
|
||||||
- **Content-Type**: Not defined
|
|
||||||
- **Accept**: application/json
|
|
||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
|
||||||
|
|
||||||
# **getTimeBuckets**
|
|
||||||
> List<TimeBucketResponseDto> getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Example
|
|
||||||
```dart
|
|
||||||
import 'package:openapi/api.dart';
|
|
||||||
// TODO Configure API key authorization: cookie
|
|
||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
|
||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
|
||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
|
||||||
// TODO Configure API key authorization: api_key
|
|
||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
|
||||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
|
||||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
|
||||||
// TODO Configure HTTP Bearer authorization: bearer
|
|
||||||
// Case 1. Use String Token
|
|
||||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
|
||||||
// Case 2. Use Function which generate token.
|
|
||||||
// String yourTokenGeneratorFunction() { ... }
|
|
||||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
|
||||||
|
|
||||||
final api_instance = AssetApi();
|
|
||||||
final size = ; // TimeBucketSize |
|
|
||||||
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
|
||||||
final isArchived = true; // bool |
|
|
||||||
final isFavorite = true; // bool |
|
|
||||||
final isTrashed = true; // bool |
|
|
||||||
final key = key_example; // String |
|
|
||||||
final order = ; // AssetOrder |
|
|
||||||
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
|
||||||
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
|
||||||
final withPartners = true; // bool |
|
|
||||||
final withStacked = true; // bool |
|
|
||||||
|
|
||||||
try {
|
|
||||||
final result = api_instance.getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked);
|
|
||||||
print(result);
|
|
||||||
} catch (e) {
|
|
||||||
print('Exception when calling AssetApi->getTimeBuckets: $e\n');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Parameters
|
|
||||||
|
|
||||||
Name | Type | Description | Notes
|
|
||||||
------------- | ------------- | ------------- | -------------
|
|
||||||
**size** | [**TimeBucketSize**](.md)| |
|
|
||||||
**albumId** | **String**| | [optional]
|
|
||||||
**isArchived** | **bool**| | [optional]
|
|
||||||
**isFavorite** | **bool**| | [optional]
|
|
||||||
**isTrashed** | **bool**| | [optional]
|
|
||||||
**key** | **String**| | [optional]
|
|
||||||
**order** | [**AssetOrder**](.md)| | [optional]
|
|
||||||
**personId** | **String**| | [optional]
|
|
||||||
**userId** | **String**| | [optional]
|
|
||||||
**withPartners** | **bool**| | [optional]
|
|
||||||
**withStacked** | **bool**| | [optional]
|
|
||||||
|
|
||||||
### Return type
|
|
||||||
|
|
||||||
[**List<TimeBucketResponseDto>**](TimeBucketResponseDto.md)
|
|
||||||
|
|
||||||
### Authorization
|
|
||||||
|
|
||||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
|
||||||
|
|
||||||
### HTTP request headers
|
|
||||||
|
|
||||||
- **Content-Type**: Not defined
|
|
||||||
- **Accept**: application/json
|
|
||||||
|
|
||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
|
||||||
|
|
||||||
# **runAssetJobs**
|
# **runAssetJobs**
|
||||||
> runAssetJobs(assetJobsDto)
|
> runAssetJobs(assetJobsDto)
|
||||||
|
|
||||||
|
1
mobile/openapi/doc/MemoryLaneResponseDto.md
generated
1
mobile/openapi/doc/MemoryLaneResponseDto.md
generated
@ -10,6 +10,7 @@ Name | Type | Description | Notes
|
|||||||
------------ | ------------- | ------------- | -------------
|
------------ | ------------- | ------------- | -------------
|
||||||
**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
|
**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
|
||||||
**title** | **String** | |
|
**title** | **String** | |
|
||||||
|
**yearsAgo** | **int** | |
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
6
mobile/openapi/doc/SystemConfigApi.md
generated
6
mobile/openapi/doc/SystemConfigApi.md
generated
@ -119,7 +119,7 @@ This endpoint does not need any parameter.
|
|||||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
# **getMapStyle**
|
# **getMapStyle**
|
||||||
> Object getMapStyle(theme)
|
> Object getMapStyle(theme, key)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -143,9 +143,10 @@ import 'package:openapi/api.dart';
|
|||||||
|
|
||||||
final api_instance = SystemConfigApi();
|
final api_instance = SystemConfigApi();
|
||||||
final theme = ; // MapTheme |
|
final theme = ; // MapTheme |
|
||||||
|
final key = key_example; // String |
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final result = api_instance.getMapStyle(theme);
|
final result = api_instance.getMapStyle(theme, key);
|
||||||
print(result);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling SystemConfigApi->getMapStyle: $e\n');
|
print('Exception when calling SystemConfigApi->getMapStyle: $e\n');
|
||||||
@ -157,6 +158,7 @@ try {
|
|||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------- | ------------- | ------------- | -------------
|
------------- | ------------- | ------------- | -------------
|
||||||
**theme** | [**MapTheme**](.md)| |
|
**theme** | [**MapTheme**](.md)| |
|
||||||
|
**key** | **String**| | [optional]
|
||||||
|
|
||||||
### Return type
|
### Return type
|
||||||
|
|
||||||
|
167
mobile/openapi/doc/TimelineApi.md
generated
Normal file
167
mobile/openapi/doc/TimelineApi.md
generated
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# openapi.api.TimelineApi
|
||||||
|
|
||||||
|
## Load the API package
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
```
|
||||||
|
|
||||||
|
All URIs are relative to */api*
|
||||||
|
|
||||||
|
Method | HTTP request | Description
|
||||||
|
------------- | ------------- | -------------
|
||||||
|
[**getTimeBucket**](TimelineApi.md#gettimebucket) | **GET** /timeline/bucket |
|
||||||
|
[**getTimeBuckets**](TimelineApi.md#gettimebuckets) | **GET** /timeline/buckets |
|
||||||
|
|
||||||
|
|
||||||
|
# **getTimeBucket**
|
||||||
|
> List<AssetResponseDto> getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure API key authorization: cookie
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure API key authorization: api_key
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = TimelineApi();
|
||||||
|
final size = ; // TimeBucketSize |
|
||||||
|
final timeBucket = timeBucket_example; // String |
|
||||||
|
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||||
|
final isArchived = true; // bool |
|
||||||
|
final isFavorite = true; // bool |
|
||||||
|
final isTrashed = true; // bool |
|
||||||
|
final key = key_example; // String |
|
||||||
|
final order = ; // AssetOrder |
|
||||||
|
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||||
|
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||||
|
final withPartners = true; // bool |
|
||||||
|
final withStacked = true; // bool |
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked);
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling TimelineApi->getTimeBucket: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**size** | [**TimeBucketSize**](.md)| |
|
||||||
|
**timeBucket** | **String**| |
|
||||||
|
**albumId** | **String**| | [optional]
|
||||||
|
**isArchived** | **bool**| | [optional]
|
||||||
|
**isFavorite** | **bool**| | [optional]
|
||||||
|
**isTrashed** | **bool**| | [optional]
|
||||||
|
**key** | **String**| | [optional]
|
||||||
|
**order** | [**AssetOrder**](.md)| | [optional]
|
||||||
|
**personId** | **String**| | [optional]
|
||||||
|
**userId** | **String**| | [optional]
|
||||||
|
**withPartners** | **bool**| | [optional]
|
||||||
|
**withStacked** | **bool**| | [optional]
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**List<AssetResponseDto>**](AssetResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
# **getTimeBuckets**
|
||||||
|
> List<TimeBucketResponseDto> getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Example
|
||||||
|
```dart
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
// TODO Configure API key authorization: cookie
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure API key authorization: api_key
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||||
|
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||||
|
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||||
|
// TODO Configure HTTP Bearer authorization: bearer
|
||||||
|
// Case 1. Use String Token
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||||
|
// Case 2. Use Function which generate token.
|
||||||
|
// String yourTokenGeneratorFunction() { ... }
|
||||||
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
|
final api_instance = TimelineApi();
|
||||||
|
final size = ; // TimeBucketSize |
|
||||||
|
final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||||
|
final isArchived = true; // bool |
|
||||||
|
final isFavorite = true; // bool |
|
||||||
|
final isTrashed = true; // bool |
|
||||||
|
final key = key_example; // String |
|
||||||
|
final order = ; // AssetOrder |
|
||||||
|
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||||
|
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||||
|
final withPartners = true; // bool |
|
||||||
|
final withStacked = true; // bool |
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = api_instance.getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked);
|
||||||
|
print(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('Exception when calling TimelineApi->getTimeBuckets: $e\n');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
Name | Type | Description | Notes
|
||||||
|
------------- | ------------- | ------------- | -------------
|
||||||
|
**size** | [**TimeBucketSize**](.md)| |
|
||||||
|
**albumId** | **String**| | [optional]
|
||||||
|
**isArchived** | **bool**| | [optional]
|
||||||
|
**isFavorite** | **bool**| | [optional]
|
||||||
|
**isTrashed** | **bool**| | [optional]
|
||||||
|
**key** | **String**| | [optional]
|
||||||
|
**order** | [**AssetOrder**](.md)| | [optional]
|
||||||
|
**personId** | **String**| | [optional]
|
||||||
|
**userId** | **String**| | [optional]
|
||||||
|
**withPartners** | **bool**| | [optional]
|
||||||
|
**withStacked** | **bool**| | [optional]
|
||||||
|
|
||||||
|
### Return type
|
||||||
|
|
||||||
|
[**List<TimeBucketResponseDto>**](TimeBucketResponseDto.md)
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||||
|
|
||||||
|
### HTTP request headers
|
||||||
|
|
||||||
|
- **Content-Type**: Not defined
|
||||||
|
- **Accept**: application/json
|
||||||
|
|
||||||
|
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||||
|
|
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@ -47,6 +47,7 @@ part 'api/server_info_api.dart';
|
|||||||
part 'api/shared_link_api.dart';
|
part 'api/shared_link_api.dart';
|
||||||
part 'api/system_config_api.dart';
|
part 'api/system_config_api.dart';
|
||||||
part 'api/tag_api.dart';
|
part 'api/tag_api.dart';
|
||||||
|
part 'api/timeline_api.dart';
|
||||||
part 'api/trash_api.dart';
|
part 'api/trash_api.dart';
|
||||||
part 'api/user_api.dart';
|
part 'api/user_api.dart';
|
||||||
|
|
||||||
|
249
mobile/openapi/lib/api/asset_api.dart
generated
249
mobile/openapi/lib/api/asset_api.dart
generated
@ -835,255 +835,6 @@ class AssetApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /asset/time-bucket' operation and returns the [Response].
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [TimeBucketSize] size (required):
|
|
||||||
///
|
|
||||||
/// * [String] timeBucket (required):
|
|
||||||
///
|
|
||||||
/// * [String] albumId:
|
|
||||||
///
|
|
||||||
/// * [bool] isArchived:
|
|
||||||
///
|
|
||||||
/// * [bool] isFavorite:
|
|
||||||
///
|
|
||||||
/// * [bool] isTrashed:
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
///
|
|
||||||
/// * [AssetOrder] order:
|
|
||||||
///
|
|
||||||
/// * [String] personId:
|
|
||||||
///
|
|
||||||
/// * [String] userId:
|
|
||||||
///
|
|
||||||
/// * [bool] withPartners:
|
|
||||||
///
|
|
||||||
/// * [bool] withStacked:
|
|
||||||
Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final path = r'/asset/time-bucket';
|
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
|
||||||
Object? postBody;
|
|
||||||
|
|
||||||
final queryParams = <QueryParam>[];
|
|
||||||
final headerParams = <String, String>{};
|
|
||||||
final formParams = <String, String>{};
|
|
||||||
|
|
||||||
if (albumId != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'albumId', albumId));
|
|
||||||
}
|
|
||||||
if (isArchived != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
|
|
||||||
}
|
|
||||||
if (isFavorite != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
|
||||||
}
|
|
||||||
if (isTrashed != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
|
|
||||||
}
|
|
||||||
if (key != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
|
||||||
}
|
|
||||||
if (order != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'order', order));
|
|
||||||
}
|
|
||||||
if (personId != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'personId', personId));
|
|
||||||
}
|
|
||||||
queryParams.addAll(_queryParams('', 'size', size));
|
|
||||||
queryParams.addAll(_queryParams('', 'timeBucket', timeBucket));
|
|
||||||
if (userId != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'userId', userId));
|
|
||||||
}
|
|
||||||
if (withPartners != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
|
|
||||||
}
|
|
||||||
if (withStacked != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'withStacked', withStacked));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
|
||||||
|
|
||||||
|
|
||||||
return apiClient.invokeAPI(
|
|
||||||
path,
|
|
||||||
'GET',
|
|
||||||
queryParams,
|
|
||||||
postBody,
|
|
||||||
headerParams,
|
|
||||||
formParams,
|
|
||||||
contentTypes.isEmpty ? null : contentTypes.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [TimeBucketSize] size (required):
|
|
||||||
///
|
|
||||||
/// * [String] timeBucket (required):
|
|
||||||
///
|
|
||||||
/// * [String] albumId:
|
|
||||||
///
|
|
||||||
/// * [bool] isArchived:
|
|
||||||
///
|
|
||||||
/// * [bool] isFavorite:
|
|
||||||
///
|
|
||||||
/// * [bool] isTrashed:
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
///
|
|
||||||
/// * [AssetOrder] order:
|
|
||||||
///
|
|
||||||
/// * [String] personId:
|
|
||||||
///
|
|
||||||
/// * [String] userId:
|
|
||||||
///
|
|
||||||
/// * [bool] withPartners:
|
|
||||||
///
|
|
||||||
/// * [bool] withStacked:
|
|
||||||
Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
|
||||||
final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
|
||||||
}
|
|
||||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
|
||||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
|
||||||
// FormatException when trying to decode an empty string.
|
|
||||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
|
||||||
final responseBody = await _decodeBodyBytes(response);
|
|
||||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
|
||||||
.cast<AssetResponseDto>()
|
|
||||||
.toList(growable: false);
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /asset/time-buckets' operation and returns the [Response].
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [TimeBucketSize] size (required):
|
|
||||||
///
|
|
||||||
/// * [String] albumId:
|
|
||||||
///
|
|
||||||
/// * [bool] isArchived:
|
|
||||||
///
|
|
||||||
/// * [bool] isFavorite:
|
|
||||||
///
|
|
||||||
/// * [bool] isTrashed:
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
///
|
|
||||||
/// * [AssetOrder] order:
|
|
||||||
///
|
|
||||||
/// * [String] personId:
|
|
||||||
///
|
|
||||||
/// * [String] userId:
|
|
||||||
///
|
|
||||||
/// * [bool] withPartners:
|
|
||||||
///
|
|
||||||
/// * [bool] withStacked:
|
|
||||||
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
|
||||||
// ignore: prefer_const_declarations
|
|
||||||
final path = r'/asset/time-buckets';
|
|
||||||
|
|
||||||
// ignore: prefer_final_locals
|
|
||||||
Object? postBody;
|
|
||||||
|
|
||||||
final queryParams = <QueryParam>[];
|
|
||||||
final headerParams = <String, String>{};
|
|
||||||
final formParams = <String, String>{};
|
|
||||||
|
|
||||||
if (albumId != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'albumId', albumId));
|
|
||||||
}
|
|
||||||
if (isArchived != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
|
|
||||||
}
|
|
||||||
if (isFavorite != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
|
||||||
}
|
|
||||||
if (isTrashed != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
|
|
||||||
}
|
|
||||||
if (key != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'key', key));
|
|
||||||
}
|
|
||||||
if (order != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'order', order));
|
|
||||||
}
|
|
||||||
if (personId != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'personId', personId));
|
|
||||||
}
|
|
||||||
queryParams.addAll(_queryParams('', 'size', size));
|
|
||||||
if (userId != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'userId', userId));
|
|
||||||
}
|
|
||||||
if (withPartners != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
|
|
||||||
}
|
|
||||||
if (withStacked != null) {
|
|
||||||
queryParams.addAll(_queryParams('', 'withStacked', withStacked));
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
|
||||||
|
|
||||||
|
|
||||||
return apiClient.invokeAPI(
|
|
||||||
path,
|
|
||||||
'GET',
|
|
||||||
queryParams,
|
|
||||||
postBody,
|
|
||||||
headerParams,
|
|
||||||
formParams,
|
|
||||||
contentTypes.isEmpty ? null : contentTypes.first,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters:
|
|
||||||
///
|
|
||||||
/// * [TimeBucketSize] size (required):
|
|
||||||
///
|
|
||||||
/// * [String] albumId:
|
|
||||||
///
|
|
||||||
/// * [bool] isArchived:
|
|
||||||
///
|
|
||||||
/// * [bool] isFavorite:
|
|
||||||
///
|
|
||||||
/// * [bool] isTrashed:
|
|
||||||
///
|
|
||||||
/// * [String] key:
|
|
||||||
///
|
|
||||||
/// * [AssetOrder] order:
|
|
||||||
///
|
|
||||||
/// * [String] personId:
|
|
||||||
///
|
|
||||||
/// * [String] userId:
|
|
||||||
///
|
|
||||||
/// * [bool] withPartners:
|
|
||||||
///
|
|
||||||
/// * [bool] withStacked:
|
|
||||||
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
|
||||||
final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
|
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
|
||||||
}
|
|
||||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
|
||||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
|
||||||
// FormatException when trying to decode an empty string.
|
|
||||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
|
||||||
final responseBody = await _decodeBodyBytes(response);
|
|
||||||
return (await apiClient.deserializeAsync(responseBody, 'List<TimeBucketResponseDto>') as List)
|
|
||||||
.cast<TimeBucketResponseDto>()
|
|
||||||
.toList(growable: false);
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs an HTTP 'POST /asset/jobs' operation and returns the [Response].
|
/// Performs an HTTP 'POST /asset/jobs' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
13
mobile/openapi/lib/api/system_config_api.dart
generated
13
mobile/openapi/lib/api/system_config_api.dart
generated
@ -102,7 +102,9 @@ class SystemConfigApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [MapTheme] theme (required):
|
/// * [MapTheme] theme (required):
|
||||||
Future<Response> getMapStyleWithHttpInfo(MapTheme theme,) async {
|
///
|
||||||
|
/// * [String] key:
|
||||||
|
Future<Response> getMapStyleWithHttpInfo(MapTheme theme, { String? key, }) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/system-config/map/style.json';
|
final path = r'/system-config/map/style.json';
|
||||||
|
|
||||||
@ -113,6 +115,9 @@ class SystemConfigApi {
|
|||||||
final headerParams = <String, String>{};
|
final headerParams = <String, String>{};
|
||||||
final formParams = <String, String>{};
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (key != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
|
}
|
||||||
queryParams.addAll(_queryParams('', 'theme', theme));
|
queryParams.addAll(_queryParams('', 'theme', theme));
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
const contentTypes = <String>[];
|
||||||
@ -132,8 +137,10 @@ class SystemConfigApi {
|
|||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [MapTheme] theme (required):
|
/// * [MapTheme] theme (required):
|
||||||
Future<Object?> getMapStyle(MapTheme theme,) async {
|
///
|
||||||
final response = await getMapStyleWithHttpInfo(theme,);
|
/// * [String] key:
|
||||||
|
Future<Object?> getMapStyle(MapTheme theme, { String? key, }) async {
|
||||||
|
final response = await getMapStyleWithHttpInfo(theme, key: key, );
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
}
|
}
|
||||||
|
267
mobile/openapi/lib/api/timeline_api.dart
generated
Normal file
267
mobile/openapi/lib/api/timeline_api.dart
generated
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
|
||||||
|
class TimelineApi {
|
||||||
|
TimelineApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||||
|
|
||||||
|
final ApiClient apiClient;
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /timeline/bucket' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [TimeBucketSize] size (required):
|
||||||
|
///
|
||||||
|
/// * [String] timeBucket (required):
|
||||||
|
///
|
||||||
|
/// * [String] albumId:
|
||||||
|
///
|
||||||
|
/// * [bool] isArchived:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] isTrashed:
|
||||||
|
///
|
||||||
|
/// * [String] key:
|
||||||
|
///
|
||||||
|
/// * [AssetOrder] order:
|
||||||
|
///
|
||||||
|
/// * [String] personId:
|
||||||
|
///
|
||||||
|
/// * [String] userId:
|
||||||
|
///
|
||||||
|
/// * [bool] withPartners:
|
||||||
|
///
|
||||||
|
/// * [bool] withStacked:
|
||||||
|
Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/timeline/bucket';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (albumId != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'albumId', albumId));
|
||||||
|
}
|
||||||
|
if (isArchived != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
|
||||||
|
}
|
||||||
|
if (isFavorite != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||||
|
}
|
||||||
|
if (isTrashed != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
|
||||||
|
}
|
||||||
|
if (key != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
|
}
|
||||||
|
if (order != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'order', order));
|
||||||
|
}
|
||||||
|
if (personId != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'personId', personId));
|
||||||
|
}
|
||||||
|
queryParams.addAll(_queryParams('', 'size', size));
|
||||||
|
queryParams.addAll(_queryParams('', 'timeBucket', timeBucket));
|
||||||
|
if (userId != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'userId', userId));
|
||||||
|
}
|
||||||
|
if (withPartners != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
|
||||||
|
}
|
||||||
|
if (withStacked != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'withStacked', withStacked));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [TimeBucketSize] size (required):
|
||||||
|
///
|
||||||
|
/// * [String] timeBucket (required):
|
||||||
|
///
|
||||||
|
/// * [String] albumId:
|
||||||
|
///
|
||||||
|
/// * [bool] isArchived:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] isTrashed:
|
||||||
|
///
|
||||||
|
/// * [String] key:
|
||||||
|
///
|
||||||
|
/// * [AssetOrder] order:
|
||||||
|
///
|
||||||
|
/// * [String] personId:
|
||||||
|
///
|
||||||
|
/// * [String] userId:
|
||||||
|
///
|
||||||
|
/// * [bool] withPartners:
|
||||||
|
///
|
||||||
|
/// * [bool] withStacked:
|
||||||
|
Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
||||||
|
final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
|
||||||
|
.cast<AssetResponseDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'GET /timeline/buckets' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [TimeBucketSize] size (required):
|
||||||
|
///
|
||||||
|
/// * [String] albumId:
|
||||||
|
///
|
||||||
|
/// * [bool] isArchived:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] isTrashed:
|
||||||
|
///
|
||||||
|
/// * [String] key:
|
||||||
|
///
|
||||||
|
/// * [AssetOrder] order:
|
||||||
|
///
|
||||||
|
/// * [String] personId:
|
||||||
|
///
|
||||||
|
/// * [String] userId:
|
||||||
|
///
|
||||||
|
/// * [bool] withPartners:
|
||||||
|
///
|
||||||
|
/// * [bool] withStacked:
|
||||||
|
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final path = r'/timeline/buckets';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (albumId != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'albumId', albumId));
|
||||||
|
}
|
||||||
|
if (isArchived != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
|
||||||
|
}
|
||||||
|
if (isFavorite != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||||
|
}
|
||||||
|
if (isTrashed != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
|
||||||
|
}
|
||||||
|
if (key != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
|
}
|
||||||
|
if (order != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'order', order));
|
||||||
|
}
|
||||||
|
if (personId != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'personId', personId));
|
||||||
|
}
|
||||||
|
queryParams.addAll(_queryParams('', 'size', size));
|
||||||
|
if (userId != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'userId', userId));
|
||||||
|
}
|
||||||
|
if (withPartners != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
|
||||||
|
}
|
||||||
|
if (withStacked != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'withStacked', withStacked));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
path,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [TimeBucketSize] size (required):
|
||||||
|
///
|
||||||
|
/// * [String] albumId:
|
||||||
|
///
|
||||||
|
/// * [bool] isArchived:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] isTrashed:
|
||||||
|
///
|
||||||
|
/// * [String] key:
|
||||||
|
///
|
||||||
|
/// * [AssetOrder] order:
|
||||||
|
///
|
||||||
|
/// * [String] personId:
|
||||||
|
///
|
||||||
|
/// * [String] userId:
|
||||||
|
///
|
||||||
|
/// * [bool] withPartners:
|
||||||
|
///
|
||||||
|
/// * [bool] withStacked:
|
||||||
|
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
|
||||||
|
final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
final responseBody = await _decodeBodyBytes(response);
|
||||||
|
return (await apiClient.deserializeAsync(responseBody, 'List<TimeBucketResponseDto>') as List)
|
||||||
|
.cast<TimeBucketResponseDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -15,30 +15,36 @@ class MemoryLaneResponseDto {
|
|||||||
MemoryLaneResponseDto({
|
MemoryLaneResponseDto({
|
||||||
this.assets = const [],
|
this.assets = const [],
|
||||||
required this.title,
|
required this.title,
|
||||||
|
required this.yearsAgo,
|
||||||
});
|
});
|
||||||
|
|
||||||
List<AssetResponseDto> assets;
|
List<AssetResponseDto> assets;
|
||||||
|
|
||||||
String title;
|
String title;
|
||||||
|
|
||||||
|
int yearsAgo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is MemoryLaneResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is MemoryLaneResponseDto &&
|
||||||
_deepEquality.equals(other.assets, assets) &&
|
_deepEquality.equals(other.assets, assets) &&
|
||||||
other.title == title;
|
other.title == title &&
|
||||||
|
other.yearsAgo == yearsAgo;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
// ignore: unnecessary_parenthesis
|
// ignore: unnecessary_parenthesis
|
||||||
(assets.hashCode) +
|
(assets.hashCode) +
|
||||||
(title.hashCode);
|
(title.hashCode) +
|
||||||
|
(yearsAgo.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'MemoryLaneResponseDto[assets=$assets, title=$title]';
|
String toString() => 'MemoryLaneResponseDto[assets=$assets, title=$title, yearsAgo=$yearsAgo]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
json[r'assets'] = this.assets;
|
json[r'assets'] = this.assets;
|
||||||
json[r'title'] = this.title;
|
json[r'title'] = this.title;
|
||||||
|
json[r'yearsAgo'] = this.yearsAgo;
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,6 +58,7 @@ class MemoryLaneResponseDto {
|
|||||||
return MemoryLaneResponseDto(
|
return MemoryLaneResponseDto(
|
||||||
assets: AssetResponseDto.listFromJson(json[r'assets']),
|
assets: AssetResponseDto.listFromJson(json[r'assets']),
|
||||||
title: mapValueOfType<String>(json, r'title')!,
|
title: mapValueOfType<String>(json, r'title')!,
|
||||||
|
yearsAgo: mapValueOfType<int>(json, r'yearsAgo')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -101,6 +108,7 @@ class MemoryLaneResponseDto {
|
|||||||
static const requiredKeys = <String>{
|
static const requiredKeys = <String>{
|
||||||
'assets',
|
'assets',
|
||||||
'title',
|
'title',
|
||||||
|
'yearsAgo',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
mobile/openapi/test/asset_api_test.dart
generated
10
mobile/openapi/test/asset_api_test.dart
generated
@ -95,16 +95,6 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<List<AssetResponseDto>> getTimeBucket(TimeBucketSize size, String timeBucket, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async
|
|
||||||
test('test getTimeBucket', () async {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
//Future<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async
|
|
||||||
test('test getTimeBuckets', () async {
|
|
||||||
// TODO
|
|
||||||
});
|
|
||||||
|
|
||||||
//Future runAssetJobs(AssetJobsDto assetJobsDto) async
|
//Future runAssetJobs(AssetJobsDto assetJobsDto) async
|
||||||
test('test runAssetJobs', () async {
|
test('test runAssetJobs', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -26,6 +26,11 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// int yearsAgo
|
||||||
|
test('to test the property `yearsAgo`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
2
mobile/openapi/test/system_config_api_test.dart
generated
2
mobile/openapi/test/system_config_api_test.dart
generated
@ -27,7 +27,7 @@ void main() {
|
|||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<Object> getMapStyle(MapTheme theme) async
|
//Future<Object> getMapStyle(MapTheme theme, { String key }) async
|
||||||
test('test getMapStyle', () async {
|
test('test getMapStyle', () async {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
31
mobile/openapi/test/timeline_api_test.dart
generated
Normal file
31
mobile/openapi/test/timeline_api_test.dart
generated
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.12
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
|
||||||
|
/// tests for TimelineApi
|
||||||
|
void main() {
|
||||||
|
// final instance = TimelineApi();
|
||||||
|
|
||||||
|
group('tests for TimelineApi', () {
|
||||||
|
//Future<List<AssetResponseDto>> getTimeBucket(TimeBucketSize size, String timeBucket, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async
|
||||||
|
test('test getTimeBucket', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
//Future<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async
|
||||||
|
test('test getTimeBuckets', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.99.0+129
|
version: 1.100.0+130
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
@ -1720,286 +1720,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/asset/time-bucket": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getTimeBucket",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "albumId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isArchived",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isFavorite",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isTrashed",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "key",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "order",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AssetOrder"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "personId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "size",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TimeBucketSize"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "timeBucket",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withPartners",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withStacked",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/AssetResponseDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Asset"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/asset/time-buckets": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "getTimeBuckets",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "albumId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isArchived",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isFavorite",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "isTrashed",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "key",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "order",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/AssetOrder"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "personId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "size",
|
|
||||||
"required": true,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/TimeBucketSize"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "userId",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"format": "uuid",
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withPartners",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "withStacked",
|
|
||||||
"required": false,
|
|
||||||
"in": "query",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/TimeBucketResponseDto"
|
|
||||||
},
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"bearer": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"cookie": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"api_key": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"tags": [
|
|
||||||
"Asset"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/asset/upload": {
|
"/asset/upload": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "uploadFile",
|
"operationId": "uploadFile",
|
||||||
@ -5628,6 +5348,14 @@
|
|||||||
"get": {
|
"get": {
|
||||||
"operationId": "getMapStyle",
|
"operationId": "getMapStyle",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "theme",
|
"name": "theme",
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -6048,6 +5776,286 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/timeline/bucket": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getTimeBucket",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "albumId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isArchived",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isFavorite",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isTrashed",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "order",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssetOrder"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TimeBucketSize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "timeBucket",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withPartners",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withStacked",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AssetResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Timeline"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/timeline/buckets": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getTimeBuckets",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "albumId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isArchived",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isFavorite",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isTrashed",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "order",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssetOrder"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/TimeBucketSize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "userId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withPartners",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withStacked",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/TimeBucketResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Timeline"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/trash/empty": {
|
"/trash/empty": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "emptyTrash",
|
"operationId": "emptyTrash",
|
||||||
@ -6538,7 +6546,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@ -8427,12 +8435,17 @@
|
|||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
"deprecated": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"yearsAgo": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"assets",
|
"assets",
|
||||||
"title"
|
"title",
|
||||||
|
"yearsAgo"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.99.0
|
* 1.100.0
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
@ -273,6 +273,7 @@ export type MapMarkerResponseDto = {
|
|||||||
export type MemoryLaneResponseDto = {
|
export type MemoryLaneResponseDto = {
|
||||||
assets: AssetResponseDto[];
|
assets: AssetResponseDto[];
|
||||||
title: string;
|
title: string;
|
||||||
|
yearsAgo: number;
|
||||||
};
|
};
|
||||||
export type UpdateStackParentDto = {
|
export type UpdateStackParentDto = {
|
||||||
newParentId: string;
|
newParentId: string;
|
||||||
@ -283,10 +284,6 @@ export type AssetStatsResponseDto = {
|
|||||||
total: number;
|
total: number;
|
||||||
videos: number;
|
videos: number;
|
||||||
};
|
};
|
||||||
export type TimeBucketResponseDto = {
|
|
||||||
count: number;
|
|
||||||
timeBucket: string;
|
|
||||||
};
|
|
||||||
export type CreateAssetDto = {
|
export type CreateAssetDto = {
|
||||||
assetData: Blob;
|
assetData: Blob;
|
||||||
deviceAssetId: string;
|
deviceAssetId: string;
|
||||||
@ -970,6 +967,10 @@ export type CreateTagDto = {
|
|||||||
export type UpdateTagDto = {
|
export type UpdateTagDto = {
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
|
export type TimeBucketResponseDto = {
|
||||||
|
count: number;
|
||||||
|
timeBucket: string;
|
||||||
|
};
|
||||||
export type CreateUserDto = {
|
export type CreateUserDto = {
|
||||||
email: string;
|
email: string;
|
||||||
memoriesEnabled?: boolean;
|
memoriesEnabled?: boolean;
|
||||||
@ -1455,72 +1456,6 @@ export function getAssetThumbnail({ format, id, key }: {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: {
|
|
||||||
albumId?: string;
|
|
||||||
isArchived?: boolean;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
isTrashed?: boolean;
|
|
||||||
key?: string;
|
|
||||||
order?: AssetOrder;
|
|
||||||
personId?: string;
|
|
||||||
size: TimeBucketSize;
|
|
||||||
timeBucket: string;
|
|
||||||
userId?: string;
|
|
||||||
withPartners?: boolean;
|
|
||||||
withStacked?: boolean;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: AssetResponseDto[];
|
|
||||||
}>(`/asset/time-bucket${QS.query(QS.explode({
|
|
||||||
albumId,
|
|
||||||
isArchived,
|
|
||||||
isFavorite,
|
|
||||||
isTrashed,
|
|
||||||
key,
|
|
||||||
order,
|
|
||||||
personId,
|
|
||||||
size,
|
|
||||||
timeBucket,
|
|
||||||
userId,
|
|
||||||
withPartners,
|
|
||||||
withStacked
|
|
||||||
}))}`, {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: {
|
|
||||||
albumId?: string;
|
|
||||||
isArchived?: boolean;
|
|
||||||
isFavorite?: boolean;
|
|
||||||
isTrashed?: boolean;
|
|
||||||
key?: string;
|
|
||||||
order?: AssetOrder;
|
|
||||||
personId?: string;
|
|
||||||
size: TimeBucketSize;
|
|
||||||
userId?: string;
|
|
||||||
withPartners?: boolean;
|
|
||||||
withStacked?: boolean;
|
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
|
||||||
status: 200;
|
|
||||||
data: TimeBucketResponseDto[];
|
|
||||||
}>(`/asset/time-buckets${QS.query(QS.explode({
|
|
||||||
albumId,
|
|
||||||
isArchived,
|
|
||||||
isFavorite,
|
|
||||||
isTrashed,
|
|
||||||
key,
|
|
||||||
order,
|
|
||||||
personId,
|
|
||||||
size,
|
|
||||||
userId,
|
|
||||||
withPartners,
|
|
||||||
withStacked
|
|
||||||
}))}`, {
|
|
||||||
...opts
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
export function uploadFile({ key, createAssetDto }: {
|
export function uploadFile({ key, createAssetDto }: {
|
||||||
key?: string;
|
key?: string;
|
||||||
createAssetDto: CreateAssetDto;
|
createAssetDto: CreateAssetDto;
|
||||||
@ -2487,13 +2422,15 @@ export function getConfigDefaults(opts?: Oazapfts.RequestOpts) {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
export function getMapStyle({ theme }: {
|
export function getMapStyle({ key, theme }: {
|
||||||
|
key?: string;
|
||||||
theme: MapTheme;
|
theme: MapTheme;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchJson<{
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
status: 200;
|
status: 200;
|
||||||
data: object;
|
data: object;
|
||||||
}>(`/system-config/map/style.json${QS.query(QS.explode({
|
}>(`/system-config/map/style.json${QS.query(QS.explode({
|
||||||
|
key,
|
||||||
theme
|
theme
|
||||||
}))}`, {
|
}))}`, {
|
||||||
...opts
|
...opts
|
||||||
@ -2594,6 +2531,72 @@ export function tagAssets({ id, assetIdsDto }: {
|
|||||||
body: assetIdsDto
|
body: assetIdsDto
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: {
|
||||||
|
albumId?: string;
|
||||||
|
isArchived?: boolean;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
isTrashed?: boolean;
|
||||||
|
key?: string;
|
||||||
|
order?: AssetOrder;
|
||||||
|
personId?: string;
|
||||||
|
size: TimeBucketSize;
|
||||||
|
timeBucket: string;
|
||||||
|
userId?: string;
|
||||||
|
withPartners?: boolean;
|
||||||
|
withStacked?: boolean;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: AssetResponseDto[];
|
||||||
|
}>(`/timeline/bucket${QS.query(QS.explode({
|
||||||
|
albumId,
|
||||||
|
isArchived,
|
||||||
|
isFavorite,
|
||||||
|
isTrashed,
|
||||||
|
key,
|
||||||
|
order,
|
||||||
|
personId,
|
||||||
|
size,
|
||||||
|
timeBucket,
|
||||||
|
userId,
|
||||||
|
withPartners,
|
||||||
|
withStacked
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: {
|
||||||
|
albumId?: string;
|
||||||
|
isArchived?: boolean;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
isTrashed?: boolean;
|
||||||
|
key?: string;
|
||||||
|
order?: AssetOrder;
|
||||||
|
personId?: string;
|
||||||
|
size: TimeBucketSize;
|
||||||
|
userId?: string;
|
||||||
|
withPartners?: boolean;
|
||||||
|
withStacked?: boolean;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: TimeBucketResponseDto[];
|
||||||
|
}>(`/timeline/buckets${QS.query(QS.explode({
|
||||||
|
albumId,
|
||||||
|
isArchived,
|
||||||
|
isFavorite,
|
||||||
|
isTrashed,
|
||||||
|
key,
|
||||||
|
order,
|
||||||
|
personId,
|
||||||
|
size,
|
||||||
|
userId,
|
||||||
|
withPartners,
|
||||||
|
withStacked
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function emptyTrash(opts?: Oazapfts.RequestOpts) {
|
export function emptyTrash(opts?: Oazapfts.RequestOpts) {
|
||||||
return oazapfts.ok(oazapfts.fetchText("/trash/empty", {
|
return oazapfts.ok(oazapfts.fetchText("/trash/empty", {
|
||||||
...opts,
|
...opts,
|
||||||
@ -2788,10 +2791,6 @@ export enum ThumbnailFormat {
|
|||||||
Jpeg = "JPEG",
|
Jpeg = "JPEG",
|
||||||
Webp = "WEBP"
|
Webp = "WEBP"
|
||||||
}
|
}
|
||||||
export enum TimeBucketSize {
|
|
||||||
Day = "DAY",
|
|
||||||
Month = "MONTH"
|
|
||||||
}
|
|
||||||
export enum EntityType {
|
export enum EntityType {
|
||||||
Asset = "ASSET",
|
Asset = "ASSET",
|
||||||
Album = "ALBUM"
|
Album = "ALBUM"
|
||||||
@ -2910,3 +2909,7 @@ export enum MapTheme {
|
|||||||
Light = "light",
|
Light = "light",
|
||||||
Dark = "dark"
|
Dark = "dark"
|
||||||
}
|
}
|
||||||
|
export enum TimeBucketSize {
|
||||||
|
Day = "DAY",
|
||||||
|
Month = "MONTH"
|
||||||
|
}
|
||||||
|
88
server/package-lock.json
generated
88
server/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.11",
|
"@babel/runtime": "^7.22.11",
|
||||||
@ -2537,9 +2537,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/common": {
|
"node_modules/@nestjs/common": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.5.tgz",
|
||||||
"integrity": "sha512-HmehujZhUZjf9TN2o0TyzWYNwEgyRYqZZ5qIcF/mCgIUZ4olIKlazna0kGK56FGlCvviHWNKQM5eTuVeTstIgA==",
|
"integrity": "sha512-XWxbDf2ey/jAyEa3/XpckgfzJZ9j3I05ZkEFx7cAlebFuVKeq5UDDb5Sq9O7hMmbH9xdQj3pYT19SSj01hKeug==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iterare": "1.2.1",
|
"iterare": "1.2.1",
|
||||||
"tslib": "2.6.2",
|
"tslib": "2.6.2",
|
||||||
@ -2591,9 +2591,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/core": {
|
"node_modules/@nestjs/core": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.5.tgz",
|
||||||
"integrity": "sha512-rF0yebuHmMj+9/CkbjPWWMvlF5x8j5Biw2DRvbl8R8n2X3OdFBN+06x/9xm3/ZssR5tLoB9tsYspFUb+SvnnwA==",
|
"integrity": "sha512-U7SrGD9/Mu4eUtxfZYiGdY38FcksEyJegs4dQZ8B19nnusw0aTocPEy4HVsmx0LLO4sG+fBLLYzCDDr9kFwXAQ==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/opencollective": "0.3.2",
|
"@nuxtjs/opencollective": "0.3.2",
|
||||||
@ -2659,9 +2659,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/platform-express": {
|
"node_modules/@nestjs/platform-express": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.5.tgz",
|
||||||
"integrity": "sha512-rzUUUZCGYNs/viT9I6W5izJ1+oYCG0ym/dAn31NmYJW9UchxJdX5PCJqWF8iIbys6JgfbdcapMR5t+L7OZsasQ==",
|
"integrity": "sha512-IhVomwLvdLlv4zCdQK2ROT/nInk1i8m4K48lAUHJV5UVktgVmg0WbQga2/9KywaTjNbx+eWhZXXFii+vtFRAOw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.2",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
@ -2679,9 +2679,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/platform-socket.io": {
|
"node_modules/@nestjs/platform-socket.io": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.5.tgz",
|
||||||
"integrity": "sha512-HiL7FbLQBanf8ORxQDpub8wdkRJmXHj8vmExDJ+lD1/E2ChrJbBgRDaKWI7QcSzPKF1uS8VVwz3w0zn3F/EDtA==",
|
"integrity": "sha512-G2N3sTd9tZ7XQQ7RlrpaQdt1/IBztVHuKg686QmBTLVlRHZ1AMOmXouBk+q5SINT1XURiABa8tQh1Ydx0OEh9w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"socket.io": "4.7.5",
|
"socket.io": "4.7.5",
|
||||||
"tslib": "2.6.2"
|
"tslib": "2.6.2"
|
||||||
@ -2764,9 +2764,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/testing": {
|
"node_modules/@nestjs/testing": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.5.tgz",
|
||||||
"integrity": "sha512-g3NQnRUFBcYF+ySkB7INg5RiV7CNfkP5zwaf3NFo0WjhBrfih9f1jMZ/19blLZ4djN/ngulYks2E3lzROAW8RQ==",
|
"integrity": "sha512-j30/lxH0BayeDTigapYtQn/XhMRR7CzlFsm3dHoWViWQv0qT1r2ffe3927BbBLX3N/ZzglE10OAqW06ADZV8dw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "2.6.2"
|
"tslib": "2.6.2"
|
||||||
@ -2806,9 +2806,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nestjs/websockets": {
|
"node_modules/@nestjs/websockets": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.5.tgz",
|
||||||
"integrity": "sha512-ZGDY8t1bBYzY2xbOe2QOxYG+D6W1mALSS3VD/rcVW34oaysF4iQQEr4t2ktYLbPAuZlEvwM5EhutqCkBUsDw7Q==",
|
"integrity": "sha512-6w383LUBFHoZ0eFODqEHN2NoIRUwbTd37Hc1KqtZZihhFUzscC/0LMAV20o9LdfS/Xjog5ShNTxvOHuzNBnE4A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"iterare": "1.2.1",
|
"iterare": "1.2.1",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
@ -8748,9 +8748,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/i18n-iso-countries": {
|
"node_modules/i18n-iso-countries": {
|
||||||
"version": "7.10.1",
|
"version": "7.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.0.tgz",
|
||||||
"integrity": "sha512-9DXmAMkfGcGNE+E/2fE85UUjjkPeT0LHMA8d+kcxXiO+s50W28lxiICel8f8qWZmCNic1cuhN1+nw7ZazMQJFA==",
|
"integrity": "sha512-MP2+aAJwvBTuruaMEj+mPEhu4D9rn03GkkbuP8/xkvNzhVwNe2cAg1ivkL5Oj+vwqEwvIcf5C7Q+5Y/UZNLBHw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"diacritics": "1.3.0"
|
"diacritics": "1.3.0"
|
||||||
},
|
},
|
||||||
@ -16141,9 +16141,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@nestjs/common": {
|
"@nestjs/common": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.5.tgz",
|
||||||
"integrity": "sha512-HmehujZhUZjf9TN2o0TyzWYNwEgyRYqZZ5qIcF/mCgIUZ4olIKlazna0kGK56FGlCvviHWNKQM5eTuVeTstIgA==",
|
"integrity": "sha512-XWxbDf2ey/jAyEa3/XpckgfzJZ9j3I05ZkEFx7cAlebFuVKeq5UDDb5Sq9O7hMmbH9xdQj3pYT19SSj01hKeug==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"iterare": "1.2.1",
|
"iterare": "1.2.1",
|
||||||
"tslib": "2.6.2",
|
"tslib": "2.6.2",
|
||||||
@ -16169,9 +16169,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@nestjs/core": {
|
"@nestjs/core": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.5.tgz",
|
||||||
"integrity": "sha512-rF0yebuHmMj+9/CkbjPWWMvlF5x8j5Biw2DRvbl8R8n2X3OdFBN+06x/9xm3/ZssR5tLoB9tsYspFUb+SvnnwA==",
|
"integrity": "sha512-U7SrGD9/Mu4eUtxfZYiGdY38FcksEyJegs4dQZ8B19nnusw0aTocPEy4HVsmx0LLO4sG+fBLLYzCDDr9kFwXAQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@nuxtjs/opencollective": "0.3.2",
|
"@nuxtjs/opencollective": "0.3.2",
|
||||||
"fast-safe-stringify": "2.1.1",
|
"fast-safe-stringify": "2.1.1",
|
||||||
@ -16196,9 +16196,9 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@nestjs/platform-express": {
|
"@nestjs/platform-express": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.5.tgz",
|
||||||
"integrity": "sha512-rzUUUZCGYNs/viT9I6W5izJ1+oYCG0ym/dAn31NmYJW9UchxJdX5PCJqWF8iIbys6JgfbdcapMR5t+L7OZsasQ==",
|
"integrity": "sha512-IhVomwLvdLlv4zCdQK2ROT/nInk1i8m4K48lAUHJV5UVktgVmg0WbQga2/9KywaTjNbx+eWhZXXFii+vtFRAOw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"body-parser": "1.20.2",
|
"body-parser": "1.20.2",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
@ -16208,9 +16208,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@nestjs/platform-socket.io": {
|
"@nestjs/platform-socket.io": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.5.tgz",
|
||||||
"integrity": "sha512-HiL7FbLQBanf8ORxQDpub8wdkRJmXHj8vmExDJ+lD1/E2ChrJbBgRDaKWI7QcSzPKF1uS8VVwz3w0zn3F/EDtA==",
|
"integrity": "sha512-G2N3sTd9tZ7XQQ7RlrpaQdt1/IBztVHuKg686QmBTLVlRHZ1AMOmXouBk+q5SINT1XURiABa8tQh1Ydx0OEh9w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"socket.io": "4.7.5",
|
"socket.io": "4.7.5",
|
||||||
"tslib": "2.6.2"
|
"tslib": "2.6.2"
|
||||||
@ -16260,9 +16260,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@nestjs/testing": {
|
"@nestjs/testing": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.5.tgz",
|
||||||
"integrity": "sha512-g3NQnRUFBcYF+ySkB7INg5RiV7CNfkP5zwaf3NFo0WjhBrfih9f1jMZ/19blLZ4djN/ngulYks2E3lzROAW8RQ==",
|
"integrity": "sha512-j30/lxH0BayeDTigapYtQn/XhMRR7CzlFsm3dHoWViWQv0qT1r2ffe3927BbBLX3N/ZzglE10OAqW06ADZV8dw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "2.6.2"
|
"tslib": "2.6.2"
|
||||||
@ -16277,9 +16277,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@nestjs/websockets": {
|
"@nestjs/websockets": {
|
||||||
"version": "10.3.4",
|
"version": "10.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.5.tgz",
|
||||||
"integrity": "sha512-ZGDY8t1bBYzY2xbOe2QOxYG+D6W1mALSS3VD/rcVW34oaysF4iQQEr4t2ktYLbPAuZlEvwM5EhutqCkBUsDw7Q==",
|
"integrity": "sha512-6w383LUBFHoZ0eFODqEHN2NoIRUwbTd37Hc1KqtZZihhFUzscC/0LMAV20o9LdfS/Xjog5ShNTxvOHuzNBnE4A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"iterare": "1.2.1",
|
"iterare": "1.2.1",
|
||||||
"object-hash": "3.0.0",
|
"object-hash": "3.0.0",
|
||||||
@ -20746,9 +20746,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"i18n-iso-countries": {
|
"i18n-iso-countries": {
|
||||||
"version": "7.10.1",
|
"version": "7.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.10.1.tgz",
|
"resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.0.tgz",
|
||||||
"integrity": "sha512-9DXmAMkfGcGNE+E/2fE85UUjjkPeT0LHMA8d+kcxXiO+s50W28lxiICel8f8qWZmCNic1cuhN1+nw7ZazMQJFA==",
|
"integrity": "sha512-MP2+aAJwvBTuruaMEj+mPEhu4D9rn03GkkbuP8/xkvNzhVwNe2cAg1ivkL5Oj+vwqEwvIcf5C7Q+5Y/UZNLBHw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"diacritics": "1.3.0"
|
"diacritics": "1.3.0"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
@ -1,229 +1,28 @@
|
|||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { Module, OnModuleInit, Provider, ValidationPipe } from '@nestjs/common';
|
import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
|
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { OpenTelemetryModule } from 'nestjs-otel';
|
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||||
import { ListUsersCommand } from 'src/commands/list-users.command';
|
import { commands } from 'src/commands';
|
||||||
import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login';
|
|
||||||
import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login';
|
|
||||||
import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
|
|
||||||
import { bullConfig, bullQueues, immichAppConfig } from 'src/config';
|
import { bullConfig, bullQueues, immichAppConfig } from 'src/config';
|
||||||
import { ActivityController } from 'src/controllers/activity.controller';
|
import { controllers } from 'src/controllers';
|
||||||
import { AlbumController } from 'src/controllers/album.controller';
|
|
||||||
import { APIKeyController } from 'src/controllers/api-key.controller';
|
|
||||||
import { AppController } from 'src/controllers/app.controller';
|
|
||||||
import { AssetControllerV1 } from 'src/controllers/asset-v1.controller';
|
|
||||||
import { AssetController, AssetsController } from 'src/controllers/asset.controller';
|
|
||||||
import { AuditController } from 'src/controllers/audit.controller';
|
|
||||||
import { AuthController } from 'src/controllers/auth.controller';
|
|
||||||
import { DownloadController } from 'src/controllers/download.controller';
|
|
||||||
import { FaceController } from 'src/controllers/face.controller';
|
|
||||||
import { JobController } from 'src/controllers/job.controller';
|
|
||||||
import { LibraryController } from 'src/controllers/library.controller';
|
|
||||||
import { OAuthController } from 'src/controllers/oauth.controller';
|
|
||||||
import { PartnerController } from 'src/controllers/partner.controller';
|
|
||||||
import { PersonController } from 'src/controllers/person.controller';
|
|
||||||
import { SearchController } from 'src/controllers/search.controller';
|
|
||||||
import { ServerInfoController } from 'src/controllers/server-info.controller';
|
|
||||||
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
|
||||||
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
|
||||||
import { TagController } from 'src/controllers/tag.controller';
|
|
||||||
import { TrashController } from 'src/controllers/trash.controller';
|
|
||||||
import { UserController } from 'src/controllers/user.controller';
|
|
||||||
import { databaseConfig } from 'src/database.config';
|
import { databaseConfig } from 'src/database.config';
|
||||||
import { databaseEntities } from 'src/entities';
|
import { entities } from 'src/entities';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
|
||||||
import { IActivityRepository } from 'src/interfaces/activity.interface';
|
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
|
||||||
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
|
||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
|
||||||
import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
|
||||||
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
|
||||||
import { IJobRepository } from 'src/interfaces/job.interface';
|
|
||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
|
||||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
|
||||||
import { IMediaRepository } from 'src/interfaces/media.interface';
|
|
||||||
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
|
||||||
import { IMetricRepository } from 'src/interfaces/metric.interface';
|
|
||||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
|
||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
|
||||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
|
||||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
|
||||||
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
|
||||||
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|
||||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
|
||||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
|
||||||
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
|
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
|
||||||
import { AuthGuard } from 'src/middleware/auth.guard';
|
import { AuthGuard } from 'src/middleware/auth.guard';
|
||||||
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
||||||
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { repositories } from 'src/repositories';
|
||||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
import { services } from 'src/services';
|
||||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
|
||||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
|
||||||
import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
|
|
||||||
import { AssetRepositoryV1 } from 'src/repositories/asset-v1.repository';
|
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
|
||||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
|
||||||
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
|
||||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
|
||||||
import { EventRepository } from 'src/repositories/event.repository';
|
|
||||||
import { JobRepository } from 'src/repositories/job.repository';
|
|
||||||
import { LibraryRepository } from 'src/repositories/library.repository';
|
|
||||||
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
|
||||||
import { MediaRepository } from 'src/repositories/media.repository';
|
|
||||||
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
|
||||||
import { MetricRepository } from 'src/repositories/metric.repository';
|
|
||||||
import { MoveRepository } from 'src/repositories/move.repository';
|
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
|
||||||
import { SearchRepository } from 'src/repositories/search.repository';
|
|
||||||
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
|
||||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
|
||||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
|
||||||
import { SystemConfigRepository } from 'src/repositories/system-config.repository';
|
|
||||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
|
||||||
import { TagRepository } from 'src/repositories/tag.repository';
|
|
||||||
import { UserTokenRepository } from 'src/repositories/user-token.repository';
|
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
|
||||||
import { ActivityService } from 'src/services/activity.service';
|
|
||||||
import { AlbumService } from 'src/services/album.service';
|
|
||||||
import { APIKeyService } from 'src/services/api-key.service';
|
|
||||||
import { ApiService } from 'src/services/api.service';
|
import { ApiService } from 'src/services/api.service';
|
||||||
import { AssetServiceV1 } from 'src/services/asset-v1.service';
|
|
||||||
import { AssetService } from 'src/services/asset.service';
|
|
||||||
import { AuditService } from 'src/services/audit.service';
|
|
||||||
import { AuthService } from 'src/services/auth.service';
|
|
||||||
import { DatabaseService } from 'src/services/database.service';
|
|
||||||
import { DownloadService } from 'src/services/download.service';
|
|
||||||
import { JobService } from 'src/services/job.service';
|
|
||||||
import { LibraryService } from 'src/services/library.service';
|
|
||||||
import { MediaService } from 'src/services/media.service';
|
|
||||||
import { MetadataService } from 'src/services/metadata.service';
|
|
||||||
import { MicroservicesService } from 'src/services/microservices.service';
|
import { MicroservicesService } from 'src/services/microservices.service';
|
||||||
import { PartnerService } from 'src/services/partner.service';
|
|
||||||
import { PersonService } from 'src/services/person.service';
|
|
||||||
import { SearchService } from 'src/services/search.service';
|
|
||||||
import { ServerInfoService } from 'src/services/server-info.service';
|
|
||||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
|
||||||
import { SmartInfoService } from 'src/services/smart-info.service';
|
|
||||||
import { StorageTemplateService } from 'src/services/storage-template.service';
|
|
||||||
import { StorageService } from 'src/services/storage.service';
|
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
|
||||||
import { TagService } from 'src/services/tag.service';
|
|
||||||
import { TrashService } from 'src/services/trash.service';
|
|
||||||
import { UserService } from 'src/services/user.service';
|
|
||||||
import { otelConfig } from 'src/utils/instrumentation';
|
import { otelConfig } from 'src/utils/instrumentation';
|
||||||
import { ImmichLogger } from 'src/utils/logger';
|
import { ImmichLogger } from 'src/utils/logger';
|
||||||
|
|
||||||
const commands = [
|
const providers = [ImmichLogger];
|
||||||
ResetAdminPasswordCommand,
|
const common = [...services, ...providers, ...repositories];
|
||||||
PromptPasswordQuestions,
|
|
||||||
EnablePasswordLoginCommand,
|
|
||||||
DisablePasswordLoginCommand,
|
|
||||||
EnableOAuthLogin,
|
|
||||||
DisableOAuthLogin,
|
|
||||||
ListUsersCommand,
|
|
||||||
];
|
|
||||||
|
|
||||||
const controllers = [
|
|
||||||
ActivityController,
|
|
||||||
AssetsController,
|
|
||||||
AssetControllerV1,
|
|
||||||
AssetController,
|
|
||||||
AppController,
|
|
||||||
AlbumController,
|
|
||||||
APIKeyController,
|
|
||||||
AuditController,
|
|
||||||
AuthController,
|
|
||||||
DownloadController,
|
|
||||||
FaceController,
|
|
||||||
JobController,
|
|
||||||
LibraryController,
|
|
||||||
OAuthController,
|
|
||||||
PartnerController,
|
|
||||||
SearchController,
|
|
||||||
ServerInfoController,
|
|
||||||
SharedLinkController,
|
|
||||||
SystemConfigController,
|
|
||||||
TagController,
|
|
||||||
TrashController,
|
|
||||||
UserController,
|
|
||||||
PersonController,
|
|
||||||
];
|
|
||||||
|
|
||||||
const services: Provider[] = [
|
|
||||||
ApiService,
|
|
||||||
MicroservicesService,
|
|
||||||
APIKeyService,
|
|
||||||
ActivityService,
|
|
||||||
AlbumService,
|
|
||||||
AssetService,
|
|
||||||
AssetServiceV1,
|
|
||||||
AuditService,
|
|
||||||
AuthService,
|
|
||||||
DatabaseService,
|
|
||||||
DownloadService,
|
|
||||||
ImmichLogger,
|
|
||||||
JobService,
|
|
||||||
LibraryService,
|
|
||||||
MediaService,
|
|
||||||
MetadataService,
|
|
||||||
PartnerService,
|
|
||||||
PersonService,
|
|
||||||
SearchService,
|
|
||||||
ServerInfoService,
|
|
||||||
SharedLinkService,
|
|
||||||
SmartInfoService,
|
|
||||||
StorageService,
|
|
||||||
StorageTemplateService,
|
|
||||||
SystemConfigService,
|
|
||||||
TagService,
|
|
||||||
TrashService,
|
|
||||||
UserService,
|
|
||||||
];
|
|
||||||
|
|
||||||
const repositories: Provider[] = [
|
|
||||||
{ provide: IActivityRepository, useClass: ActivityRepository },
|
|
||||||
{ provide: IAccessRepository, useClass: AccessRepository },
|
|
||||||
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
|
||||||
{ provide: IAssetRepository, useClass: AssetRepository },
|
|
||||||
{ provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
|
|
||||||
{ provide: IAssetStackRepository, useClass: AssetStackRepository },
|
|
||||||
{ provide: IAuditRepository, useClass: AuditRepository },
|
|
||||||
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
|
||||||
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
|
||||||
{ provide: IEventRepository, useClass: EventRepository },
|
|
||||||
{ provide: IJobRepository, useClass: JobRepository },
|
|
||||||
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
|
||||||
{ provide: IKeyRepository, useClass: ApiKeyRepository },
|
|
||||||
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
|
||||||
{ provide: IMetadataRepository, useClass: MetadataRepository },
|
|
||||||
{ provide: IMetricRepository, useClass: MetricRepository },
|
|
||||||
{ provide: IMoveRepository, useClass: MoveRepository },
|
|
||||||
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
|
||||||
{ provide: IPersonRepository, useClass: PersonRepository },
|
|
||||||
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
|
||||||
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
|
||||||
{ provide: ISearchRepository, useClass: SearchRepository },
|
|
||||||
{ provide: IStorageRepository, useClass: StorageRepository },
|
|
||||||
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
|
||||||
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
|
||||||
{ provide: ITagRepository, useClass: TagRepository },
|
|
||||||
{ provide: IMediaRepository, useClass: MediaRepository },
|
|
||||||
{ provide: IUserRepository, useClass: UserRepository },
|
|
||||||
{ provide: IUserTokenRepository, useClass: UserTokenRepository },
|
|
||||||
];
|
|
||||||
|
|
||||||
const middleware = [
|
const middleware = [
|
||||||
FileUploadInterceptor,
|
FileUploadInterceptor,
|
||||||
@ -239,13 +38,13 @@ const imports = [
|
|||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
OpenTelemetryModule.forRoot(otelConfig),
|
OpenTelemetryModule.forRoot(otelConfig),
|
||||||
TypeOrmModule.forRoot(databaseConfig),
|
TypeOrmModule.forRoot(databaseConfig),
|
||||||
TypeOrmModule.forFeature(databaseEntities),
|
TypeOrmModule.forFeature(entities),
|
||||||
];
|
];
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [...imports, ScheduleModule.forRoot()],
|
imports: [...imports, ScheduleModule.forRoot()],
|
||||||
controllers: [...controllers],
|
controllers: [...controllers],
|
||||||
providers: [...services, ...repositories, ...middleware],
|
providers: [...common, ...middleware],
|
||||||
})
|
})
|
||||||
export class ApiModule implements OnModuleInit {
|
export class ApiModule implements OnModuleInit {
|
||||||
constructor(private service: ApiService) {}
|
constructor(private service: ApiService) {}
|
||||||
@ -257,7 +56,7 @@ export class ApiModule implements OnModuleInit {
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [...imports],
|
imports: [...imports],
|
||||||
providers: [...services, ...repositories, SchedulerRegistry],
|
providers: [...common, SchedulerRegistry],
|
||||||
})
|
})
|
||||||
export class MicroservicesModule implements OnModuleInit {
|
export class MicroservicesModule implements OnModuleInit {
|
||||||
constructor(private service: MicroservicesService) {}
|
constructor(private service: MicroservicesService) {}
|
||||||
@ -269,7 +68,7 @@ export class MicroservicesModule implements OnModuleInit {
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [...imports],
|
imports: [...imports],
|
||||||
providers: [...services, ...repositories, ...commands, SchedulerRegistry],
|
providers: [...common, ...commands, SchedulerRegistry],
|
||||||
})
|
})
|
||||||
export class ImmichAdminModule {}
|
export class ImmichAdminModule {}
|
||||||
|
|
||||||
@ -278,10 +77,10 @@ export class ImmichAdminModule {}
|
|||||||
ConfigModule.forRoot(immichAppConfig),
|
ConfigModule.forRoot(immichAppConfig),
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
TypeOrmModule.forRoot(databaseConfig),
|
TypeOrmModule.forRoot(databaseConfig),
|
||||||
TypeOrmModule.forFeature(databaseEntities),
|
TypeOrmModule.forFeature(entities),
|
||||||
OpenTelemetryModule.forRoot(otelConfig),
|
OpenTelemetryModule.forRoot(otelConfig),
|
||||||
],
|
],
|
||||||
controllers: [...controllers],
|
controllers: [...controllers],
|
||||||
providers: [...services, ...repositories, ...middleware, SchedulerRegistry],
|
providers: [...common, ...middleware, SchedulerRegistry],
|
||||||
})
|
})
|
||||||
export class AppTestModule {}
|
export class AppTestModule {}
|
||||||
|
14
server/src/commands/index.ts
Normal file
14
server/src/commands/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { ListUsersCommand } from 'src/commands/list-users.command';
|
||||||
|
import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login';
|
||||||
|
import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login';
|
||||||
|
import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
|
||||||
|
|
||||||
|
export const commands = [
|
||||||
|
ResetAdminPasswordCommand,
|
||||||
|
PromptPasswordQuestions,
|
||||||
|
EnablePasswordLoginCommand,
|
||||||
|
DisablePasswordLoginCommand,
|
||||||
|
EnableOAuthLogin,
|
||||||
|
DisableOAuthLogin,
|
||||||
|
ListUsersCommand,
|
||||||
|
];
|
@ -14,7 +14,6 @@ import {
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
|
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
|
||||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||||
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
|
||||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||||
import { Route } from 'src/middleware/file-upload.interceptor';
|
import { Route } from 'src/middleware/file-upload.interceptor';
|
||||||
import { AssetService } from 'src/services/asset.service';
|
import { AssetService } from 'src/services/asset.service';
|
||||||
@ -71,18 +70,6 @@ export class AssetController {
|
|||||||
return this.service.getStatistics(auth, dto);
|
return this.service.getStatistics(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated({ isShared: true })
|
|
||||||
@Get('time-buckets')
|
|
||||||
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
|
||||||
return this.service.getTimeBuckets(auth, dto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Authenticated({ isShared: true })
|
|
||||||
@Get('time-bucket')
|
|
||||||
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
|
|
||||||
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('jobs')
|
@Post('jobs')
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
|
runAssetJobs(@Auth() auth: AuthDto, @Body() dto: AssetJobsDto): Promise<void> {
|
||||||
|
50
server/src/controllers/index.ts
Normal file
50
server/src/controllers/index.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { ActivityController } from 'src/controllers/activity.controller';
|
||||||
|
import { AlbumController } from 'src/controllers/album.controller';
|
||||||
|
import { APIKeyController } from 'src/controllers/api-key.controller';
|
||||||
|
import { AppController } from 'src/controllers/app.controller';
|
||||||
|
import { AssetControllerV1 } from 'src/controllers/asset-v1.controller';
|
||||||
|
import { AssetController, AssetsController } from 'src/controllers/asset.controller';
|
||||||
|
import { AuditController } from 'src/controllers/audit.controller';
|
||||||
|
import { AuthController } from 'src/controllers/auth.controller';
|
||||||
|
import { DownloadController } from 'src/controllers/download.controller';
|
||||||
|
import { FaceController } from 'src/controllers/face.controller';
|
||||||
|
import { JobController } from 'src/controllers/job.controller';
|
||||||
|
import { LibraryController } from 'src/controllers/library.controller';
|
||||||
|
import { OAuthController } from 'src/controllers/oauth.controller';
|
||||||
|
import { PartnerController } from 'src/controllers/partner.controller';
|
||||||
|
import { PersonController } from 'src/controllers/person.controller';
|
||||||
|
import { SearchController } from 'src/controllers/search.controller';
|
||||||
|
import { ServerInfoController } from 'src/controllers/server-info.controller';
|
||||||
|
import { SharedLinkController } from 'src/controllers/shared-link.controller';
|
||||||
|
import { SystemConfigController } from 'src/controllers/system-config.controller';
|
||||||
|
import { TagController } from 'src/controllers/tag.controller';
|
||||||
|
import { TimelineController } from 'src/controllers/timeline.controller';
|
||||||
|
import { TrashController } from 'src/controllers/trash.controller';
|
||||||
|
import { UserController } from 'src/controllers/user.controller';
|
||||||
|
|
||||||
|
export const controllers = [
|
||||||
|
ActivityController,
|
||||||
|
AssetsController,
|
||||||
|
AssetControllerV1,
|
||||||
|
AssetController,
|
||||||
|
AppController,
|
||||||
|
AlbumController,
|
||||||
|
APIKeyController,
|
||||||
|
AuditController,
|
||||||
|
AuthController,
|
||||||
|
DownloadController,
|
||||||
|
FaceController,
|
||||||
|
JobController,
|
||||||
|
LibraryController,
|
||||||
|
OAuthController,
|
||||||
|
PartnerController,
|
||||||
|
SearchController,
|
||||||
|
ServerInfoController,
|
||||||
|
SharedLinkController,
|
||||||
|
SystemConfigController,
|
||||||
|
TagController,
|
||||||
|
TimelineController,
|
||||||
|
TrashController,
|
||||||
|
UserController,
|
||||||
|
PersonController,
|
||||||
|
];
|
@ -1,7 +1,7 @@
|
|||||||
import { Body, Controller, Get, Put, Query } from '@nestjs/common';
|
import { Body, Controller, Get, Put, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
import { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
||||||
import { AdminRoute, Authenticated } from 'src/middleware/auth.guard';
|
import { AdminRoute, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
|
||||||
@ApiTags('System Config')
|
@ApiTags('System Config')
|
||||||
@ -31,6 +31,7 @@ export class SystemConfigController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@AdminRoute(false)
|
@AdminRoute(false)
|
||||||
|
@SharedLinkRoute()
|
||||||
@Get('map/style.json')
|
@Get('map/style.json')
|
||||||
getMapStyle(@Query() dto: MapThemeDto) {
|
getMapStyle(@Query() dto: MapThemeDto) {
|
||||||
return this.service.getMapStyle(dto.theme);
|
return this.service.getMapStyle(dto.theme);
|
||||||
|
26
server/src/controllers/timeline.controller.ts
Normal file
26
server/src/controllers/timeline.controller.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
||||||
|
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
|
|
||||||
|
@ApiTags('Timeline')
|
||||||
|
@Controller('timeline')
|
||||||
|
@Authenticated()
|
||||||
|
export class TimelineController {
|
||||||
|
constructor(private service: TimelineService) {}
|
||||||
|
|
||||||
|
@Authenticated({ isShared: true })
|
||||||
|
@Get('buckets')
|
||||||
|
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
||||||
|
return this.service.getTimeBuckets(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Authenticated({ isShared: true })
|
||||||
|
@Get('bucket')
|
||||||
|
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> {
|
||||||
|
return this.service.getTimeBucket(auth, dto) as Promise<AssetResponseDto[]>;
|
||||||
|
}
|
||||||
|
}
|
@ -84,7 +84,7 @@ export class AccessCore {
|
|||||||
*
|
*
|
||||||
* @returns Set<string>
|
* @returns Set<string>
|
||||||
*/
|
*/
|
||||||
async checkAccess(auth: AuthDto, permission: Permission, ids: Set<string> | string[]) {
|
async checkAccess(auth: AuthDto, permission: Permission, ids: Set<string> | string[]): Promise<Set<string>> {
|
||||||
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
|
const idSet = Array.isArray(ids) ? new Set(ids) : ids;
|
||||||
if (idSet.size === 0) {
|
if (idSet.size === 0) {
|
||||||
return new Set();
|
return new Set();
|
||||||
@ -97,7 +97,11 @@ export class AccessCore {
|
|||||||
return this.checkAccessOther(auth, permission, idSet);
|
return this.checkAccessOther(auth, permission, idSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkAccessSharedLink(sharedLink: SharedLinkEntity, permission: Permission, ids: Set<string>) {
|
private async checkAccessSharedLink(
|
||||||
|
sharedLink: SharedLinkEntity,
|
||||||
|
permission: Permission,
|
||||||
|
ids: Set<string>,
|
||||||
|
): Promise<Set<string>> {
|
||||||
const sharedLinkId = sharedLink.id;
|
const sharedLinkId = sharedLink.id;
|
||||||
|
|
||||||
switch (permission) {
|
switch (permission) {
|
||||||
@ -140,7 +144,7 @@ export class AccessCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>) {
|
private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>): Promise<Set<string>> {
|
||||||
switch (permission) {
|
switch (permission) {
|
||||||
// uses album id
|
// uses album id
|
||||||
case Permission.ACTIVITY_CREATE: {
|
case Permission.ACTIVITY_CREATE: {
|
||||||
|
@ -131,7 +131,12 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class MemoryLaneResponseDto {
|
export class MemoryLaneResponseDto {
|
||||||
|
@ApiProperty({ deprecated: true })
|
||||||
title!: string;
|
title!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
yearsAgo!: number;
|
||||||
|
|
||||||
assets!: AssetResponseDto[];
|
assets!: AssetResponseDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ import { TagEntity } from 'src/entities/tag.entity';
|
|||||||
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
import { UserTokenEntity } from 'src/entities/user-token.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
|
|
||||||
export const databaseEntities = [
|
export const entities = [
|
||||||
ActivityEntity,
|
ActivityEntity,
|
||||||
AlbumEntity,
|
AlbumEntity,
|
||||||
APIKeyEntity,
|
APIKeyEntity,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
|
import { IBulkAsset } from 'src/utils/asset.util';
|
||||||
|
|
||||||
export const IAlbumRepository = 'IAlbumRepository';
|
export const IAlbumRepository = 'IAlbumRepository';
|
||||||
|
|
||||||
@ -23,15 +24,14 @@ export interface AlbumAssets {
|
|||||||
assetIds: string[];
|
assetIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAlbumRepository {
|
export interface IAlbumRepository extends IBulkAsset {
|
||||||
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
|
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
|
||||||
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
getByIds(ids: string[]): Promise<AlbumEntity[]>;
|
||||||
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
|
||||||
addAssets(assets: AlbumAssets): Promise<void>;
|
|
||||||
getAssetIds(albumId: string, assetIds?: string[]): Promise<Set<string>>;
|
getAssetIds(albumId: string, assetIds?: string[]): Promise<Set<string>>;
|
||||||
hasAsset(asset: AlbumAsset): Promise<boolean>;
|
hasAsset(asset: AlbumAsset): Promise<boolean>;
|
||||||
removeAsset(assetId: string): Promise<void>;
|
removeAsset(assetId: string): Promise<void>;
|
||||||
removeAssets(albumId: string, assetIds: string[]): Promise<void>;
|
removeAssetIds(albumId: string, assetIds: string[]): Promise<void>;
|
||||||
getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]>;
|
||||||
getInvalidThumbnail(): Promise<string[]>;
|
getInvalidThumbnail(): Promise<string[]>;
|
||||||
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
getOwned(ownerId: string): Promise<AlbumEntity[]>;
|
||||||
|
54
server/src/queries/activity.repository.sql
Normal file
54
server/src/queries/activity.repository.sql
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
-- NOTE: This file is auto generated by ./sql-generator
|
||||||
|
|
||||||
|
-- ActivityRepository.search
|
||||||
|
SELECT
|
||||||
|
"ActivityEntity"."id" AS "ActivityEntity_id",
|
||||||
|
"ActivityEntity"."createdAt" AS "ActivityEntity_createdAt",
|
||||||
|
"ActivityEntity"."updatedAt" AS "ActivityEntity_updatedAt",
|
||||||
|
"ActivityEntity"."albumId" AS "ActivityEntity_albumId",
|
||||||
|
"ActivityEntity"."userId" AS "ActivityEntity_userId",
|
||||||
|
"ActivityEntity"."assetId" AS "ActivityEntity_assetId",
|
||||||
|
"ActivityEntity"."comment" AS "ActivityEntity_comment",
|
||||||
|
"ActivityEntity"."isLiked" AS "ActivityEntity_isLiked",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."id" AS "ActivityEntity__ActivityEntity_user_id",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."name" AS "ActivityEntity__ActivityEntity_user_name",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."avatarColor" AS "ActivityEntity__ActivityEntity_user_avatarColor",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."isAdmin" AS "ActivityEntity__ActivityEntity_user_isAdmin",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."email" AS "ActivityEntity__ActivityEntity_user_email",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."storageLabel" AS "ActivityEntity__ActivityEntity_user_storageLabel",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."oauthId" AS "ActivityEntity__ActivityEntity_user_oauthId",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."profileImagePath" AS "ActivityEntity__ActivityEntity_user_profileImagePath",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."shouldChangePassword" AS "ActivityEntity__ActivityEntity_user_shouldChangePassword",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."createdAt" AS "ActivityEntity__ActivityEntity_user_createdAt",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."deletedAt" AS "ActivityEntity__ActivityEntity_user_deletedAt",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."memoriesEnabled" AS "ActivityEntity__ActivityEntity_user_memoriesEnabled",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes",
|
||||||
|
"ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes"
|
||||||
|
FROM
|
||||||
|
"activity" "ActivityEntity"
|
||||||
|
LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId"
|
||||||
|
AND (
|
||||||
|
"ActivityEntity__ActivityEntity_user"."deletedAt" IS NULL
|
||||||
|
)
|
||||||
|
WHERE
|
||||||
|
(("ActivityEntity"."albumId" = $1))
|
||||||
|
ORDER BY
|
||||||
|
"ActivityEntity"."createdAt" ASC
|
||||||
|
|
||||||
|
-- ActivityRepository.getStatistics
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT ("ActivityEntity"."id")) AS "cnt"
|
||||||
|
FROM
|
||||||
|
"activity" "ActivityEntity"
|
||||||
|
LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId"
|
||||||
|
AND (
|
||||||
|
"ActivityEntity__ActivityEntity_user"."deletedAt" IS NULL
|
||||||
|
)
|
||||||
|
WHERE
|
||||||
|
(
|
||||||
|
("ActivityEntity"."assetId" = $1)
|
||||||
|
AND ("ActivityEntity"."albumId" = $2)
|
||||||
|
AND ("ActivityEntity"."isLiked" = $3)
|
||||||
|
)
|
@ -590,7 +590,7 @@ DELETE FROM "albums_assets_assets"
|
|||||||
WHERE
|
WHERE
|
||||||
"albums_assets_assets"."assetsId" = $1
|
"albums_assets_assets"."assetsId" = $1
|
||||||
|
|
||||||
-- AlbumRepository.removeAssets
|
-- AlbumRepository.removeAssetIds
|
||||||
DELETE FROM "albums_assets_assets"
|
DELETE FROM "albums_assets_assets"
|
||||||
WHERE
|
WHERE
|
||||||
(
|
(
|
||||||
@ -646,7 +646,7 @@ WHERE
|
|||||||
LIMIT
|
LIMIT
|
||||||
1
|
1
|
||||||
|
|
||||||
-- AlbumRepository.addAssets
|
-- AlbumRepository.addAssetIds
|
||||||
INSERT INTO
|
INSERT INTO
|
||||||
"albums_assets_assets" ("albumsId", "assetsId")
|
"albums_assets_assets" ("albumsId", "assetsId")
|
||||||
VALUES
|
VALUES
|
||||||
|
@ -1,5 +1,88 @@
|
|||||||
-- NOTE: This file is auto generated by ./sql-generator
|
-- NOTE: This file is auto generated by ./sql-generator
|
||||||
|
|
||||||
|
-- AssetRepository.getByDayOfYear
|
||||||
|
SELECT
|
||||||
|
"entity"."id" AS "entity_id",
|
||||||
|
"entity"."deviceAssetId" AS "entity_deviceAssetId",
|
||||||
|
"entity"."ownerId" AS "entity_ownerId",
|
||||||
|
"entity"."libraryId" AS "entity_libraryId",
|
||||||
|
"entity"."deviceId" AS "entity_deviceId",
|
||||||
|
"entity"."type" AS "entity_type",
|
||||||
|
"entity"."originalPath" AS "entity_originalPath",
|
||||||
|
"entity"."resizePath" AS "entity_resizePath",
|
||||||
|
"entity"."webpPath" AS "entity_webpPath",
|
||||||
|
"entity"."thumbhash" AS "entity_thumbhash",
|
||||||
|
"entity"."encodedVideoPath" AS "entity_encodedVideoPath",
|
||||||
|
"entity"."createdAt" AS "entity_createdAt",
|
||||||
|
"entity"."updatedAt" AS "entity_updatedAt",
|
||||||
|
"entity"."deletedAt" AS "entity_deletedAt",
|
||||||
|
"entity"."fileCreatedAt" AS "entity_fileCreatedAt",
|
||||||
|
"entity"."localDateTime" AS "entity_localDateTime",
|
||||||
|
"entity"."fileModifiedAt" AS "entity_fileModifiedAt",
|
||||||
|
"entity"."isFavorite" AS "entity_isFavorite",
|
||||||
|
"entity"."isArchived" AS "entity_isArchived",
|
||||||
|
"entity"."isExternal" AS "entity_isExternal",
|
||||||
|
"entity"."isReadOnly" AS "entity_isReadOnly",
|
||||||
|
"entity"."isOffline" AS "entity_isOffline",
|
||||||
|
"entity"."checksum" AS "entity_checksum",
|
||||||
|
"entity"."duration" AS "entity_duration",
|
||||||
|
"entity"."isVisible" AS "entity_isVisible",
|
||||||
|
"entity"."livePhotoVideoId" AS "entity_livePhotoVideoId",
|
||||||
|
"entity"."originalFileName" AS "entity_originalFileName",
|
||||||
|
"entity"."sidecarPath" AS "entity_sidecarPath",
|
||||||
|
"entity"."stackId" AS "entity_stackId",
|
||||||
|
"exifInfo"."assetId" AS "exifInfo_assetId",
|
||||||
|
"exifInfo"."description" AS "exifInfo_description",
|
||||||
|
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
|
||||||
|
"exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight",
|
||||||
|
"exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte",
|
||||||
|
"exifInfo"."orientation" AS "exifInfo_orientation",
|
||||||
|
"exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal",
|
||||||
|
"exifInfo"."modifyDate" AS "exifInfo_modifyDate",
|
||||||
|
"exifInfo"."timeZone" AS "exifInfo_timeZone",
|
||||||
|
"exifInfo"."latitude" AS "exifInfo_latitude",
|
||||||
|
"exifInfo"."longitude" AS "exifInfo_longitude",
|
||||||
|
"exifInfo"."projectionType" AS "exifInfo_projectionType",
|
||||||
|
"exifInfo"."city" AS "exifInfo_city",
|
||||||
|
"exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
|
||||||
|
"exifInfo"."autoStackId" AS "exifInfo_autoStackId",
|
||||||
|
"exifInfo"."state" AS "exifInfo_state",
|
||||||
|
"exifInfo"."country" AS "exifInfo_country",
|
||||||
|
"exifInfo"."make" AS "exifInfo_make",
|
||||||
|
"exifInfo"."model" AS "exifInfo_model",
|
||||||
|
"exifInfo"."lensModel" AS "exifInfo_lensModel",
|
||||||
|
"exifInfo"."fNumber" AS "exifInfo_fNumber",
|
||||||
|
"exifInfo"."focalLength" AS "exifInfo_focalLength",
|
||||||
|
"exifInfo"."iso" AS "exifInfo_iso",
|
||||||
|
"exifInfo"."exposureTime" AS "exifInfo_exposureTime",
|
||||||
|
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||||
|
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||||
|
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||||
|
"exifInfo"."fps" AS "exifInfo_fps"
|
||||||
|
FROM
|
||||||
|
"assets" "entity"
|
||||||
|
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id"
|
||||||
|
WHERE
|
||||||
|
(
|
||||||
|
"entity"."ownerId" IN ($1)
|
||||||
|
AND "entity"."isVisible" = true
|
||||||
|
AND "entity"."isArchived" = false
|
||||||
|
AND "entity"."resizePath" IS NOT NULL
|
||||||
|
AND EXTRACT(
|
||||||
|
DAY
|
||||||
|
FROM
|
||||||
|
"entity"."localDateTime" AT TIME ZONE 'UTC'
|
||||||
|
) = $2
|
||||||
|
AND EXTRACT(
|
||||||
|
MONTH
|
||||||
|
FROM
|
||||||
|
"entity"."localDateTime" AT TIME ZONE 'UTC'
|
||||||
|
) = $3
|
||||||
|
)
|
||||||
|
AND ("entity"."deletedAt" IS NULL)
|
||||||
|
ORDER BY
|
||||||
|
"entity"."localDateTime" ASC
|
||||||
|
|
||||||
-- AssetRepository.getByIds
|
-- AssetRepository.getByIds
|
||||||
SELECT
|
SELECT
|
||||||
"AssetEntity"."id" AS "AssetEntity_id",
|
"AssetEntity"."id" AS "AssetEntity_id",
|
||||||
@ -170,6 +253,34 @@ DELETE FROM "assets"
|
|||||||
WHERE
|
WHERE
|
||||||
"ownerId" = $1
|
"ownerId" = $1
|
||||||
|
|
||||||
|
-- AssetRepository.getLibraryAssetPaths
|
||||||
|
SELECT DISTINCT
|
||||||
|
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
|
||||||
|
FROM
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
"AssetEntity"."id" AS "AssetEntity_id",
|
||||||
|
"AssetEntity"."originalPath" AS "AssetEntity_originalPath",
|
||||||
|
"AssetEntity"."isOffline" AS "AssetEntity_isOffline"
|
||||||
|
FROM
|
||||||
|
"assets" "AssetEntity"
|
||||||
|
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
|
||||||
|
AND (
|
||||||
|
"AssetEntity__AssetEntity_library"."deletedAt" IS NULL
|
||||||
|
)
|
||||||
|
WHERE
|
||||||
|
(
|
||||||
|
(
|
||||||
|
((("AssetEntity__AssetEntity_library"."id" = $1)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND ("AssetEntity"."deletedAt" IS NULL)
|
||||||
|
) "distinctAlias"
|
||||||
|
ORDER BY
|
||||||
|
"AssetEntity_id" ASC
|
||||||
|
LIMIT
|
||||||
|
2
|
||||||
|
|
||||||
-- AssetRepository.getByLibraryIdAndOriginalPath
|
-- AssetRepository.getByLibraryIdAndOriginalPath
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
|
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
|
||||||
|
@ -1 +0,0 @@
|
|||||||
-- NOTE: This file is auto generated by ./sql-generator
|
|
66
server/src/queries/metadata.repository.sql
Normal file
66
server/src/queries/metadata.repository.sql
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
-- NOTE: This file is auto generated by ./sql-generator
|
||||||
|
|
||||||
|
-- MetadataRepository.getCountries
|
||||||
|
SELECT DISTINCT
|
||||||
|
ON ("exif"."country") "exif"."country" AS "exif_country",
|
||||||
|
"exif"."assetId" AS "exif_assetId"
|
||||||
|
FROM
|
||||||
|
"exif" "exif"
|
||||||
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
|
WHERE
|
||||||
|
"asset"."ownerId" = $1
|
||||||
|
AND "exif"."country" IS NOT NULL
|
||||||
|
|
||||||
|
-- MetadataRepository.getStates
|
||||||
|
SELECT DISTINCT
|
||||||
|
ON ("exif"."state") "exif"."state" AS "exif_state",
|
||||||
|
"exif"."assetId" AS "exif_assetId"
|
||||||
|
FROM
|
||||||
|
"exif" "exif"
|
||||||
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
|
WHERE
|
||||||
|
"asset"."ownerId" = $1
|
||||||
|
AND "exif"."state" IS NOT NULL
|
||||||
|
AND "exif"."country" = $2
|
||||||
|
|
||||||
|
-- MetadataRepository.getCities
|
||||||
|
SELECT DISTINCT
|
||||||
|
ON ("exif"."city") "exif"."city" AS "exif_city",
|
||||||
|
"exif"."assetId" AS "exif_assetId"
|
||||||
|
FROM
|
||||||
|
"exif" "exif"
|
||||||
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
|
WHERE
|
||||||
|
"asset"."ownerId" = $1
|
||||||
|
AND "exif"."city" IS NOT NULL
|
||||||
|
AND "exif"."country" = $2
|
||||||
|
AND "exif"."state" = $3
|
||||||
|
|
||||||
|
-- MetadataRepository.getCameraMakes
|
||||||
|
SELECT DISTINCT
|
||||||
|
ON ("exif"."make") "exif"."make" AS "exif_make",
|
||||||
|
"exif"."assetId" AS "exif_assetId"
|
||||||
|
FROM
|
||||||
|
"exif" "exif"
|
||||||
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
|
WHERE
|
||||||
|
"asset"."ownerId" = $1
|
||||||
|
AND "exif"."make" IS NOT NULL
|
||||||
|
AND "exif"."model" = $2
|
||||||
|
|
||||||
|
-- MetadataRepository.getCameraModels
|
||||||
|
SELECT DISTINCT
|
||||||
|
ON ("exif"."model") "exif"."model" AS "exif_model",
|
||||||
|
"exif"."assetId" AS "exif_assetId"
|
||||||
|
FROM
|
||||||
|
"exif" "exif"
|
||||||
|
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
|
||||||
|
AND ("asset"."deletedAt" IS NULL)
|
||||||
|
WHERE
|
||||||
|
"asset"."ownerId" = $1
|
||||||
|
AND "exif"."model" IS NOT NULL
|
||||||
|
AND "exif"."make" = $2
|
@ -1 +0,0 @@
|
|||||||
-- NOTE: This file is auto generated by ./sql-generator
|
|
@ -278,7 +278,7 @@ WITH RECURSIVE
|
|||||||
exif
|
exif
|
||||||
INNER JOIN assets ON exif."assetId" = assets.id
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
WHERE
|
WHERE
|
||||||
"ownerId" = ANY ('$1'::uuid [])
|
"ownerId" = ANY ($1::uuid [])
|
||||||
AND "isVisible" = $2
|
AND "isVisible" = $2
|
||||||
AND "isArchived" = $3
|
AND "isArchived" = $3
|
||||||
AND type = $4
|
AND type = $4
|
||||||
@ -302,7 +302,7 @@ WITH RECURSIVE
|
|||||||
INNER JOIN assets ON exif."assetId" = assets.id
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
WHERE
|
WHERE
|
||||||
city > c.city
|
city > c.city
|
||||||
AND "ownerId" = ANY ('$1'::uuid [])
|
AND "ownerId" = ANY ($1::uuid [])
|
||||||
AND "isVisible" = $2
|
AND "isVisible" = $2
|
||||||
AND "isArchived" = $3
|
AND "isArchived" = $3
|
||||||
AND type = $4
|
AND type = $4
|
||||||
|
@ -1 +0,0 @@
|
|||||||
-- NOTE: This file is auto generated by ./sql-generator
|
|
@ -1 +0,0 @@
|
|||||||
-- NOTE: This file is auto generated by ./sql-generator
|
|
@ -5,13 +5,7 @@ import { dataSource } from 'src/database.config';
|
|||||||
import { Chunked, ChunkedArray, DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/decorators';
|
import { Chunked, ChunkedArray, DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import {
|
import { AlbumAsset, AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
AlbumAsset,
|
|
||||||
AlbumAssetCount,
|
|
||||||
AlbumAssets,
|
|
||||||
AlbumInfoOptions,
|
|
||||||
IAlbumRepository,
|
|
||||||
} from 'src/interfaces/album.interface';
|
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { setUnion } from 'src/utils/set';
|
import { setUnion } from 'src/utils/set';
|
||||||
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
|
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
|
||||||
@ -203,7 +197,7 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||||
@Chunked({ paramIndex: 1 })
|
@Chunked({ paramIndex: 1 })
|
||||||
async removeAssets(albumId: string, assetIds: string[]): Promise<void> {
|
async removeAssetIds(albumId: string, assetIds: string[]): Promise<void> {
|
||||||
await this.dataSource
|
await this.dataSource
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.delete()
|
.delete()
|
||||||
@ -260,8 +254,8 @@ export class AlbumRepository implements IAlbumRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [{ albumId: DummyValue.UUID, assetIds: [DummyValue.UUID] }] })
|
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
|
||||||
async addAssets({ albumId, assetIds }: AlbumAssets): Promise<void> {
|
async addAssetIds(albumId: string, assetIds: string[]): Promise<void> {
|
||||||
await this.dataSource
|
await this.dataSource
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.insert()
|
.insert()
|
||||||
|
@ -75,7 +75,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
return this.repository.save(asset);
|
return this.repository.save(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
|
@GenerateSql({ params: [[DummyValue.UUID], { day: 1, month: 1 }] })
|
||||||
getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise<AssetEntity[]> {
|
getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise<AssetEntity[]> {
|
||||||
return this.repository
|
return this.repository
|
||||||
.createQueryBuilder('entity')
|
.createQueryBuilder('entity')
|
||||||
@ -93,7 +93,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.leftJoinAndSelect('entity.exifInfo', 'exifInfo')
|
.leftJoinAndSelect('entity.exifInfo', 'exifInfo')
|
||||||
.orderBy('entity.localDateTime', 'DESC')
|
.orderBy('entity.localDateTime', 'ASC')
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +159,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
return this.getAll(pagination, { ...options, userIds: [userId] });
|
return this.getAll(pagination, { ...options, userIds: [userId] });
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID]] })
|
@GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] })
|
||||||
getLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity> {
|
getLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity> {
|
||||||
return paginate(this.repository, pagination, {
|
return paginate(this.repository, pagination, {
|
||||||
select: { id: true, originalPath: true, isOffline: true },
|
select: { id: true, originalPath: true, isOffline: true },
|
||||||
|
90
server/src/repositories/index.ts
Normal file
90
server/src/repositories/index.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
|
import { IActivityRepository } from 'src/interfaces/activity.interface';
|
||||||
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
|
import { IKeyRepository } from 'src/interfaces/api-key.interface';
|
||||||
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
|
import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
||||||
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
|
import { IAuditRepository } from 'src/interfaces/audit.interface';
|
||||||
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
|
import { IJobRepository } from 'src/interfaces/job.interface';
|
||||||
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||||
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||||
|
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||||
|
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
||||||
|
import { IMetricRepository } from 'src/interfaces/metric.interface';
|
||||||
|
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||||
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
|
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||||
|
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||||
|
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
|
||||||
|
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
|
||||||
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
|
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||||
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
|
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||||
|
import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
|
||||||
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||||
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||||
|
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
||||||
|
import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
|
||||||
|
import { AssetRepositoryV1 } from 'src/repositories/asset-v1.repository';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
import { AuditRepository } from 'src/repositories/audit.repository';
|
||||||
|
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||||
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||||
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
|
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||||
|
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
||||||
|
import { MediaRepository } from 'src/repositories/media.repository';
|
||||||
|
import { MetadataRepository } from 'src/repositories/metadata.repository';
|
||||||
|
import { MetricRepository } from 'src/repositories/metric.repository';
|
||||||
|
import { MoveRepository } from 'src/repositories/move.repository';
|
||||||
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
|
import { ServerInfoRepository } from 'src/repositories/server-info.repository';
|
||||||
|
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||||
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
|
import { SystemConfigRepository } from 'src/repositories/system-config.repository';
|
||||||
|
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
||||||
|
import { TagRepository } from 'src/repositories/tag.repository';
|
||||||
|
import { UserTokenRepository } from 'src/repositories/user-token.repository';
|
||||||
|
import { UserRepository } from 'src/repositories/user.repository';
|
||||||
|
|
||||||
|
export const repositories = [
|
||||||
|
{ provide: IActivityRepository, useClass: ActivityRepository },
|
||||||
|
{ provide: IAccessRepository, useClass: AccessRepository },
|
||||||
|
{ provide: IAlbumRepository, useClass: AlbumRepository },
|
||||||
|
{ provide: IAssetRepository, useClass: AssetRepository },
|
||||||
|
{ provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
|
||||||
|
{ provide: IAssetStackRepository, useClass: AssetStackRepository },
|
||||||
|
{ provide: IAuditRepository, useClass: AuditRepository },
|
||||||
|
{ provide: ICryptoRepository, useClass: CryptoRepository },
|
||||||
|
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
|
||||||
|
{ provide: IEventRepository, useClass: EventRepository },
|
||||||
|
{ provide: IJobRepository, useClass: JobRepository },
|
||||||
|
{ provide: ILibraryRepository, useClass: LibraryRepository },
|
||||||
|
{ provide: IKeyRepository, useClass: ApiKeyRepository },
|
||||||
|
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
|
||||||
|
{ provide: IMetadataRepository, useClass: MetadataRepository },
|
||||||
|
{ provide: IMetricRepository, useClass: MetricRepository },
|
||||||
|
{ provide: IMoveRepository, useClass: MoveRepository },
|
||||||
|
{ provide: IPartnerRepository, useClass: PartnerRepository },
|
||||||
|
{ provide: IPersonRepository, useClass: PersonRepository },
|
||||||
|
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
|
||||||
|
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
|
||||||
|
{ provide: ISearchRepository, useClass: SearchRepository },
|
||||||
|
{ provide: IStorageRepository, useClass: StorageRepository },
|
||||||
|
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository },
|
||||||
|
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
|
||||||
|
{ provide: ITagRepository, useClass: TagRepository },
|
||||||
|
{ provide: IMediaRepository, useClass: MediaRepository },
|
||||||
|
{ provide: IUserRepository, useClass: UserRepository },
|
||||||
|
{ provide: IUserTokenRepository, useClass: UserTokenRepository },
|
||||||
|
];
|
@ -5,7 +5,7 @@ import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
|||||||
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
|
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
|
||||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { IsNull, Not } from 'typeorm';
|
import { EntityNotFoundError, IsNull, Not } from 'typeorm';
|
||||||
import { Repository } from 'typeorm/repository/Repository.js';
|
import { Repository } from 'typeorm/repository/Repository.js';
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
@ -139,6 +139,10 @@ export class LibraryRepository implements ILibraryRepository {
|
|||||||
.where('libraries.id = :id', { id })
|
.where('libraries.id = :id', { id })
|
||||||
.getRawOne();
|
.getRawOne();
|
||||||
|
|
||||||
|
if (!stats) {
|
||||||
|
throw new EntityNotFoundError(LibraryEntity, { where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
photos: Number(stats.photos),
|
photos: Number(stats.photos),
|
||||||
videos: Number(stats.videos),
|
videos: Number(stats.videos),
|
||||||
|
@ -107,6 +107,7 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
getFaceById(id: string): Promise<AssetFaceEntity> {
|
getFaceById(id: string): Promise<AssetFaceEntity> {
|
||||||
|
// TODO return null instead of find or fail
|
||||||
return this.assetFaceRepository.findOneOrFail({
|
return this.assetFaceRepository.findOneOrFail({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: {
|
relations: {
|
||||||
|
@ -225,7 +225,7 @@ export class SearchRepository implements ISearchRepository {
|
|||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[DummyValue.UUID, DummyValue.UUID]] })
|
@GenerateSql({ params: [[DummyValue.UUID]] })
|
||||||
async getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
|
async getAssetsByCity(userIds: string[]): Promise<AssetEntity[]> {
|
||||||
const parameters = [userIds, true, false, AssetType.IMAGE];
|
const parameters = [userIds, true, false, AssetType.IMAGE];
|
||||||
const rawRes = await this.repository.query(this.assetsByCityQuery, parameters);
|
const rawRes = await this.repository.query(this.assetsByCityQuery, parameters);
|
||||||
@ -315,7 +315,7 @@ WITH RECURSIVE cte AS (
|
|||||||
SELECT city, "assetId"
|
SELECT city, "assetId"
|
||||||
FROM exif
|
FROM exif
|
||||||
INNER JOIN assets ON exif."assetId" = assets.id
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
WHERE "ownerId" = ANY('$1'::uuid[]) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4
|
WHERE "ownerId" = ANY($1::uuid[]) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4
|
||||||
ORDER BY city
|
ORDER BY city
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
@ -328,7 +328,7 @@ WITH RECURSIVE cte AS (
|
|||||||
SELECT city, "assetId"
|
SELECT city, "assetId"
|
||||||
FROM exif
|
FROM exif
|
||||||
INNER JOIN assets ON exif."assetId" = assets.id
|
INNER JOIN assets ON exif."assetId" = assets.id
|
||||||
WHERE city > c.city AND "ownerId" = ANY('$1'::uuid[]) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4
|
WHERE city > c.city AND "ownerId" = ANY($1::uuid[]) AND "isVisible" = $2 AND "isArchived" = $3 AND type = $4
|
||||||
ORDER BY city
|
ORDER BY city
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
) l
|
) l
|
||||||
|
@ -518,10 +518,7 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(albumMock.addAssets).toHaveBeenCalledWith({
|
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||||
albumId: 'album-123',
|
|
||||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not set the thumbnail if the album has one already', async () => {
|
it('should not set the thumbnail if the album has one already', async () => {
|
||||||
@ -539,7 +536,7 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-id',
|
albumThumbnailAssetId: 'asset-id',
|
||||||
});
|
});
|
||||||
expect(albumMock.addAssets).toHaveBeenCalled();
|
expect(albumMock.addAssetIds).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow a shared user to add assets', async () => {
|
it('should allow a shared user to add assets', async () => {
|
||||||
@ -561,10 +558,7 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(albumMock.addAssets).toHaveBeenCalledWith({
|
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||||
albumId: 'album-123',
|
|
||||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow a shared link user to add assets', async () => {
|
it('should allow a shared link user to add assets', async () => {
|
||||||
@ -586,10 +580,7 @@ describe(AlbumService.name, () => {
|
|||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
albumThumbnailAssetId: 'asset-1',
|
albumThumbnailAssetId: 'asset-1',
|
||||||
});
|
});
|
||||||
expect(albumMock.addAssets).toHaveBeenCalledWith({
|
expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||||
albumId: 'album-123',
|
|
||||||
assetIds: ['asset-1', 'asset-2', 'asset-3'],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
|
expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith(
|
||||||
authStub.adminSharedLink.sharedLink?.id,
|
authStub.adminSharedLink.sharedLink?.id,
|
||||||
@ -665,23 +656,23 @@ describe(AlbumService.name, () => {
|
|||||||
|
|
||||||
describe('removeAssets', () => {
|
describe('removeAssets', () => {
|
||||||
it('should allow the owner to remove assets', async () => {
|
it('should allow the owner to remove assets', async () => {
|
||||||
accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123']));
|
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||||
accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id']));
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id']));
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
|
albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id']));
|
||||||
|
|
||||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
{ success: true, id: 'asset-id' },
|
{ success: true, id: 'asset-id' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) });
|
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) });
|
||||||
expect(albumMock.removeAssets).toHaveBeenCalledWith('album-123', ['asset-id']);
|
expect(albumMock.removeAssetIds).toHaveBeenCalledWith('album-123', ['asset-id']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip assets not in the album', async () => {
|
it('should skip assets not in the album', async () => {
|
||||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.empty));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.empty));
|
||||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set());
|
albumMock.getAssetIds.mockResolvedValue(new Set());
|
||||||
|
|
||||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
{ success: false, id: 'asset-id', error: BulkIdErrorReason.NOT_FOUND },
|
{ success: false, id: 'asset-id', error: BulkIdErrorReason.NOT_FOUND },
|
||||||
@ -693,7 +684,7 @@ describe(AlbumService.name, () => {
|
|||||||
it('should skip assets without user permission to remove', async () => {
|
it('should skip assets without user permission to remove', async () => {
|
||||||
accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
|
accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123']));
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
|
||||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
|
albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id']));
|
||||||
|
|
||||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
{
|
{
|
||||||
@ -707,10 +698,10 @@ describe(AlbumService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reset the thumbnail if it is removed', async () => {
|
it('should reset the thumbnail if it is removed', async () => {
|
||||||
accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123']));
|
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123']));
|
||||||
accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id']));
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-id']));
|
||||||
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
|
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
|
||||||
albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id']));
|
albumMock.getAssetIds.mockResolvedValue(new Set(['asset-id']));
|
||||||
|
|
||||||
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
|
||||||
{ success: true, id: 'asset-id' },
|
{ success: true, id: 'asset-id' },
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
mapAlbumWithAssets,
|
mapAlbumWithAssets,
|
||||||
mapAlbumWithoutAssets,
|
mapAlbumWithoutAssets,
|
||||||
} from 'src/dtos/album.dto';
|
} from 'src/dtos/album.dto';
|
||||||
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
@ -21,13 +21,13 @@ import { IAccessRepository } from 'src/interfaces/access.interface';
|
|||||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { setUnion } from 'src/utils/set';
|
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AlbumService {
|
export class AlbumService {
|
||||||
private access: AccessCore;
|
private access: AccessCore;
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
@Inject(IAccessRepository) private accessRepository: IAccessRepository,
|
||||||
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private albumRepository: IAlbumRepository,
|
||||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||||
@ -164,37 +164,20 @@ export class AlbumService {
|
|||||||
|
|
||||||
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
|
||||||
const album = await this.findOrFail(id, { withAssets: false });
|
const album = await this.findOrFail(id, { withAssets: false });
|
||||||
|
|
||||||
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
|
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
|
||||||
|
|
||||||
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
|
const results = await addAssets(
|
||||||
const notPresentAssetIds = dto.ids.filter((id) => !existingAssetIds.has(id));
|
auth,
|
||||||
const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds);
|
{ accessRepository: this.accessRepository, repository: this.albumRepository },
|
||||||
|
{ id, assetIds: dto.ids },
|
||||||
|
);
|
||||||
|
|
||||||
const results: BulkIdResponseDto[] = [];
|
const { id: firstNewAssetId } = results.find(({ success }) => success) || {};
|
||||||
for (const assetId of dto.ids) {
|
if (firstNewAssetId) {
|
||||||
const hasAsset = existingAssetIds.has(assetId);
|
|
||||||
if (hasAsset) {
|
|
||||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.DUPLICATE });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasAccess = allowedAssetIds.has(assetId);
|
|
||||||
if (!hasAccess) {
|
|
||||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ id: assetId, success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id);
|
|
||||||
if (newAssetIds.length > 0) {
|
|
||||||
await this.albumRepository.addAssets({ albumId: id, assetIds: newAssetIds });
|
|
||||||
await this.albumRepository.update({
|
await this.albumRepository.update({
|
||||||
id,
|
id,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAssetIds[0],
|
albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,31 +189,14 @@ export class AlbumService {
|
|||||||
|
|
||||||
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
|
await this.access.requirePermission(auth, Permission.ALBUM_READ, id);
|
||||||
|
|
||||||
const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids);
|
const results = await removeAssets(
|
||||||
const canRemove = await this.access.checkAccess(auth, Permission.ALBUM_REMOVE_ASSET, existingAssetIds);
|
auth,
|
||||||
const canShare = await this.access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds);
|
{ accessRepository: this.accessRepository, repository: this.albumRepository },
|
||||||
const allowedAssetIds = setUnion(canRemove, canShare);
|
{ id, assetIds: dto.ids, permissions: [Permission.ASSET_SHARE, Permission.ALBUM_REMOVE_ASSET] },
|
||||||
|
);
|
||||||
const results: BulkIdResponseDto[] = [];
|
|
||||||
for (const assetId of dto.ids) {
|
|
||||||
const hasAsset = existingAssetIds.has(assetId);
|
|
||||||
if (!hasAsset) {
|
|
||||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NOT_FOUND });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasAccess = allowedAssetIds.has(assetId);
|
|
||||||
if (!hasAccess) {
|
|
||||||
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({ id: assetId, success: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
|
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||||
if (removedIds.length > 0) {
|
if (removedIds.length > 0) {
|
||||||
await this.albumRepository.removeAssets(id, removedIds);
|
|
||||||
await this.albumRepository.update({ id, updatedAt: new Date() });
|
await this.albumRepository.update({ id, updatedAt: new Date() });
|
||||||
if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
|
||||||
await this.albumRepository.updateThumbnails();
|
await this.albumRepository.updateThumbnails();
|
||||||
|
@ -4,7 +4,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
|
|||||||
import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
|
import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
|
||||||
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
import { AssetStats, IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface';
|
import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository, JobItem, JobName } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobItem, JobName } from 'src/interfaces/job.interface';
|
||||||
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
@ -307,13 +307,17 @@ describe(AssetService.name, () => {
|
|||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the title correctly', async () => {
|
it('should group the assets correctly', async () => {
|
||||||
|
const image1 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 0, 0, 0) };
|
||||||
|
const image2 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 1, 0, 0) };
|
||||||
|
const image3 = { ...assetStub.image, localDateTime: new Date(2015, 1, 15) };
|
||||||
|
|
||||||
partnerMock.getAll.mockResolvedValue([]);
|
partnerMock.getAll.mockResolvedValue([]);
|
||||||
assetMock.getByDayOfYear.mockResolvedValue([assetStub.image, assetStub.imageFrom2015]);
|
assetMock.getByDayOfYear.mockResolvedValue([image1, image2, image3]);
|
||||||
|
|
||||||
await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([
|
await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([
|
||||||
{ title: '1 year since...', assets: [mapAsset(assetStub.image)] },
|
{ yearsAgo: 1, title: '1 year since...', assets: [mapAsset(image1), mapAsset(image2)] },
|
||||||
{ title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] },
|
{ yearsAgo: 9, title: '9 years since...', assets: [mapAsset(image3)] },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]);
|
expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]);
|
||||||
@ -321,6 +325,7 @@ describe(AssetService.name, () => {
|
|||||||
|
|
||||||
it('should get memories with partners with inTimeline enabled', async () => {
|
it('should get memories with partners with inTimeline enabled', async () => {
|
||||||
partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
|
partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
|
||||||
|
assetMock.getByDayOfYear.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 });
|
await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 });
|
||||||
|
|
||||||
@ -330,129 +335,6 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getTimeBuckets', () => {
|
|
||||||
it("should return buckets if userId and albumId aren't set", async () => {
|
|
||||||
assetMock.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBuckets(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]));
|
|
||||||
expect(assetMock.getTimeBuckets).toBeCalledWith({ size: TimeBucketSize.DAY, userIds: [authStub.admin.user.id] });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getTimeBucket', () => {
|
|
||||||
it('should return the assets for a album time bucket if user has album.read', async () => {
|
|
||||||
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
|
||||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
|
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
|
||||||
|
|
||||||
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
|
||||||
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
albumId: 'album-id',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
|
||||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
|
||||||
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: true,
|
|
||||||
userIds: [authStub.admin.user.id],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the assets for a library time bucket if user has library.read', async () => {
|
|
||||||
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
|
||||||
expect(assetMock.getTimeBucket).toBeCalledWith('bucket', {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
userIds: [authStub.admin.user.id],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: true,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isArchived: undefined,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if withParners is true and isFavorite is either true or false', async () => {
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isFavorite: true,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isFavorite: false,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should throw an error if withParners is true and isTrash is true', async () => {
|
|
||||||
await expect(
|
|
||||||
sut.getTimeBucket(authStub.admin, {
|
|
||||||
size: TimeBucketSize.DAY,
|
|
||||||
timeBucket: 'bucket',
|
|
||||||
isTrashed: true,
|
|
||||||
withPartners: true,
|
|
||||||
userId: authStub.admin.user.id,
|
|
||||||
}),
|
|
||||||
).rejects.toThrowError(BadRequestException);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getStatistics', () => {
|
describe('getStatistics', () => {
|
||||||
it('should get the statistics for a user, excluding archived assets', async () => {
|
it('should get the statistics for a user, excluding archived assets', async () => {
|
||||||
assetMock.getStatistics.mockResolvedValue(stats);
|
assetMock.getStatistics.mockResolvedValue(stats);
|
||||||
|
@ -25,12 +25,11 @@ import {
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
|
import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
|
||||||
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
import { UpdateStackParentDto } from 'src/dtos/stack.dto';
|
||||||
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { LibraryType } from 'src/entities/library.entity';
|
import { LibraryType } from 'src/entities/library.entity';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
|
||||||
import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
IAssetDeletionJob,
|
IAssetDeletionJob,
|
||||||
@ -174,86 +173,25 @@ export class AssetService {
|
|||||||
userIds.push(...partnersIds);
|
userIds.push(...partnersIds);
|
||||||
|
|
||||||
const assets = await this.assetRepository.getByDayOfYear(userIds, dto);
|
const assets = await this.assetRepository.getByDayOfYear(userIds, dto);
|
||||||
|
const groups: Record<number, AssetEntity[]> = {};
|
||||||
return _.chain(assets)
|
for (const asset of assets) {
|
||||||
.filter((asset) => asset.localDateTime.getFullYear() < currentYear)
|
const yearsAgo = currentYear - asset.localDateTime.getFullYear();
|
||||||
.map((asset) => {
|
if (!groups[yearsAgo]) {
|
||||||
const years = currentYear - asset.localDateTime.getFullYear();
|
groups[yearsAgo] = [];
|
||||||
|
}
|
||||||
return {
|
groups[yearsAgo].push(asset);
|
||||||
title: `${years} year${years > 1 ? 's' : ''} since...`,
|
|
||||||
asset: mapAsset(asset, { auth }),
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.groupBy((asset) => asset.title)
|
|
||||||
.map((items, title) => ({ title, assets: items.map(({ asset }) => asset) }))
|
|
||||||
.value();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
return Object.keys(groups)
|
||||||
if (dto.albumId) {
|
.map(Number)
|
||||||
await this.access.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
|
.sort()
|
||||||
} else {
|
.filter((yearsAgo) => yearsAgo > 0)
|
||||||
dto.userId = dto.userId || auth.user.id;
|
.map((yearsAgo) => ({
|
||||||
}
|
yearsAgo,
|
||||||
|
// TODO move this to clients
|
||||||
if (dto.userId) {
|
title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`,
|
||||||
await this.access.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]);
|
assets: groups[yearsAgo].map((asset) => mapAsset(asset, { auth })),
|
||||||
if (dto.isArchived !== false) {
|
}));
|
||||||
await this.access.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dto.withPartners) {
|
|
||||||
const requestedArchived = dto.isArchived === true || dto.isArchived === undefined;
|
|
||||||
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
|
||||||
const requestedTrash = dto.isTrashed === true;
|
|
||||||
|
|
||||||
if (requestedArchived || requestedFavorite || requestedTrash) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
|
||||||
await this.timeBucketChecks(auth, dto);
|
|
||||||
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
|
||||||
|
|
||||||
return this.assetRepository.getTimeBuckets(timeBucketOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTimeBucket(
|
|
||||||
auth: AuthDto,
|
|
||||||
dto: TimeBucketAssetDto,
|
|
||||||
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
|
|
||||||
await this.timeBucketChecks(auth, dto);
|
|
||||||
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
|
||||||
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
|
|
||||||
return !auth.sharedLink || auth.sharedLink?.showExif
|
|
||||||
? assets.map((asset) => mapAsset(asset, { withStack: true, auth }))
|
|
||||||
: assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
|
||||||
const { userId, ...options } = dto;
|
|
||||||
let userIds: string[] | undefined = undefined;
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
userIds = [userId];
|
|
||||||
|
|
||||||
if (dto.withPartners) {
|
|
||||||
const partners = await this.partnerRepository.getAll(auth.user.id);
|
|
||||||
const partnersIds = partners
|
|
||||||
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline)
|
|
||||||
.map((partner) => partner.sharedById);
|
|
||||||
|
|
||||||
userIds.push(...partnersIds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...options, userIds };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
|
async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
|
||||||
|
59
server/src/services/index.ts
Normal file
59
server/src/services/index.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { ActivityService } from 'src/services/activity.service';
|
||||||
|
import { AlbumService } from 'src/services/album.service';
|
||||||
|
import { APIKeyService } from 'src/services/api-key.service';
|
||||||
|
import { ApiService } from 'src/services/api.service';
|
||||||
|
import { AssetServiceV1 } from 'src/services/asset-v1.service';
|
||||||
|
import { AssetService } from 'src/services/asset.service';
|
||||||
|
import { AuditService } from 'src/services/audit.service';
|
||||||
|
import { AuthService } from 'src/services/auth.service';
|
||||||
|
import { DatabaseService } from 'src/services/database.service';
|
||||||
|
import { DownloadService } from 'src/services/download.service';
|
||||||
|
import { JobService } from 'src/services/job.service';
|
||||||
|
import { LibraryService } from 'src/services/library.service';
|
||||||
|
import { MediaService } from 'src/services/media.service';
|
||||||
|
import { MetadataService } from 'src/services/metadata.service';
|
||||||
|
import { MicroservicesService } from 'src/services/microservices.service';
|
||||||
|
import { PartnerService } from 'src/services/partner.service';
|
||||||
|
import { PersonService } from 'src/services/person.service';
|
||||||
|
import { SearchService } from 'src/services/search.service';
|
||||||
|
import { ServerInfoService } from 'src/services/server-info.service';
|
||||||
|
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||||
|
import { SmartInfoService } from 'src/services/smart-info.service';
|
||||||
|
import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||||
|
import { StorageService } from 'src/services/storage.service';
|
||||||
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
import { TagService } from 'src/services/tag.service';
|
||||||
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
|
import { TrashService } from 'src/services/trash.service';
|
||||||
|
import { UserService } from 'src/services/user.service';
|
||||||
|
|
||||||
|
export const services = [
|
||||||
|
ApiService,
|
||||||
|
MicroservicesService,
|
||||||
|
APIKeyService,
|
||||||
|
ActivityService,
|
||||||
|
AlbumService,
|
||||||
|
AssetService,
|
||||||
|
AssetServiceV1,
|
||||||
|
AuditService,
|
||||||
|
AuthService,
|
||||||
|
DatabaseService,
|
||||||
|
DownloadService,
|
||||||
|
JobService,
|
||||||
|
LibraryService,
|
||||||
|
MediaService,
|
||||||
|
MetadataService,
|
||||||
|
PartnerService,
|
||||||
|
PersonService,
|
||||||
|
SearchService,
|
||||||
|
ServerInfoService,
|
||||||
|
SharedLinkService,
|
||||||
|
SmartInfoService,
|
||||||
|
StorageService,
|
||||||
|
StorageTemplateService,
|
||||||
|
SystemConfigService,
|
||||||
|
TagService,
|
||||||
|
TimelineService,
|
||||||
|
TrashService,
|
||||||
|
UserService,
|
||||||
|
];
|
@ -549,17 +549,17 @@ describe(LibraryService.name, () => {
|
|||||||
const mockLibraryJob: ILibraryFileJob = {
|
const mockLibraryJob: ILibraryFileJob = {
|
||||||
id: libraryStub.externalLibrary1.id,
|
id: libraryStub.externalLibrary1.id,
|
||||||
ownerId: mockUser.id,
|
ownerId: mockUser.id,
|
||||||
assetPath: '/data/user1/photo.jpg',
|
assetPath: assetStub.hasFileExtension.originalPath,
|
||||||
force: false,
|
force: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
storageMock.stat.mockResolvedValue({
|
storageMock.stat.mockResolvedValue({
|
||||||
size: 100,
|
size: 100,
|
||||||
mtime: assetStub.image.fileModifiedAt,
|
mtime: assetStub.hasFileExtension.fileModifiedAt,
|
||||||
ctime: new Date('2023-01-01'),
|
ctime: new Date('2023-01-01'),
|
||||||
} as Stats);
|
} as Stats);
|
||||||
|
|
||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED);
|
||||||
|
|
||||||
@ -596,6 +596,26 @@ describe(LibraryService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should import an asset that is missing a file extension', async () => {
|
||||||
|
// This tests for the case where the file extension is missing from the asset path.
|
||||||
|
// This happened in previous versions of Immich
|
||||||
|
const mockLibraryJob: ILibraryFileJob = {
|
||||||
|
id: libraryStub.externalLibrary1.id,
|
||||||
|
ownerId: mockUser.id,
|
||||||
|
assetPath: assetStub.missingFileExtension.originalPath,
|
||||||
|
force: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.missingFileExtension);
|
||||||
|
|
||||||
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
|
expect(assetMock.updateAll).toHaveBeenCalledWith(
|
||||||
|
[assetStub.missingFileExtension.id],
|
||||||
|
expect.objectContaining({ originalFileName: 'photo.jpg' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should set a missing asset to offline', async () => {
|
it('should set a missing asset to offline', async () => {
|
||||||
storageMock.stat.mockRejectedValue(new Error('Path not found'));
|
storageMock.stat.mockRejectedValue(new Error('Path not found'));
|
||||||
|
|
||||||
@ -666,19 +686,20 @@ describe(LibraryService.name, () => {
|
|||||||
it('should refresh an existing asset if forced', async () => {
|
it('should refresh an existing asset if forced', async () => {
|
||||||
const mockLibraryJob: ILibraryFileJob = {
|
const mockLibraryJob: ILibraryFileJob = {
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
ownerId: assetStub.image.ownerId,
|
ownerId: assetStub.hasFileExtension.ownerId,
|
||||||
assetPath: '/data/user1/photo.jpg',
|
assetPath: assetStub.hasFileExtension.originalPath,
|
||||||
force: true,
|
force: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension);
|
||||||
assetMock.create.mockResolvedValue(assetStub.image);
|
assetMock.create.mockResolvedValue(assetStub.hasFileExtension);
|
||||||
|
|
||||||
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
|
||||||
|
|
||||||
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], {
|
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasFileExtension.id], {
|
||||||
fileCreatedAt: new Date('2023-01-01'),
|
fileCreatedAt: new Date('2023-01-01'),
|
||||||
fileModifiedAt: new Date('2023-01-01'),
|
fileModifiedAt: new Date('2023-01-01'),
|
||||||
|
originalFileName: assetStub.hasFileExtension.originalFileName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -436,6 +436,8 @@ export class LibraryService extends EventEmitter {
|
|||||||
doRefresh = true;
|
doRefresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const originalFileName = parse(assetPath).base;
|
||||||
|
|
||||||
if (!existingAssetEntity) {
|
if (!existingAssetEntity) {
|
||||||
// This asset is new to us, read it from disk
|
// This asset is new to us, read it from disk
|
||||||
this.logger.debug(`Importing new asset: ${assetPath}`);
|
this.logger.debug(`Importing new asset: ${assetPath}`);
|
||||||
@ -446,6 +448,12 @@ export class LibraryService extends EventEmitter {
|
|||||||
`File modification time has changed, re-importing asset: ${assetPath}. Old mtime: ${existingAssetEntity.fileModifiedAt}. New mtime: ${stats.mtime}`,
|
`File modification time has changed, re-importing asset: ${assetPath}. Old mtime: ${existingAssetEntity.fileModifiedAt}. New mtime: ${stats.mtime}`,
|
||||||
);
|
);
|
||||||
doRefresh = true;
|
doRefresh = true;
|
||||||
|
} else if (existingAssetEntity.originalFileName !== originalFileName) {
|
||||||
|
// TODO: We can likely remove this check in the second half of 2024 when all assets have likely been re-imported by all users
|
||||||
|
this.logger.debug(
|
||||||
|
`Asset is missing file extension, re-importing: ${assetPath}. Current incorrect filename: ${existingAssetEntity.originalFileName}.`,
|
||||||
|
);
|
||||||
|
doRefresh = true;
|
||||||
} else if (!job.force && stats && !existingAssetEntity.isOffline) {
|
} else if (!job.force && stats && !existingAssetEntity.isOffline) {
|
||||||
// Asset exists on disk and in db and mtime has not changed. Also, we are not forcing refresn. Therefore, do nothing
|
// Asset exists on disk and in db and mtime has not changed. Also, we are not forcing refresn. Therefore, do nothing
|
||||||
this.logger.debug(`Asset already exists in database and on disk, will not import: ${assetPath}`);
|
this.logger.debug(`Asset already exists in database and on disk, will not import: ${assetPath}`);
|
||||||
@ -504,7 +512,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
fileModifiedAt: stats.mtime,
|
fileModifiedAt: stats.mtime,
|
||||||
localDateTime: stats.mtime,
|
localDateTime: stats.mtime,
|
||||||
type: assetType,
|
type: assetType,
|
||||||
originalFileName: parse(assetPath).base,
|
originalFileName,
|
||||||
sidecarPath,
|
sidecarPath,
|
||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
isExternal: true,
|
isExternal: true,
|
||||||
@ -515,6 +523,7 @@ export class LibraryService extends EventEmitter {
|
|||||||
await this.assetRepository.updateAll([existingAssetEntity.id], {
|
await this.assetRepository.updateAll([existingAssetEntity.id], {
|
||||||
fileCreatedAt: stats.mtime,
|
fileCreatedAt: stats.mtime,
|
||||||
fileModifiedAt: stats.mtime,
|
fileModifiedAt: stats.mtime,
|
||||||
|
originalFileName,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Not importing and not refreshing, do nothing
|
// Not importing and not refreshing, do nothing
|
||||||
|
149
server/src/services/timeline.service.spec.ts
Normal file
149
server/src/services/timeline.service.spec.ts
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface';
|
||||||
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
|
import { TimelineService } from 'src/services/timeline.service';
|
||||||
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
|
import { authStub } from 'test/fixtures/auth.stub';
|
||||||
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||||
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
|
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
|
||||||
|
|
||||||
|
describe(TimelineService.name, () => {
|
||||||
|
let sut: TimelineService;
|
||||||
|
let accessMock: IAccessRepositoryMock;
|
||||||
|
let assetMock: jest.Mocked<IAssetRepository>;
|
||||||
|
let partnerMock: jest.Mocked<IPartnerRepository>;
|
||||||
|
beforeEach(() => {
|
||||||
|
accessMock = newAccessRepositoryMock();
|
||||||
|
assetMock = newAssetRepositoryMock();
|
||||||
|
partnerMock = newPartnerRepositoryMock();
|
||||||
|
|
||||||
|
sut = new TimelineService(accessMock, assetMock, partnerMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTimeBuckets', () => {
|
||||||
|
it("should return buckets if userId and albumId aren't set", async () => {
|
||||||
|
assetMock.getTimeBuckets.mockResolvedValue([{ timeBucket: 'bucket', count: 1 }]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBuckets(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expect.arrayContaining([{ timeBucket: 'bucket', count: 1 }]));
|
||||||
|
expect(assetMock.getTimeBuckets).toHaveBeenCalledWith({
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getTimeBucket', () => {
|
||||||
|
it('should return the assets for a album time bucket if user has album.read', async () => {
|
||||||
|
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id']));
|
||||||
|
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }),
|
||||||
|
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||||
|
|
||||||
|
expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-id']));
|
||||||
|
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
albumId: 'album-id',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the assets for a archive time bucket if user has archive.read', async () => {
|
||||||
|
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isArchived: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||||
|
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isArchived: true,
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the assets for a library time bucket if user has library.read', async () => {
|
||||||
|
assetMock.getTimeBucket.mockResolvedValue([assetStub.image]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
|
||||||
|
expect(assetMock.getTimeBucket).toHaveBeenCalledWith('bucket', {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
userIds: [authStub.admin.user.id],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isArchived: true,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isArchived: undefined,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if withParners is true and isFavorite is either true or false', async () => {
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isFavorite: true,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isFavorite: false,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if withParners is true and isTrash is true', async () => {
|
||||||
|
await expect(
|
||||||
|
sut.getTimeBucket(authStub.admin, {
|
||||||
|
size: TimeBucketSize.DAY,
|
||||||
|
timeBucket: 'bucket',
|
||||||
|
isTrashed: true,
|
||||||
|
withPartners: true,
|
||||||
|
userId: authStub.admin.user.id,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
86
server/src/services/timeline.service.ts
Normal file
86
server/src/services/timeline.service.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { BadRequestException, Inject } from '@nestjs/common';
|
||||||
|
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||||
|
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
|
||||||
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
|
import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface';
|
||||||
|
import { IPartnerRepository } from 'src/interfaces/partner.interface';
|
||||||
|
|
||||||
|
export class TimelineService {
|
||||||
|
private accessCore: AccessCore;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||||
|
@Inject(IAssetRepository) private repository: IAssetRepository,
|
||||||
|
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
|
||||||
|
) {
|
||||||
|
this.accessCore = AccessCore.create(accessRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketResponseDto[]> {
|
||||||
|
await this.timeBucketChecks(auth, dto);
|
||||||
|
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
||||||
|
|
||||||
|
return this.repository.getTimeBuckets(timeBucketOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTimeBucket(
|
||||||
|
auth: AuthDto,
|
||||||
|
dto: TimeBucketAssetDto,
|
||||||
|
): Promise<AssetResponseDto[] | SanitizedAssetResponseDto[]> {
|
||||||
|
await this.timeBucketChecks(auth, dto);
|
||||||
|
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
||||||
|
const assets = await this.repository.getTimeBucket(dto.timeBucket, timeBucketOptions);
|
||||||
|
return !auth.sharedLink || auth.sharedLink?.showExif
|
||||||
|
? assets.map((asset) => mapAsset(asset, { withStack: true, auth }))
|
||||||
|
: assets.map((asset) => mapAsset(asset, { stripMetadata: true, auth }));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
||||||
|
const { userId, ...options } = dto;
|
||||||
|
let userIds: string[] | undefined = undefined;
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
userIds = [userId];
|
||||||
|
|
||||||
|
if (dto.withPartners) {
|
||||||
|
const partners = await this.partnerRepository.getAll(auth.user.id);
|
||||||
|
const partnersIds = partners
|
||||||
|
.filter((partner) => partner.sharedBy && partner.sharedWith && partner.inTimeline)
|
||||||
|
.map((partner) => partner.sharedById);
|
||||||
|
|
||||||
|
userIds.push(...partnersIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...options, userIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {
|
||||||
|
if (dto.albumId) {
|
||||||
|
await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]);
|
||||||
|
} else {
|
||||||
|
dto.userId = dto.userId || auth.user.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.userId) {
|
||||||
|
await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]);
|
||||||
|
if (dto.isArchived !== false) {
|
||||||
|
await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.withPartners) {
|
||||||
|
const requestedArchived = dto.isArchived === true || dto.isArchived === undefined;
|
||||||
|
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||||
|
const requestedTrash = dto.isTrashed === true;
|
||||||
|
|
||||||
|
if (requestedArchived || requestedFavorite || requestedTrash) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
'withPartners is only supported for non-archived, non-trashed, non-favorited assets',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
server/src/utils/asset.util.ts
Normal file
91
server/src/utils/asset.util.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||||
|
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
|
import { setDifference, setUnion } from 'src/utils/set';
|
||||||
|
|
||||||
|
export interface IBulkAsset {
|
||||||
|
getAssetIds: (id: string, assetIds: string[]) => Promise<Set<string>>;
|
||||||
|
addAssetIds: (id: string, assetIds: string[]) => Promise<void>;
|
||||||
|
removeAssetIds: (id: string, assetIds: string[]) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addAssets = async (
|
||||||
|
auth: AuthDto,
|
||||||
|
repositories: { accessRepository: IAccessRepository; repository: IBulkAsset },
|
||||||
|
dto: { id: string; assetIds: string[] },
|
||||||
|
) => {
|
||||||
|
const { accessRepository, repository } = repositories;
|
||||||
|
const access = AccessCore.create(accessRepository);
|
||||||
|
|
||||||
|
const existingAssetIds = await repository.getAssetIds(dto.id, dto.assetIds);
|
||||||
|
const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id));
|
||||||
|
const allowedAssetIds = await access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds);
|
||||||
|
|
||||||
|
const results: BulkIdResponseDto[] = [];
|
||||||
|
for (const assetId of dto.assetIds) {
|
||||||
|
const hasAsset = existingAssetIds.has(assetId);
|
||||||
|
if (hasAsset) {
|
||||||
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.DUPLICATE });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAccess = allowedAssetIds.has(assetId);
|
||||||
|
if (!hasAccess) {
|
||||||
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ id: assetId, success: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||||
|
if (newAssetIds.length > 0) {
|
||||||
|
await repository.addAssetIds(dto.id, newAssetIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeAssets = async (
|
||||||
|
auth: AuthDto,
|
||||||
|
repositories: { accessRepository: IAccessRepository; repository: IBulkAsset },
|
||||||
|
dto: { id: string; assetIds: string[]; permissions: Permission[] },
|
||||||
|
) => {
|
||||||
|
const { accessRepository, repository } = repositories;
|
||||||
|
const access = AccessCore.create(accessRepository);
|
||||||
|
|
||||||
|
const existingAssetIds = await repository.getAssetIds(dto.id, dto.assetIds);
|
||||||
|
let allowedAssetIds = new Set<string>();
|
||||||
|
let remainingAssetIds = existingAssetIds;
|
||||||
|
|
||||||
|
for (const permission of dto.permissions) {
|
||||||
|
const newAssetIds = await access.checkAccess(auth, permission, setDifference(remainingAssetIds, allowedAssetIds));
|
||||||
|
remainingAssetIds = setDifference(remainingAssetIds, newAssetIds);
|
||||||
|
allowedAssetIds = setUnion(allowedAssetIds, newAssetIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: BulkIdResponseDto[] = [];
|
||||||
|
for (const assetId of dto.assetIds) {
|
||||||
|
const hasAsset = existingAssetIds.has(assetId);
|
||||||
|
if (!hasAsset) {
|
||||||
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NOT_FOUND });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAccess = allowedAssetIds.has(assetId);
|
||||||
|
if (!hasAccess) {
|
||||||
|
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
results.push({ id: assetId, success: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
|
||||||
|
if (removedIds.length > 0) {
|
||||||
|
await repository.removeAssetIds(dto.id, removedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
@ -1,31 +1,21 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||||
|
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||||
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { format } from 'sql-formatter';
|
import { format } from 'sql-formatter';
|
||||||
import { databaseConfig } from 'src/database.config';
|
import { databaseConfig } from 'src/database.config';
|
||||||
import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators';
|
import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators';
|
||||||
import { databaseEntities } from 'src/entities';
|
import { entities } from 'src/entities';
|
||||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
import { repositories } from 'src/repositories';
|
||||||
import { AccessRepository } from 'src/repositories/access.repository';
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
import { AuthService } from 'src/services/auth.service';
|
||||||
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
|
import { otelConfig } from 'src/utils/instrumentation';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
|
||||||
import { AuditRepository } from 'src/repositories/audit.repository';
|
|
||||||
import { LibraryRepository } from 'src/repositories/library.repository';
|
|
||||||
import { MoveRepository } from 'src/repositories/move.repository';
|
|
||||||
import { PartnerRepository } from 'src/repositories/partner.repository';
|
|
||||||
import { PersonRepository } from 'src/repositories/person.repository';
|
|
||||||
import { SearchRepository } from 'src/repositories/search.repository';
|
|
||||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
|
||||||
import { SystemConfigRepository } from 'src/repositories/system-config.repository';
|
|
||||||
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
|
|
||||||
import { TagRepository } from 'src/repositories/tag.repository';
|
|
||||||
import { UserTokenRepository } from 'src/repositories/user-token.repository';
|
|
||||||
import { UserRepository } from 'src/repositories/user.repository';
|
|
||||||
import { Logger } from 'typeorm';
|
import { Logger } from 'typeorm';
|
||||||
|
|
||||||
export class SqlLogger implements Logger {
|
export class SqlLogger implements Logger {
|
||||||
@ -52,26 +42,9 @@ export class SqlLogger implements Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const reflector = new Reflector();
|
const reflector = new Reflector();
|
||||||
const repositories = [
|
|
||||||
AccessRepository,
|
|
||||||
AlbumRepository,
|
|
||||||
ApiKeyRepository,
|
|
||||||
AssetRepository,
|
|
||||||
AuditRepository,
|
|
||||||
LibraryRepository,
|
|
||||||
MoveRepository,
|
|
||||||
PartnerRepository,
|
|
||||||
PersonRepository,
|
|
||||||
SharedLinkRepository,
|
|
||||||
SearchRepository,
|
|
||||||
SystemConfigRepository,
|
|
||||||
SystemMetadataRepository,
|
|
||||||
TagRepository,
|
|
||||||
UserTokenRepository,
|
|
||||||
UserRepository,
|
|
||||||
];
|
|
||||||
|
|
||||||
type Repository = (typeof repositories)[0];
|
type Repository = (typeof repositories)[0]['useClass'];
|
||||||
|
type Provider = { provide: any; useClass: Repository };
|
||||||
type SqlGeneratorOptions = { targetDir: string };
|
type SqlGeneratorOptions = { targetDir: string };
|
||||||
|
|
||||||
class SqlGenerator {
|
class SqlGenerator {
|
||||||
@ -84,8 +57,8 @@ class SqlGenerator {
|
|||||||
async run() {
|
async run() {
|
||||||
try {
|
try {
|
||||||
await this.setup();
|
await this.setup();
|
||||||
for (const Repository of repositories) {
|
for (const repository of repositories) {
|
||||||
await this.process(Repository);
|
await this.process(repository);
|
||||||
}
|
}
|
||||||
await this.write();
|
await this.write();
|
||||||
this.stats();
|
this.stats();
|
||||||
@ -102,25 +75,27 @@ class SqlGenerator {
|
|||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forRoot({
|
TypeOrmModule.forRoot({
|
||||||
...databaseConfig,
|
...databaseConfig,
|
||||||
entities: databaseEntities,
|
entities,
|
||||||
logging: ['query'],
|
logging: ['query'],
|
||||||
logger: this.sqlLogger,
|
logger: this.sqlLogger,
|
||||||
}),
|
}),
|
||||||
TypeOrmModule.forFeature(databaseEntities),
|
TypeOrmModule.forFeature(entities),
|
||||||
|
EventEmitterModule.forRoot(),
|
||||||
|
OpenTelemetryModule.forRoot(otelConfig),
|
||||||
],
|
],
|
||||||
providers: [{ provide: ISystemConfigRepository, useClass: SystemConfigRepository }, ...repositories],
|
providers: [...repositories, AuthService, SchedulerRegistry],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
this.app = await moduleFixture.createNestApplication().init();
|
this.app = await moduleFixture.createNestApplication().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
async process(Repository: Repository) {
|
async process({ provide: token, useClass: Repository }: Provider) {
|
||||||
if (!this.app) {
|
if (!this.app) {
|
||||||
throw new Error('Not initialized');
|
throw new Error('Not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: string[] = [`-- NOTE: This file is auto generated by ./sql-generator`];
|
const data: string[] = [`-- NOTE: This file is auto generated by ./sql-generator`];
|
||||||
const instance = this.app.get<Repository>(Repository);
|
const instance = this.app.get<Repository>(token);
|
||||||
|
|
||||||
// normal repositories
|
// normal repositories
|
||||||
data.push(...(await this.runTargets(instance, `${Repository.name}`)));
|
data.push(...(await this.runTargets(instance, `${Repository.name}`)));
|
||||||
@ -180,6 +155,10 @@ class SqlGenerator {
|
|||||||
|
|
||||||
private async write() {
|
private async write() {
|
||||||
for (const [repoName, data] of Object.entries(this.results)) {
|
for (const [repoName, data] of Object.entries(this.results)) {
|
||||||
|
// only contains the header
|
||||||
|
if (data.length === 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const filename = repoName.replaceAll(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', '');
|
const filename = repoName.replaceAll(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', '');
|
||||||
const file = join(this.options.targetDir, `${filename}.sql`);
|
const file = join(this.options.targetDir, `${filename}.sql`);
|
||||||
await writeFile(file, data.join('\n\n') + '\n');
|
await writeFile(file, data.join('\n\n') + '\n');
|
||||||
|
78
server/test/fixtures/asset.stub.ts
vendored
78
server/test/fixtures/asset.stub.ts
vendored
@ -639,4 +639,82 @@ export const assetStub = {
|
|||||||
} as ExifEntity,
|
} as ExifEntity,
|
||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
}),
|
}),
|
||||||
|
missingFileExtension: Object.freeze<AssetEntity>({
|
||||||
|
id: 'asset-id',
|
||||||
|
deviceAssetId: 'device-asset-id',
|
||||||
|
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
owner: userStub.user1,
|
||||||
|
ownerId: 'user-id',
|
||||||
|
deviceId: 'device-id',
|
||||||
|
originalPath: '/data/user1/photo.jpg',
|
||||||
|
resizePath: '/uploads/user-id/thumbs/path.jpg',
|
||||||
|
checksum: Buffer.from('file hash', 'utf8'),
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
webpPath: '/uploads/user-id/webp/path.ext',
|
||||||
|
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||||
|
encodedVideoPath: null,
|
||||||
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
isFavorite: true,
|
||||||
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isExternal: true,
|
||||||
|
duration: null,
|
||||||
|
isVisible: true,
|
||||||
|
livePhotoVideo: null,
|
||||||
|
livePhotoVideoId: null,
|
||||||
|
isOffline: false,
|
||||||
|
libraryId: 'library-id',
|
||||||
|
library: libraryStub.externalLibrary1,
|
||||||
|
tags: [],
|
||||||
|
sharedLinks: [],
|
||||||
|
originalFileName: 'photo',
|
||||||
|
faces: [],
|
||||||
|
deletedAt: null,
|
||||||
|
sidecarPath: null,
|
||||||
|
exifInfo: {
|
||||||
|
fileSizeInByte: 5000,
|
||||||
|
} as ExifEntity,
|
||||||
|
}),
|
||||||
|
hasFileExtension: Object.freeze<AssetEntity>({
|
||||||
|
id: 'asset-id',
|
||||||
|
deviceAssetId: 'device-asset-id',
|
||||||
|
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
owner: userStub.user1,
|
||||||
|
ownerId: 'user-id',
|
||||||
|
deviceId: 'device-id',
|
||||||
|
originalPath: '/data/user1/photo.jpg',
|
||||||
|
resizePath: '/uploads/user-id/thumbs/path.jpg',
|
||||||
|
checksum: Buffer.from('file hash', 'utf8'),
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
webpPath: '/uploads/user-id/webp/path.ext',
|
||||||
|
thumbhash: Buffer.from('blablabla', 'base64'),
|
||||||
|
encodedVideoPath: null,
|
||||||
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
|
isFavorite: true,
|
||||||
|
isArchived: false,
|
||||||
|
isReadOnly: false,
|
||||||
|
isExternal: true,
|
||||||
|
duration: null,
|
||||||
|
isVisible: true,
|
||||||
|
livePhotoVideo: null,
|
||||||
|
livePhotoVideoId: null,
|
||||||
|
isOffline: false,
|
||||||
|
libraryId: 'library-id',
|
||||||
|
library: libraryStub.externalLibrary1,
|
||||||
|
tags: [],
|
||||||
|
sharedLinks: [],
|
||||||
|
originalFileName: 'photo.jpg',
|
||||||
|
faces: [],
|
||||||
|
deletedAt: null,
|
||||||
|
sidecarPath: null,
|
||||||
|
exifInfo: {
|
||||||
|
fileSizeInByte: 5000,
|
||||||
|
} as ExifEntity,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
@ -14,9 +14,9 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
|
|||||||
softDeleteAll: jest.fn(),
|
softDeleteAll: jest.fn(),
|
||||||
deleteAll: jest.fn(),
|
deleteAll: jest.fn(),
|
||||||
getAll: jest.fn(),
|
getAll: jest.fn(),
|
||||||
addAssets: jest.fn(),
|
addAssetIds: jest.fn(),
|
||||||
removeAsset: jest.fn(),
|
removeAsset: jest.fn(),
|
||||||
removeAssets: jest.fn(),
|
removeAssetIds: jest.fn(),
|
||||||
getAssetIds: jest.fn(),
|
getAssetIds: jest.fn(),
|
||||||
hasAsset: jest.fn(),
|
hasAsset: jest.fn(),
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
|
16
web/package-lock.json
generated
16
web/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
@ -63,14 +63,14 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.11.30",
|
"@types/node": "^20.11.0",
|
||||||
"typescript": "^5.4.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aashutoshrathi/word-wrap": {
|
"node_modules/@aashutoshrathi/word-wrap": {
|
||||||
@ -8671,9 +8671,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.3.tgz",
|
||||||
"integrity": "sha512-FWZbz0oSdLq5snUI0b6sULbz58iXFXdvkZfZWR/F0ZJuKTSPO7v72QPXt6KqYeMFb0yytNp6kZosxJ96Nr/wDQ==",
|
"integrity": "sha512-+i1oagbvkVIhEy9TnEV+fgXsng13nZM90JQbrcPrf6DvW2mXARlz+DK7DLiDP+qeKoD1FCVx/1SpFL1CLq9Mhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.20.1",
|
"esbuild": "^0.20.1",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.99.0",
|
"version": "1.100.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">Options</h1>
|
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">Options</h1>
|
||||||
<div>
|
<div>
|
||||||
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
|
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -97,6 +97,7 @@
|
|||||||
{#if isOwned}
|
{#if isOwned}
|
||||||
<div>
|
<div>
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
|
title="Options"
|
||||||
on:click={(event) => showContextMenu(event, user)}
|
on:click={(event) => showContextMenu(event, user)}
|
||||||
icon={mdiDotsVertical}
|
icon={mdiDotsVertical}
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
|
@ -305,7 +305,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else if message}
|
{:else if message}
|
||||||
<div class="flex items-end w-fit ml-0">
|
<div class="flex items-end w-fit ml-0">
|
||||||
<CircleIconButton size="15" icon={mdiSend} iconColor={'dark'} hoverColor={'rgb(173,203,250)'} />
|
<CircleIconButton
|
||||||
|
title="Send message"
|
||||||
|
size="15"
|
||||||
|
icon={mdiSend}
|
||||||
|
iconColor={'dark'}
|
||||||
|
hoverColor={'rgb(173,203,250)'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</form>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
import { ThumbnailFormat, type AlbumResponseDto } from '@immich/sdk';
|
import { ThumbnailFormat, type AlbumResponseDto } from '@immich/sdk';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { normalizeSearchString } from '$lib/utils/string-utils.js';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
album: void;
|
album: void;
|
||||||
@ -16,7 +17,7 @@
|
|||||||
// It is used to highlight the search query in the album name
|
// It is used to highlight the search query in the album name
|
||||||
$: {
|
$: {
|
||||||
let { albumName } = album;
|
let { albumName } = album;
|
||||||
let findIndex = albumName.toLowerCase().indexOf(searchQuery.toLowerCase());
|
let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery));
|
||||||
let findLength = searchQuery.length;
|
let findLength = searchQuery.length;
|
||||||
albumNameArray = [
|
albumNameArray = [
|
||||||
albumName.slice(0, findIndex),
|
albumName.slice(0, findIndex),
|
||||||
|
@ -102,7 +102,7 @@
|
|||||||
class="z-[1001] flex h-16 place-items-center justify-between bg-gradient-to-b from-black/40 px-3 transition-transform duration-200"
|
class="z-[1001] flex h-16 place-items-center justify-between bg-gradient-to-b from-black/40 px-3 transition-transform duration-200"
|
||||||
>
|
>
|
||||||
<div class="text-white">
|
<div class="text-white">
|
||||||
<CircleIconButton isOpacity={true} icon={mdiArrowLeft} on:click={() => dispatch('back')} />
|
<CircleIconButton isOpacity={true} icon={mdiArrowLeft} title="Go back" on:click={() => dispatch('back')} />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white">
|
<div class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white">
|
||||||
{#if showShareButton}
|
{#if showShareButton}
|
||||||
@ -182,7 +182,12 @@
|
|||||||
{#if !asset.isReadOnly || !asset.isExternal}
|
{#if !asset.isReadOnly || !asset.isExternal}
|
||||||
<CircleIconButton isOpacity={true} icon={mdiDeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
|
<CircleIconButton isOpacity={true} icon={mdiDeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
|
||||||
{/if}
|
{/if}
|
||||||
<div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}>
|
<div
|
||||||
|
use:clickOutside={{
|
||||||
|
onOutclick: () => (isShowAssetOptions = false),
|
||||||
|
onEscape: () => (isShowAssetOptions = false),
|
||||||
|
}}
|
||||||
|
>
|
||||||
<CircleIconButton isOpacity={true} icon={mdiDotsVertical} on:click={showOptionsMenu} title="More" />
|
<CircleIconButton isOpacity={true} icon={mdiDotsVertical} on:click={showOptionsMenu} title="More" />
|
||||||
{#if isShowAssetOptions}
|
{#if isShowAssetOptions}
|
||||||
<ContextMenu {...contextMenuPosition} direction="left">
|
<ContextMenu {...contextMenuPosition} direction="left">
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { AppRoute, AssetAction, ProjectionType } from '$lib/constants';
|
import { AssetAction, ProjectionType } from '$lib/constants';
|
||||||
import { updateNumberOfComments } from '$lib/stores/activity.store';
|
import { updateNumberOfComments } from '$lib/stores/activity.store';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import type { AssetStore } from '$lib/stores/assets.store';
|
import type { AssetStore } from '$lib/stores/assets.store';
|
||||||
@ -11,7 +10,7 @@
|
|||||||
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
|
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils';
|
import { getAssetJobMessage, isSharedLink, handlePromiseError } from '$lib/utils';
|
||||||
import { addAssetsToAlbum, downloadFile } from '$lib/utils/asset-utils';
|
import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile } from '$lib/utils/asset-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { shortcuts } from '$lib/utils/shortcut';
|
import { shortcuts } from '$lib/utils/shortcut';
|
||||||
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
||||||
@ -20,7 +19,6 @@
|
|||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
ReactionType,
|
ReactionType,
|
||||||
createActivity,
|
createActivity,
|
||||||
createAlbum,
|
|
||||||
deleteActivity,
|
deleteActivity,
|
||||||
deleteAssets,
|
deleteAssets,
|
||||||
getActivities,
|
getActivities,
|
||||||
@ -52,6 +50,7 @@
|
|||||||
import SlideshowBar from './slideshow-bar.svelte';
|
import SlideshowBar from './slideshow-bar.svelte';
|
||||||
import VideoViewer from './video-viewer.svelte';
|
import VideoViewer from './video-viewer.svelte';
|
||||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||||
|
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
|
||||||
|
|
||||||
export let assetStore: AssetStore | null = null;
|
export let assetStore: AssetStore | null = null;
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
@ -287,7 +286,15 @@
|
|||||||
$restartSlideshowProgress = true;
|
$restartSlideshowProgress = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateAsset = async (order: 'previous' | 'next', e?: Event) => {
|
const navigateAsset = async (order?: 'previous' | 'next', e?: Event) => {
|
||||||
|
if (!order) {
|
||||||
|
if ($slideshowState === SlideshowState.PlaySlideshow) {
|
||||||
|
order = $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) {
|
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) {
|
||||||
return (order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next()) || navigateAssetRandom();
|
return (order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next()) || navigateAssetRandom();
|
||||||
}
|
}
|
||||||
@ -390,8 +397,7 @@
|
|||||||
const handleAddToNewAlbum = async (albumName: string) => {
|
const handleAddToNewAlbum = async (albumName: string) => {
|
||||||
isShowAlbumPicker = false;
|
isShowAlbumPicker = false;
|
||||||
|
|
||||||
const album = await createAlbum({ createAlbumDto: { albumName, assetIds: [asset.id] } });
|
await addAssetsToNewAlbum(albumName, [asset.id]);
|
||||||
await goto(`${AppRoute.ALBUMS}/${album.id}`);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToAlbum = async (album: AlbumResponseDto) => {
|
const handleAddToAlbum = async (album: AlbumResponseDto) => {
|
||||||
@ -448,12 +454,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVideoEnded = async () => {
|
|
||||||
if ($slideshowState === SlideshowState.PlaySlideshow) {
|
|
||||||
await navigateAsset('next');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePlaySlideshow = async () => {
|
const handlePlaySlideshow = async () => {
|
||||||
try {
|
try {
|
||||||
await assetViewerHtmlElement.requestFullscreen();
|
await assetViewerHtmlElement.requestFullscreen();
|
||||||
@ -466,6 +466,7 @@
|
|||||||
const handleStopSlideshow = async () => {
|
const handleStopSlideshow = async () => {
|
||||||
try {
|
try {
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
|
document.body.style.cursor = '';
|
||||||
await document.exitFullscreen();
|
await document.exitFullscreen();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -517,6 +518,7 @@
|
|||||||
|
|
||||||
<svelte:document bind:fullscreenElement />
|
<svelte:document bind:fullscreenElement />
|
||||||
|
|
||||||
|
<FocusTrap>
|
||||||
<section
|
<section
|
||||||
id="immich-asset-viewer"
|
id="immich-asset-viewer"
|
||||||
class="fixed left-0 top-0 z-[1001] grid h-screen w-screen grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
|
class="fixed left-0 top-0 z-[1001] grid h-screen w-screen grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
|
||||||
@ -579,12 +581,17 @@
|
|||||||
{#if previewStackedAsset}
|
{#if previewStackedAsset}
|
||||||
{#key previewStackedAsset.id}
|
{#key previewStackedAsset.id}
|
||||||
{#if previewStackedAsset.type === AssetTypeEnum.Image}
|
{#if previewStackedAsset.type === AssetTypeEnum.Image}
|
||||||
<PhotoViewer asset={previewStackedAsset} {preloadAssets} on:close={closeViewer} haveFadeTransition={false} />
|
<PhotoViewer
|
||||||
|
asset={previewStackedAsset}
|
||||||
|
{preloadAssets}
|
||||||
|
on:close={closeViewer}
|
||||||
|
haveFadeTransition={false}
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={previewStackedAsset.id}
|
assetId={previewStackedAsset.id}
|
||||||
on:close={closeViewer}
|
on:close={closeViewer}
|
||||||
on:onVideoEnded={handleVideoEnded}
|
on:onVideoEnded={() => navigateAsset()}
|
||||||
on:onVideoStarted={handleVideoStarted}
|
on:onVideoStarted={handleVideoStarted}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@ -617,7 +624,7 @@
|
|||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={asset.id}
|
assetId={asset.id}
|
||||||
on:close={closeViewer}
|
on:close={closeViewer}
|
||||||
on:onVideoEnded={handleVideoEnded}
|
on:onVideoEnded={() => navigateAsset()}
|
||||||
on:onVideoStarted={handleVideoStarted}
|
on:onVideoStarted={handleVideoStarted}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
@ -730,6 +737,7 @@
|
|||||||
on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)}
|
on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)}
|
||||||
on:album={({ detail }) => handleAddToAlbum(detail)}
|
on:album={({ detail }) => handleAddToAlbum(detail)}
|
||||||
on:close={() => (isShowAlbumPicker = false)}
|
on:close={() => (isShowAlbumPicker = false)}
|
||||||
|
on:escape={() => (isShowAlbumPicker = false)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -743,13 +751,22 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isShowProfileImageCrop}
|
{#if isShowProfileImageCrop}
|
||||||
<ProfileImageCropper {asset} on:close={() => (isShowProfileImageCrop = false)} />
|
<ProfileImageCropper
|
||||||
|
{asset}
|
||||||
|
on:close={() => (isShowProfileImageCrop = false)}
|
||||||
|
on:escape={() => (isShowProfileImageCrop = false)}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isShowShareModal}
|
{#if isShowShareModal}
|
||||||
<CreateSharedLinkModal assetIds={[asset.id]} on:close={() => (isShowShareModal = false)} />
|
<CreateSharedLinkModal
|
||||||
|
assetIds={[asset.id]}
|
||||||
|
on:close={() => (isShowShareModal = false)}
|
||||||
|
on:escape={() => (isShowShareModal = false)}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
</FocusTrap>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
#immich-asset-viewer {
|
#immich-asset-viewer {
|
||||||
|
@ -39,7 +39,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute right-2">
|
<div class="absolute right-2">
|
||||||
<CircleIconButton on:click={() => abort(downloadKey, download)} size="20" icon={mdiClose} forceDark />
|
<CircleIconButton
|
||||||
|
title="Close"
|
||||||
|
on:click={() => abort(downloadKey, download)}
|
||||||
|
size="20"
|
||||||
|
icon={mdiClose}
|
||||||
|
forceDark
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
|
||||||
export let icon: string;
|
export let icon: string;
|
||||||
|
export let title: string;
|
||||||
export let backgroundColor = '';
|
export let backgroundColor = '';
|
||||||
export let hoverColor = '#e2e7e9';
|
export let hoverColor = '#e2e7e9';
|
||||||
export let padding = '3';
|
export let padding = '3';
|
||||||
export let size = '24';
|
export let size = '24';
|
||||||
export let title = '';
|
|
||||||
export let isOpacity = false;
|
export let isOpacity = false;
|
||||||
export let forceDark = false;
|
export let forceDark = false;
|
||||||
export let hideMobile = false;
|
export let hideMobile = false;
|
||||||
@ -27,7 +27,7 @@
|
|||||||
{hideMobile && 'hidden sm:flex'}"
|
{hideMobile && 'hidden sm:flex'}"
|
||||||
on:click
|
on:click
|
||||||
>
|
>
|
||||||
<Icon path={icon} {size} color={iconColor} />
|
<Icon path={icon} {size} ariaLabel={title} color={iconColor} />
|
||||||
<slot />
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
? 'rounded-2xl'
|
? 'rounded-2xl'
|
||||||
: 'rounded-t-lg'} bg-gray-200 p-2 dark:bg-immich-dark-gray gap-2 place-items-center h-full"
|
: 'rounded-t-lg'} bg-gray-200 p-2 dark:bg-immich-dark-gray gap-2 place-items-center h-full"
|
||||||
>
|
>
|
||||||
<button on:click={() => dispatch('search', { force: true })}>
|
<button type="button" on:click={() => dispatch('search', { force: true })}>
|
||||||
<div class="w-fit">
|
<div class="w-fit">
|
||||||
<Icon path={mdiMagnify} size="24" />
|
<Icon path={mdiMagnify} size="24" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -135,7 +135,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if selectedPeople.length === 1}
|
{#if selectedPeople.length === 1}
|
||||||
<div class="absolute bottom-2">
|
<div class="absolute bottom-2">
|
||||||
<CircleIconButton icon={mdiSwapHorizontal} size="24" on:click={handleSwapPeople} />
|
<CircleIconButton
|
||||||
|
title="Swap merge direction"
|
||||||
|
icon={mdiSwapHorizontal}
|
||||||
|
size="24"
|
||||||
|
on:click={handleSwapPeople}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
Merge People - {title}
|
Merge People - {title}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
|
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -57,6 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mx-0.5 flex md:mx-2">
|
<div class="mx-0.5 flex md:mx-2">
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
|
title="Swap merge direction"
|
||||||
icon={mdiMerge}
|
icon={mdiMerge}
|
||||||
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
|
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
|
||||||
/>
|
/>
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
|
class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
|
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<p class="ml-2">Show & hide people</p>
|
<p class="ml-2">Show & hide people</p>
|
||||||
<p class="text-sm text-gray-400 dark:text-gray-600">({countTotalPeople.toLocaleString($locale)})</p>
|
<p class="text-sm text-gray-400 dark:text-gray-600">({countTotalPeople.toLocaleString($locale)})</p>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Button from '../elements/buttons/button.svelte';
|
import Button from '../elements/buttons/button.svelte';
|
||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
|
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
export let canResetPassword = true;
|
export let canResetPassword = true;
|
||||||
@ -90,11 +91,12 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<FocusTrap>
|
||||||
<div
|
<div
|
||||||
class="relative max-h-screen w-[500px] max-w-[95vw] overflow-y-auto rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
class="relative max-h-screen w-[500px] max-w-[95vw] overflow-y-auto rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 right-0 px-2 py-2 h-fit">
|
<div class="absolute top-0 right-0 px-2 py-2 h-fit">
|
||||||
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
|
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -160,6 +162,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</FocusTrap>
|
||||||
|
|
||||||
{#if isShowResetPasswordConfirmation}
|
{#if isShowResetPasswordConfirmation}
|
||||||
<ConfirmDialogue
|
<ConfirmDialogue
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import type { Viewport } from '$lib/stores/assets.store';
|
import type { Viewport } from '$lib/stores/assets.store';
|
||||||
import { memoryStore } from '$lib/stores/memory.store';
|
import { memoryStore } from '$lib/stores/memory.store';
|
||||||
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
|
||||||
import { shortcuts } from '$lib/utils/shortcut';
|
import { shortcuts } from '$lib/utils/shortcut';
|
||||||
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
||||||
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
|
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
|
||||||
@ -102,13 +102,18 @@
|
|||||||
<ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark>
|
<ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark>
|
||||||
<svelte:fragment slot="leading">
|
<svelte:fragment slot="leading">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
{currentMemory.title}
|
{memoryLaneTitle(currentMemory.yearsAgo)}
|
||||||
</p>
|
</p>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
{#if canGoForward}
|
{#if canGoForward}
|
||||||
<div class="flex place-content-center place-items-center gap-2 overflow-hidden">
|
<div class="flex place-content-center place-items-center gap-2 overflow-hidden">
|
||||||
<CircleIconButton icon={paused ? mdiPlay : mdiPause} forceDark on:click={() => (paused = !paused)} />
|
<CircleIconButton
|
||||||
|
title={paused ? 'Play memories' : 'Pause memories'}
|
||||||
|
icon={paused ? mdiPlay : mdiPause}
|
||||||
|
forceDark
|
||||||
|
on:click={() => (paused = !paused)}
|
||||||
|
/>
|
||||||
|
|
||||||
{#each currentMemory.assets as _, index}
|
{#each currentMemory.assets as _, index}
|
||||||
<button
|
<button
|
||||||
@ -144,7 +149,7 @@
|
|||||||
class:opacity-100={galleryInView}
|
class:opacity-100={galleryInView}
|
||||||
>
|
>
|
||||||
<button on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })} disabled={!galleryInView}>
|
<button on:click={() => memoryWrapper.scrollIntoView({ behavior: 'smooth' })} disabled={!galleryInView}>
|
||||||
<CircleIconButton icon={mdiChevronUp} backgroundColor="white" forceDark />
|
<CircleIconButton title="Hide gallery" icon={mdiChevronUp} backgroundColor="white" forceDark />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -181,7 +186,7 @@
|
|||||||
{#if previousMemory}
|
{#if previousMemory}
|
||||||
<div class="absolute bottom-4 right-4 text-left text-white">
|
<div class="absolute bottom-4 right-4 text-left text-white">
|
||||||
<p class="text-xs font-semibold text-gray-200">PREVIOUS</p>
|
<p class="text-xs font-semibold text-gray-200">PREVIOUS</p>
|
||||||
<p class="text-xl">{previousMemory.title}</p>
|
<p class="text-xl">{memoryLaneTitle(previousMemory.yearsAgo)}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
@ -204,13 +209,23 @@
|
|||||||
<!-- CONTROL BUTTONS -->
|
<!-- CONTROL BUTTONS -->
|
||||||
{#if canGoBack}
|
{#if canGoBack}
|
||||||
<div class="absolute top-1/2 left-0 ml-4">
|
<div class="absolute top-1/2 left-0 ml-4">
|
||||||
<CircleIconButton icon={mdiChevronLeft} backgroundColor="#202123" on:click={toPrevious} />
|
<CircleIconButton
|
||||||
|
title="Previous memory"
|
||||||
|
icon={mdiChevronLeft}
|
||||||
|
backgroundColor="#202123"
|
||||||
|
on:click={toPrevious}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if canGoForward}
|
{#if canGoForward}
|
||||||
<div class="absolute top-1/2 right-0 mr-4">
|
<div class="absolute top-1/2 right-0 mr-4">
|
||||||
<CircleIconButton icon={mdiChevronRight} backgroundColor="#202123" on:click={toNext} />
|
<CircleIconButton
|
||||||
|
title="Next memory"
|
||||||
|
icon={mdiChevronRight}
|
||||||
|
backgroundColor="#202123"
|
||||||
|
on:click={toNext}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -254,7 +269,7 @@
|
|||||||
{#if nextMemory}
|
{#if nextMemory}
|
||||||
<div class="absolute bottom-4 left-4 text-left text-white">
|
<div class="absolute bottom-4 left-4 text-left text-white">
|
||||||
<p class="text-xs font-semibold text-gray-200">UP NEXT</p>
|
<p class="text-xs font-semibold text-gray-200">UP NEXT</p>
|
||||||
<p class="text-xl">{nextMemory.title}</p>
|
<p class="text-xl">{memoryLaneTitle(nextMemory.yearsAgo)}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
@ -271,7 +286,7 @@
|
|||||||
class:opacity-100={!galleryInView}
|
class:opacity-100={!galleryInView}
|
||||||
>
|
>
|
||||||
<button on:click={() => memoryGallery.scrollIntoView({ behavior: 'smooth' })}>
|
<button on:click={() => memoryGallery.scrollIntoView({ behavior: 'smooth' })}>
|
||||||
<CircleIconButton icon={mdiChevronDown} backgroundColor="white" forceDark />
|
<CircleIconButton title="Show gallery" icon={mdiChevronDown} backgroundColor="white" forceDark />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,19 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
|
import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import {
|
import { addAssetsToAlbum, addAssetsToNewAlbum } from '$lib/utils/asset-utils';
|
||||||
NotificationType,
|
import type { AlbumResponseDto } from '@immich/sdk';
|
||||||
notificationController,
|
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
|
||||||
import { AppRoute } from '$lib/constants';
|
|
||||||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
|
||||||
import { createAlbum, type AlbumResponseDto } from '@immich/sdk';
|
|
||||||
import { getMenuContext } from '../asset-select-context-menu.svelte';
|
import { getMenuContext } from '../asset-select-context-menu.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
|
import { mdiImageAlbum, mdiShareVariantOutline } from '@mdi/js';
|
||||||
|
|
||||||
export let shared = false;
|
export let shared = false;
|
||||||
|
|
||||||
let showAlbumPicker = false;
|
let showAlbumPicker = false;
|
||||||
|
|
||||||
const { getAssets, clearSelect } = getAssetControlContext();
|
const { getAssets, clearSelect } = getAssetControlContext();
|
||||||
@ -24,26 +19,12 @@
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToNewAlbum = (albumName: string) => {
|
const handleAddToNewAlbum = async (albumName: string) => {
|
||||||
showAlbumPicker = false;
|
showAlbumPicker = false;
|
||||||
|
closeMenu();
|
||||||
|
|
||||||
const assetIds = [...getAssets()].map((asset) => asset.id);
|
const assetIds = [...getAssets()].map((asset) => asset.id);
|
||||||
createAlbum({ createAlbumDto: { albumName, assetIds } })
|
await addAssetsToNewAlbum(albumName, assetIds);
|
||||||
.then(async (response) => {
|
|
||||||
const { id, albumName } = response;
|
|
||||||
|
|
||||||
notificationController.show({
|
|
||||||
message: `Added ${assetIds.length} to ${albumName}`,
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
|
|
||||||
clearSelect();
|
|
||||||
|
|
||||||
await goto(`${AppRoute.ALBUMS}/${id}`);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(`[add-to-album.svelte]:handleAddToNewAlbum ${error}`, error);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddToAlbum = async (album: AlbumResponseDto) => {
|
const handleAddToAlbum = async (album: AlbumResponseDto) => {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import { memoryStore } from '$lib/stores/memory.store';
|
import { memoryStore } from '$lib/stores/memory.store';
|
||||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
import { getAssetThumbnailUrl, memoryLaneTitle } from '$lib/utils';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
|
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
|
||||||
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
|
||||||
@ -66,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="inline-block" bind:offsetWidth={innerWidth}>
|
<div class="inline-block" bind:offsetWidth={innerWidth}>
|
||||||
{#each $memoryStore as memory, index (memory.title)}
|
{#each $memoryStore as memory, index (memory.yearsAgo)}
|
||||||
<button
|
<button
|
||||||
class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl"
|
class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl"
|
||||||
on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${index}`)}
|
on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${index}`)}
|
||||||
@ -77,7 +77,9 @@
|
|||||||
alt={`Memory Lane ${getAltText(memory.assets[0])}`}
|
alt={`Memory Lane ${getAltText(memory.assets[0])}`}
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
<p class="absolute bottom-2 left-4 z-10 text-lg text-white">{memory.title}</p>
|
<p class="absolute bottom-2 left-4 z-10 text-lg text-white">
|
||||||
|
{memoryLaneTitle(memory.yearsAgo)}
|
||||||
|
</p>
|
||||||
<div
|
<div
|
||||||
class="absolute left-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
|
class="absolute left-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
|
||||||
/>
|
/>
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
import { createEventDispatcher, onMount } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import AlbumListItem from '../asset-viewer/album-list-item.svelte';
|
import AlbumListItem from '../asset-viewer/album-list-item.svelte';
|
||||||
import BaseModal from './base-modal.svelte';
|
import BaseModal from './base-modal.svelte';
|
||||||
|
import { normalizeSearchString } from '$lib/utils/string-utils';
|
||||||
|
|
||||||
let albums: AlbumResponseDto[] = [];
|
let albums: AlbumResponseDto[] = [];
|
||||||
let recentAlbums: AlbumResponseDto[] = [];
|
let recentAlbums: AlbumResponseDto[] = [];
|
||||||
@ -30,7 +31,7 @@
|
|||||||
filteredAlbums =
|
filteredAlbums =
|
||||||
search.length > 0 && albums.length > 0
|
search.length > 0 && albums.length > 0
|
||||||
? albums.filter((album) => {
|
? albums.filter((album) => {
|
||||||
return album.albumName.toLowerCase().includes(search.toLowerCase());
|
return normalizeSearchString(album.albumName).includes(normalizeSearchString(search));
|
||||||
})
|
})
|
||||||
: albums;
|
: albums;
|
||||||
}
|
}
|
||||||
@ -44,7 +45,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<BaseModal on:close={() => dispatch('close')}>
|
<BaseModal on:close on:escape>
|
||||||
<svelte:fragment slot="title">
|
<svelte:fragment slot="title">
|
||||||
<span class="flex place-items-center gap-2">
|
<span class="flex place-items-center gap-2">
|
||||||
<p class="font-medium">
|
<p class="font-medium">
|
||||||
@ -84,7 +85,7 @@
|
|||||||
<Icon path={mdiPlus} size="30" />
|
<Icon path={mdiPlus} size="30" />
|
||||||
</div>
|
</div>
|
||||||
<p class="">
|
<p class="">
|
||||||
New {shared ? 'Shared ' : ''}Album {#if search.length > 0}<b>{search}</b>{/if}
|
New Album {#if search.length > 0}<b>{search}</b>{/if}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
{#if filteredAlbums.length > 0}
|
{#if filteredAlbums.length > 0}
|
||||||
|
@ -6,13 +6,13 @@
|
|||||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
import { clickOutside } from '$lib/utils/click-outside';
|
import { clickOutside } from '$lib/utils/click-outside';
|
||||||
import { mdiClose } from '@mdi/js';
|
import { mdiClose } from '@mdi/js';
|
||||||
|
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
escape: void;
|
escape: void;
|
||||||
close: void;
|
close: void;
|
||||||
}>();
|
}>();
|
||||||
export let zIndex = 9999;
|
export let zIndex = 9999;
|
||||||
export let ignoreClickOutside = false;
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (browser) {
|
if (browser) {
|
||||||
@ -34,6 +34,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<FocusTrap>
|
||||||
<div
|
<div
|
||||||
id="immich-modal"
|
id="immich-modal"
|
||||||
style:z-index={zIndex}
|
style:z-index={zIndex}
|
||||||
@ -41,10 +42,12 @@
|
|||||||
class="fixed left-0 top-0 flex h-full w-full place-content-center place-items-center overflow-hidden bg-black/50"
|
class="fixed left-0 top-0 flex h-full w-full place-content-center place-items-center overflow-hidden bg-black/50"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
use:clickOutside
|
use:clickOutside={{
|
||||||
on:outclick={() => !ignoreClickOutside && dispatch('close')}
|
onOutclick: () => dispatch('close'),
|
||||||
on:escape={() => dispatch('escape')}
|
onEscape: () => dispatch('escape'),
|
||||||
|
}}
|
||||||
class="max-h-[800px] min-h-[200px] w-[450px] overflow-y-auto rounded-lg bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
|
class="max-h-[800px] min-h-[200px] w-[450px] overflow-y-auto rounded-lg bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div class="flex place-items-center justify-between px-5 py-3">
|
<div class="flex place-items-center justify-between px-5 py-3">
|
||||||
<div>
|
<div>
|
||||||
@ -53,7 +56,7 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} size={'20'} />
|
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} size={'20'} title="Close" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -67,3 +70,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</FocusTrap>
|
||||||
|
@ -185,7 +185,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
shortcut: { key: 'Escape' },
|
shortcut: { key: 'Escape' },
|
||||||
onShortcut: () => {
|
onShortcut: (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
closeDropdown();
|
closeDropdown();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
{#if text}
|
{#if text}
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<p class="flex gap-2">
|
<p class="flex gap-2">
|
||||||
<Icon path={icon} size="18" />
|
<Icon path={icon} ariaHidden={true} size="18" />
|
||||||
{text}
|
{text}
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user