You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-10 23:22:22 +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:
187
server/apps/immich/src/api-v1/asset/asset-repository.ts
Normal file
187
server/apps/immich/src/api-v1/asset/asset-repository.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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
|
||||
*/
|
||||
|
@@ -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 {}
|
||||
|
92
server/apps/immich/src/api-v1/asset/asset.service.spec.ts
Normal file
92
server/apps/immich/src/api-v1/asset/asset.service.spec.ts
Normal 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');
|
||||
});
|
||||
});
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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),
|
||||
};
|
||||
}
|
@@ -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
Reference in New Issue
Block a user