1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

Add asset repository and refactor asset service (#540)

* build endpoint to get asset count by month

* Added asset repository

* Added create asset

* get asset by device ID

* Added test for existing methods

* Refactor additional endpoint

* Refactor database api to get curated locations and curated objects

* Refactor get search properties

* Fixed cookies parsing for websocket

* Added API to get asset count by time group

* Remove unused code
This commit is contained in:
Alex 2022-08-26 22:53:37 -07:00 committed by GitHub
parent 6b7c97c02a
commit f980a2f27a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 587 additions and 126 deletions

View File

@ -8,6 +8,8 @@ doc/AdminSignupResponseDto.md
doc/AlbumApi.md
doc/AlbumResponseDto.md
doc/AssetApi.md
doc/AssetCountByTimeGroupDto.md
doc/AssetCountByTimeGroupResponseDto.md
doc/AssetFileUploadResponseDto.md
doc/AssetResponseDto.md
doc/AssetTypeEnum.md
@ -27,6 +29,7 @@ doc/DeviceInfoApi.md
doc/DeviceInfoResponseDto.md
doc/DeviceTypeEnum.md
doc/ExifResponseDto.md
doc/GetAssetCountByTimeGroupDto.md
doc/LoginCredentialDto.md
doc/LoginResponseDto.md
doc/LogoutResponseDto.md
@ -39,6 +42,7 @@ doc/ServerVersionReponseDto.md
doc/SignUpDto.md
doc/SmartInfoResponseDto.md
doc/ThumbnailFormat.md
doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md
doc/UpdateDeviceInfoDto.md
doc/UpdateUserDto.md
@ -66,6 +70,8 @@ lib/model/add_assets_dto.dart
lib/model/add_users_dto.dart
lib/model/admin_signup_response_dto.dart
lib/model/album_response_dto.dart
lib/model/asset_count_by_time_group_dto.dart
lib/model/asset_count_by_time_group_response_dto.dart
lib/model/asset_file_upload_response_dto.dart
lib/model/asset_response_dto.dart
lib/model/asset_type_enum.dart
@ -83,6 +89,7 @@ lib/model/delete_asset_status.dart
lib/model/device_info_response_dto.dart
lib/model/device_type_enum.dart
lib/model/exif_response_dto.dart
lib/model/get_asset_count_by_time_group_dto.dart
lib/model/login_credential_dto.dart
lib/model/login_response_dto.dart
lib/model/logout_response_dto.dart
@ -94,6 +101,7 @@ lib/model/server_version_reponse_dto.dart
lib/model/sign_up_dto.dart
lib/model/smart_info_response_dto.dart
lib/model/thumbnail_format.dart
lib/model/time_group_enum.dart
lib/model/update_album_dto.dart
lib/model/update_device_info_dto.dart
lib/model/update_user_dto.dart

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,187 @@
import { SearchPropertiesDto } from './dto/search-properties.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm/repository/Repository';
import { CreateAssetDto } from './dto/create-asset.dto';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
import { AssetCountByTimeGroupDto } from './response-dto/asset-count-by-time-group-response.dto';
import { TimeGroupEnum } from './dto/get-asset-count-by-time-group.dto';
export interface IAssetRepository {
create(createAssetDto: CreateAssetDto, ownerId: string, originalPath: string, mimeType: string): Promise<AssetEntity>;
getAllByUserId(userId: string): Promise<AssetEntity[]>;
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
getById(assetId: string): Promise<AssetEntity>;
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
getAssetCountByTimeGroup(userId: string, timeGroup: TimeGroupEnum): Promise<AssetCountByTimeGroupDto[]>;
}
export const ASSET_REPOSITORY = 'ASSET_REPOSITORY';
@Injectable()
export class AssetRepository implements IAssetRepository {
constructor(
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
) {}
async getAssetCountByTimeGroup(userId: string, timeGroup: TimeGroupEnum) {
let result: AssetCountByTimeGroupDto[] = [];
if (timeGroup === TimeGroupEnum.Month) {
result = await this.assetRepository
.createQueryBuilder('asset')
.select(`COUNT(asset.id)::int`, 'count')
.addSelect(`to_char(date_trunc('month', "createdAt"::timestamptz), 'YYYY_MM')`, 'timeGroup')
.where('"userId" = :userId', { userId: userId })
.groupBy(`date_trunc('month', "createdAt"::timestamptz)`)
.orderBy(`date_trunc('month', "createdAt"::timestamptz)`, 'DESC')
.getRawMany();
} else if (timeGroup === TimeGroupEnum.Day) {
result = await this.assetRepository
.createQueryBuilder('asset')
.select(`COUNT(asset.id)::int`, 'count')
.addSelect(`to_char(date_trunc('day', "createdAt"::timestamptz), 'YYYY_MM_DD')`, 'timeGroup')
.where('"userId" = :userId', { userId: userId })
.groupBy(`date_trunc('day', "createdAt"::timestamptz)`)
.orderBy(`date_trunc('day', "createdAt"::timestamptz)`, 'DESC')
.getRawMany();
}
return result;
}
async getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
return await this.assetRepository
.createQueryBuilder('asset')
.where('asset.userId = :userId', { userId: userId })
.leftJoin('asset.exifInfo', 'ei')
.leftJoin('asset.smartInfo', 'si')
.select('si.tags', 'tags')
.addSelect('si.objects', 'objects')
.addSelect('asset.type', 'assetType')
.addSelect('ei.orientation', 'orientation')
.addSelect('ei."lensModel"', 'lensModel')
.addSelect('ei.make', 'make')
.addSelect('ei.model', 'model')
.addSelect('ei.city', 'city')
.addSelect('ei.state', 'state')
.addSelect('ei.country', 'country')
.distinctOn(['si.tags'])
.getRawMany();
}
async getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]> {
return await this.assetRepository.query(
`
SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN smart_info si ON a.id = si."assetId"
WHERE a."userId" = $1
AND si.objects IS NOT NULL
`,
[userId],
);
}
async getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]> {
return await this.assetRepository.query(
`
SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN exif e ON a.id = e."assetId"
WHERE a."userId" = $1
AND e.city IS NOT NULL
AND a.type = 'IMAGE';
`,
[userId],
);
}
/**
* Get a single asset information by its ID
* - include exif info
* @param assetId
*/
async getById(assetId: string): Promise<AssetEntity> {
return await this.assetRepository.findOneOrFail({
where: {
id: assetId,
},
relations: ['exifInfo'],
});
}
/**
* Get all assets belong to the user on the database
* @param userId
*/
async getAllByUserId(userId: string): Promise<AssetEntity[]> {
const query = this.assetRepository
.createQueryBuilder('asset')
.where('asset.userId = :userId', { userId: userId })
.andWhere('asset.resizePath is not NULL')
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
.orderBy('asset.createdAt', 'DESC');
return await query.getMany();
}
/**
* Create new asset information in database
* @param createAssetDto
* @param ownerId
* @param originalPath
* @param mimeType
* @returns Promise<AssetEntity>
*/
async create(
createAssetDto: CreateAssetDto,
ownerId: string,
originalPath: string,
mimeType: string,
): Promise<AssetEntity> {
const asset = new AssetEntity();
asset.deviceAssetId = createAssetDto.deviceAssetId;
asset.userId = ownerId;
asset.deviceId = createAssetDto.deviceId;
asset.type = createAssetDto.assetType || AssetType.OTHER;
asset.originalPath = originalPath;
asset.createdAt = createAssetDto.createdAt;
asset.modifiedAt = createAssetDto.modifiedAt;
asset.isFavorite = createAssetDto.isFavorite;
asset.mimeType = mimeType;
asset.duration = createAssetDto.duration || null;
const createdAsset = await this.assetRepository.save(asset);
if (!createdAsset) {
throw new BadRequestException('Asset not created');
}
return createdAsset;
}
/**
* Get assets by device's Id on the database
* @param userId
* @param deviceId
*
* @returns Promise<string[]> - Array of assetIds belong to the device
*/
async getAllByDeviceId(userId: string, deviceId: string): Promise<string[]> {
const rows = await this.assetRepository.find({
where: {
userId: userId,
deviceId: deviceId,
},
select: ['deviceAssetId'],
});
const res: string[] = [];
rows.forEach((v) => res.push(v.deviceAssetId));
return res;
}
}

View File

@ -2,7 +2,6 @@ import {
Controller,
Post,
UseInterceptors,
UploadedFiles,
Body,
UseGuards,
Get,
@ -44,6 +43,8 @@ import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
import { AssetCountByTimeGroupResponseDto } from './response-dto/asset-count-by-time-group-response.dto';
import { GetAssetCountByTimeGroupDto } from './dto/get-asset-count-by-time-group.dto';
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ -117,17 +118,17 @@ export class AssetController {
return this.assetService.getAssetThumbnail(assetId, query);
}
@Get('/allObjects')
@Get('/curated-objects')
async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
return this.assetService.getCuratedObject(authUser);
}
@Get('/allLocation')
@Get('/curated-locations')
async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
return this.assetService.getCuratedLocation(authUser);
}
@Get('/searchTerm')
@Get('/search-terms')
async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise<string[]> {
return this.assetService.getAssetSearchTerm(authUser);
}
@ -140,6 +141,14 @@ export class AssetController {
return this.assetService.searchAsset(authUser, searchAssetDto);
}
@Get('/count-by-date')
async getAssetCountByTimeGroup(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto,
): Promise<AssetCountByTimeGroupResponseDto> {
return this.assetService.getAssetCountByTimeGroup(authUser, getAssetCountByTimeGroupDto);
}
/**
* Get all AssetEntity belong to the user
*/

View File

@ -8,6 +8,7 @@ import { BackgroundTaskModule } from '../../modules/background-task/background-t
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
import { CommunicationModule } from '../communication/communication.module';
import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
import { AssetRepository, ASSET_REPOSITORY } from './asset-repository';
@Module({
imports: [
@ -24,7 +25,14 @@ import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant';
}),
],
controllers: [AssetController],
providers: [AssetService, BackgroundTaskService],
providers: [
AssetService,
BackgroundTaskService,
{
provide: ASSET_REPOSITORY,
useClass: AssetRepository,
},
],
exports: [AssetService],
})
export class AssetModule {}

View File

@ -0,0 +1,92 @@
import { AssetRepository, IAssetRepository } from './asset-repository';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetService } from './asset.service';
import { Repository } from 'typeorm';
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import { CreateAssetDto } from './dto/create-asset.dto';
describe('AssetService', () => {
let sui: AssetService;
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
const authUser: AuthUserDto = Object.freeze({
id: '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd',
email: 'auth@test.com',
});
const _getCreateAssetDto = (): CreateAssetDto => {
const createAssetDto = new CreateAssetDto();
createAssetDto.deviceAssetId = 'deviceAssetId';
createAssetDto.deviceId = 'deviceId';
createAssetDto.assetType = AssetType.OTHER;
createAssetDto.createdAt = '2022-06-19T23:41:36.910Z';
createAssetDto.modifiedAt = '2022-06-19T23:41:36.910Z';
createAssetDto.isFavorite = false;
createAssetDto.duration = '0:00:00.000000';
return createAssetDto;
};
const _getAsset = () => {
const assetEntity = new AssetEntity();
assetEntity.id = 'e8edabfd-7d8a-45d0-9d61-7c7ca60f2c67';
assetEntity.userId = '3ea54709-e168-42b7-90b0-a0dfe8a7ecbd';
assetEntity.deviceAssetId = '4967046344801';
assetEntity.deviceId = '116766fd-2ef2-52dc-a3ef-149988997291';
assetEntity.type = AssetType.VIDEO;
assetEntity.originalPath =
'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg';
assetEntity.resizePath = '';
assetEntity.createdAt = '2022-06-19T23:41:36.910Z';
assetEntity.modifiedAt = '2022-06-19T23:41:36.910Z';
assetEntity.isFavorite = false;
assetEntity.mimeType = 'image/jpeg';
assetEntity.webpPath = '';
assetEntity.encodedVideoPath = '';
assetEntity.duration = '0:00:00.000000';
return assetEntity;
};
beforeAll(() => {
assetRepositoryMock = {
create: jest.fn(),
getAllByUserId: jest.fn(),
getAllByDeviceId: jest.fn(),
getAssetCountByTimeGroup: jest.fn(),
getById: jest.fn(),
getDetectedObjectsByUserId: jest.fn(),
getLocationsByUserId: jest.fn(),
getSearchPropertiesByUserId: jest.fn(),
};
sui = new AssetService(assetRepositoryMock, a);
});
it('create an asset', async () => {
const assetEntity = _getAsset();
assetRepositoryMock.create.mockImplementation(() => Promise.resolve<AssetEntity>(assetEntity));
const originalPath =
'upload/3ea54709-e168-42b7-90b0-a0dfe8a7ecbd/original/116766fd-2ef2-52dc-a3ef-149988997291/51c97f95-244f-462d-bdf0-e1dc19913516.jpg';
const mimeType = 'image/jpeg';
const createAssetDto = _getCreateAssetDto();
const result = await sui.createUserAsset(authUser, createAssetDto, originalPath, mimeType);
expect(result.userId).toEqual(authUser.id);
expect(result.resizePath).toEqual('');
expect(result.webpPath).toEqual('');
});
it('get assets by device id', async () => {
assetRepositoryMock.getAllByDeviceId.mockImplementation(() => Promise.resolve<string[]>(['4967046344801']));
const deviceId = '116766fd-2ef2-52dc-a3ef-149988997291';
const result = await sui.getUserAssetsByDeviceId(authUser, deviceId);
expect(result.length).toEqual(1);
expect(result[0]).toEqual('4967046344801');
});
});

View File

@ -1,5 +1,7 @@
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import {
BadRequestException,
Inject,
Injectable,
InternalServerErrorException,
Logger,
@ -7,7 +9,7 @@ import {
StreamableFile,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IsNull, Not, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetEntity, AssetType } from '@app/database/entities/asset.entity';
import { constants, createReadStream, ReadStream, stat } from 'fs';
@ -25,83 +27,49 @@ import { CreateAssetDto } from './dto/create-asset.dto';
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
import { ASSET_REPOSITORY, IAssetRepository } from './asset-repository';
import { SearchPropertiesDto } from './dto/search-properties.dto';
import {
AssetCountByTimeGroupResponseDto,
mapAssetCountByTimeGroupResponse,
} from './response-dto/asset-count-by-time-group-response.dto';
import { GetAssetCountByTimeGroupDto } from './dto/get-asset-count-by-time-group.dto';
const fileInfo = promisify(stat);
@Injectable()
export class AssetService {
constructor(
@Inject(ASSET_REPOSITORY)
private _assetRepository: IAssetRepository,
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
) {}
public async updateThumbnailInfo(asset: AssetEntity, thumbnailPath: string): Promise<AssetEntity> {
const updatedAsset = await this.assetRepository
.createQueryBuilder('assets')
.update<AssetEntity>(AssetEntity, { ...asset, resizePath: thumbnailPath })
.where('assets.id = :id', { id: asset.id })
.returning('*')
.updateEntity(true)
.execute();
return updatedAsset.raw[0];
}
public async createUserAsset(
authUser: AuthUserDto,
assetInfo: CreateAssetDto,
path: string,
createAssetDto: CreateAssetDto,
originalPath: string,
mimeType: string,
): Promise<AssetEntity | undefined> {
const asset = new AssetEntity();
asset.deviceAssetId = assetInfo.deviceAssetId;
asset.userId = authUser.id;
asset.deviceId = assetInfo.deviceId;
asset.type = assetInfo.assetType || AssetType.OTHER;
asset.originalPath = path;
asset.createdAt = assetInfo.createdAt;
asset.modifiedAt = assetInfo.modifiedAt;
asset.isFavorite = assetInfo.isFavorite;
asset.mimeType = mimeType;
asset.duration = assetInfo.duration || null;
): Promise<AssetEntity> {
const assetEntity = await this._assetRepository.create(createAssetDto, authUser.id, originalPath, mimeType);
const createdAsset = await this.assetRepository.save(asset);
if (!createdAsset) {
throw new Error('Asset not created');
}
return createdAsset;
return assetEntity;
}
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
const rows = await this.assetRepository.find({
where: {
userId: authUser.id,
deviceId: deviceId,
},
select: ['deviceAssetId'],
});
const res: string[] = [];
rows.forEach((v) => res.push(v.deviceAssetId));
return res;
return this._assetRepository.getAllByDeviceId(authUser.id, deviceId);
}
public async getAllAssets(authUser: AuthUserDto): Promise<AssetResponseDto[]> {
const assets = await this.assetRepository.find({
where: {
userId: authUser.id,
resizePath: Not(IsNull()),
},
relations: ['exifInfo'],
order: {
createdAt: 'DESC',
},
});
const assets = await this._assetRepository.getAllByUserId(authUser.id);
return assets.map((asset) => mapAsset(asset));
}
public async findAssetOfDevice(deviceId: string, assetId: string): Promise<AssetResponseDto> {
// TODO - Refactor this to get asset by its own id
private async findAssetOfDevice(deviceId: string, assetId: string): Promise<AssetResponseDto> {
const rows = await this.assetRepository.query(
'SELECT * FROM assets a WHERE a."deviceAssetId" = $1 AND a."deviceId" = $2',
[assetId, deviceId],
@ -117,16 +85,7 @@ export class AssetService {
}
public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> {
const asset = await this.assetRepository.findOne({
where: {
id: assetId,
},
relations: ['exifInfo'],
});
if (!asset) {
throw new NotFoundException('Asset not found');
}
const asset = await this._assetRepository.getById(assetId);
return mapAsset(asset);
}
@ -394,45 +353,35 @@ export class AssetService {
async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> {
const possibleSearchTerm = new Set<string>();
// TODO: should use query builder
const rows = await this.assetRepository.query(
`
SELECT DISTINCT si.tags, si.objects, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
FROM assets a
LEFT JOIN exif e ON a.id = e."assetId"
LEFT JOIN smart_info si ON a.id = si."assetId"
WHERE a."userId" = $1;
`,
[authUser.id],
);
rows.forEach((row: { [x: string]: any }) => {
const rows = await this._assetRepository.getSearchPropertiesByUserId(authUser.id);
rows.forEach((row: SearchPropertiesDto) => {
// tags
row['tags']?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
// objects
row['objects']?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase()));
row.objects?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase()));
// asset's tyoe
possibleSearchTerm.add(row['type']?.toLowerCase());
possibleSearchTerm.add(row.assetType?.toLowerCase() || '');
// image orientation
possibleSearchTerm.add(row['orientation']?.toLowerCase());
possibleSearchTerm.add(row.orientation?.toLowerCase() || '');
// Lens model
possibleSearchTerm.add(row['lensModel']?.toLowerCase());
possibleSearchTerm.add(row.lensModel?.toLowerCase() || '');
// Make and model
possibleSearchTerm.add(row['make']?.toLowerCase());
possibleSearchTerm.add(row['model']?.toLowerCase());
possibleSearchTerm.add(row.make?.toLowerCase() || '');
possibleSearchTerm.add(row.model?.toLowerCase() || '');
// Location
possibleSearchTerm.add(row['city']?.toLowerCase());
possibleSearchTerm.add(row['state']?.toLowerCase());
possibleSearchTerm.add(row['country']?.toLowerCase());
possibleSearchTerm.add(row.city?.toLowerCase() || '');
possibleSearchTerm.add(row.state?.toLowerCase() || '');
possibleSearchTerm.add(row.country?.toLowerCase() || '');
});
return Array.from(possibleSearchTerm).filter((x) => x != null);
return Array.from(possibleSearchTerm).filter((x) => x != null && x != '');
}
async searchAsset(authUser: AuthUserDto, searchAssetDto: SearchAssetDto): Promise<AssetResponseDto[]> {
@ -459,33 +408,12 @@ export class AssetService {
return searchResults.map((asset) => mapAsset(asset));
}
async getCuratedLocation(authUser: AuthUserDto) {
return await this.assetRepository.query(
`
SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN exif e ON a.id = e."assetId"
WHERE a."userId" = $1
AND e.city IS NOT NULL
AND a.type = 'IMAGE';
`,
[authUser.id],
);
async getCuratedLocation(authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
return this._assetRepository.getLocationsByUserId(authUser.id);
}
async getCuratedObject(authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
const curatedObjects: CuratedObjectsResponseDto[] = await this.assetRepository.query(
`
SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
FROM assets a
LEFT JOIN smart_info si ON a.id = si."assetId"
WHERE a."userId" = $1
AND si.objects IS NOT NULL
`,
[authUser.id],
);
return curatedObjects;
return this._assetRepository.getDetectedObjectsByUserId(authUser.id);
}
async checkDuplicatedAsset(
@ -504,4 +432,16 @@ export class AssetService {
return new CheckDuplicateAssetResponseDto(isDuplicated, res?.id);
}
async getAssetCountByTimeGroup(
authUser: AuthUserDto,
getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto,
): Promise<AssetCountByTimeGroupResponseDto> {
const result = await this._assetRepository.getAssetCountByTimeGroup(
authUser.id,
getAssetCountByTimeGroupDto.timeGroup,
);
return mapAssetCountByTimeGroupResponse(result);
}
}

View File

@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export enum TimeGroupEnum {
Day = 'day',
Month = 'month',
}
export class GetAssetCountByTimeGroupDto {
@IsNotEmpty()
@ApiProperty({
type: String,
enum: TimeGroupEnum,
enumName: 'TimeGroupEnum',
})
timeGroup!: TimeGroupEnum;
}

View File

@ -0,0 +1,12 @@
export class SearchPropertiesDto {
tags?: string[];
objects?: string[];
assetType?: string;
orientation?: string;
lensModel?: string;
make?: string;
model?: string;
city?: string;
state?: string;
country?: string;
}

View File

@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger';
export class AssetCountByTimeGroupDto {
@ApiProperty({ type: 'string' })
timeGroup!: string;
@ApiProperty({ type: 'integer' })
count!: number;
}
export class AssetCountByTimeGroupResponseDto {
groups!: AssetCountByTimeGroupDto[];
@ApiProperty({ type: 'integer' })
totalAssets!: number;
}
export function mapAssetCountByTimeGroupResponse(result: AssetCountByTimeGroupDto[]): AssetCountByTimeGroupResponseDto {
return {
groups: result,
totalAssets: result.map((group) => group.count).reduce((a, b) => a + b, 0),
};
}

View File

@ -5,7 +5,7 @@ import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserEntity } from '@app/database/entities/user.entity';
import { Repository } from 'typeorm';
import cookieParser from 'cookie';
@WebSocketGateway({ cors: true })
export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor(
@ -26,8 +26,24 @@ export class CommunicationGateway implements OnGatewayConnection, OnGatewayDisco
async handleConnection(client: Socket) {
try {
Logger.log(`New websocket connection: ${client.id}`, 'WebsocketConnectionEvent');
let accessToken = '';
const accessToken = client.handshake.headers.authorization?.split(' ')[1];
if (client.handshake.headers.cookie != undefined) {
const cookies = cookieParser.parse(client.handshake.headers.cookie);
if (cookies.immich_access_token) {
accessToken = cookies.immich_access_token;
} else {
client.emit('error', 'unauthorized');
client.disconnect();
return;
}
} else if (client.handshake.headers.authorization != undefined) {
accessToken = client.handshake.headers.authorization.split(' ')[1];
} else {
client.emit('error', 'unauthorized');
client.disconnect();
return;
}
const res: JwtValidationResult = accessToken
? await this.immichJwtService.validateToken(accessToken)

File diff suppressed because one or more lines are too long

View File

@ -145,6 +145,44 @@ export interface AlbumResponseDto {
*/
'assets': Array<AssetResponseDto>;
}
/**
*
* @export
* @interface AssetCountByTimeGroupDto
*/
export interface AssetCountByTimeGroupDto {
/**
*
* @type {string}
* @memberof AssetCountByTimeGroupDto
*/
'timeGroup': string;
/**
*
* @type {number}
* @memberof AssetCountByTimeGroupDto
*/
'count': number;
}
/**
*
* @export
* @interface AssetCountByTimeGroupResponseDto
*/
export interface AssetCountByTimeGroupResponseDto {
/**
*
* @type {number}
* @memberof AssetCountByTimeGroupResponseDto
*/
'totalAssets': number;
/**
*
* @type {Array<AssetCountByTimeGroupDto>}
* @memberof AssetCountByTimeGroupResponseDto
*/
'groups': Array<AssetCountByTimeGroupDto>;
}
/**
*
* @export
@ -720,6 +758,19 @@ export interface ExifResponseDto {
*/
'country'?: string | null;
}
/**
*
* @export
* @interface GetAssetCountByTimeGroupDto
*/
export interface GetAssetCountByTimeGroupDto {
/**
*
* @type {TimeGroupEnum}
* @memberof GetAssetCountByTimeGroupDto
*/
'timeGroup': TimeGroupEnum;
}
/**
*
* @export
@ -996,6 +1047,20 @@ export const ThumbnailFormat = {
export type ThumbnailFormat = typeof ThumbnailFormat[keyof typeof ThumbnailFormat];
/**
*
* @export
* @enum {string}
*/
export const TimeGroupEnum = {
Day: 'day',
Month: 'month'
} as const;
export type TimeGroupEnum = typeof TimeGroupEnum[keyof typeof TimeGroupEnum];
/**
*
* @export
@ -2072,13 +2137,52 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions,
};
},
/**
*
* @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByTimeGroup: async (getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'getAssetCountByTimeGroupDto' is not null or undefined
assertParamExists('getAssetCountByTimeGroup', 'getAssetCountByTimeGroupDto', getAssetCountByTimeGroupDto)
const localVarPath = `/asset/count-by-date`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(getAssetCountByTimeGroupDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetSearchTerms: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/searchTerm`;
const localVarPath = `/asset/search-terms`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@ -2153,7 +2257,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @throws {RequiredError}
*/
getCuratedLocations: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/allLocation`;
const localVarPath = `/asset/curated-locations`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@ -2186,7 +2290,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @throws {RequiredError}
*/
getCuratedObjects: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/asset/allObjects`;
const localVarPath = `/asset/curated-objects`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
@ -2456,6 +2560,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetById(assetId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetCountByTimeGroupResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
@ -2598,6 +2712,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
getAssetById(assetId: string, options?: any): AxiosPromise<AssetResponseDto> {
return localVarFp.getAssetById(assetId, options).then((request) => request(axios, basePath));
},
/**
*
* @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: any): AxiosPromise<AssetCountByTimeGroupResponseDto> {
return localVarFp.getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
@ -2742,6 +2865,17 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).getAssetById(assetId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {GetAssetCountByTimeGroupDto} getAssetCountByTimeGroupDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public getAssetCountByTimeGroup(getAssetCountByTimeGroupDto: GetAssetCountByTimeGroupDto, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getAssetCountByTimeGroup(getAssetCountByTimeGroupDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.

View File

@ -1,5 +1,4 @@
import { Socket, io } from 'socket.io-client';
import { writable } from 'svelte/store';
let websocket: Socket;

View File

@ -1,4 +1,4 @@
import { serverApi } from '@api';
import { serverApi, TimeGroupEnum } from '@api';
import * as cookieParser from 'cookie';
import type { LayoutServerLoad } from './$types';
@ -21,6 +21,9 @@ export const load: LayoutServerLoad = async ({ request }) => {
user: userInfo
};
} catch (e) {
console.log('[ERROR] layout.server.ts [LayoutServerLoad]: ', e);
console.error('[ERROR] layout.server.ts [LayoutServerLoad]: ', e);
return {
user: undefined
};
}
};

View File

@ -20,11 +20,13 @@
import Close from 'svelte-material-icons/Close.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import type { PageData } from './$types';
import { onMount } from 'svelte';
import { onMount, onDestroy } from 'svelte';
import {
notificationController,
NotificationType
} from '$lib/components/shared-components/notification/notification';
import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket';
export let data: PageData;
@ -193,6 +195,18 @@
console.error('Error deleteSelectedAssetHandler', e);
}
};
onMount(async () => {
openWebsocketConnection();
const { data: assets } = await api.assetApi.getAllAssets();
setAssetInfo(assets);
});
onDestroy(() => {
closeWebsocketConnection();
});
</script>
<svelte:head>