1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-24 10:37:28 +02:00

fix(server): memory lane title (#2772)

* fix(server): memory lane title

* feat: parallel requests

* pr feedback

* fix test

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen 2023-06-15 14:05:30 -04:00 committed by GitHub
parent 045bb855d2
commit 896645130b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 123 additions and 54 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1539,10 +1539,12 @@
"operationId": "getMemoryLane",
"parameters": [
{
"name": "timezone",
"name": "timestamp",
"required": true,
"in": "query",
"description": "Get pictures for +24 hours from this time going back x years",
"schema": {
"format": "date-time",
"type": "string"
}
}

View File

@ -1,5 +1,6 @@
import { assetEntityStub, authStub, newAssetRepositoryMock } from '@test';
import { AssetService, IAssetRepository } from '.';
import { when } from 'jest-when';
import { AssetService, IAssetRepository, mapAsset } from '.';
describe(AssetService.name, () => {
let sut: AssetService;
@ -38,4 +39,62 @@ describe(AssetService.name, () => {
});
});
});
describe('getMemoryLane', () => {
it('should get pictures for each year', async () => {
assetMock.getByDate.mockResolvedValue([]);
await expect(sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15), years: 10 })).resolves.toEqual(
[],
);
expect(assetMock.getByDate).toHaveBeenCalledTimes(10);
expect(assetMock.getByDate.mock.calls).toEqual([
[authStub.admin.id, new Date('2022-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2020-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2019-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2018-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2017-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2016-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2015-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2014-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2013-06-15T00:00:00.000Z')],
]);
});
it('should keep hours from the date', async () => {
assetMock.getByDate.mockResolvedValue([]);
await expect(
sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15, 5), years: 2 }),
).resolves.toEqual([]);
expect(assetMock.getByDate).toHaveBeenCalledTimes(2);
expect(assetMock.getByDate.mock.calls).toEqual([
[authStub.admin.id, new Date('2022-06-15T05:00:00.000Z')],
[authStub.admin.id, new Date('2021-06-15T05:00:00.000Z')],
]);
});
});
it('should set the title correctly', async () => {
when(assetMock.getByDate)
.calledWith(authStub.admin.id, new Date('2022-06-15T00:00:00.000Z'))
.mockResolvedValue([assetEntityStub.image]);
when(assetMock.getByDate)
.calledWith(authStub.admin.id, new Date('2021-06-15T00:00:00.000Z'))
.mockResolvedValue([assetEntityStub.video]);
await expect(sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15), years: 2 })).resolves.toEqual([
{ title: '1 year since...', assets: [mapAsset(assetEntityStub.image)] },
{ title: '2 years since...', assets: [mapAsset(assetEntityStub.video)] },
]);
expect(assetMock.getByDate).toHaveBeenCalledTimes(2);
expect(assetMock.getByDate.mock.calls).toEqual([
[authStub.admin.id, new Date('2022-06-15T00:00:00.000Z')],
[authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')],
]);
});
});

View File

@ -1,10 +1,11 @@
import { Inject } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AuthUserDto } from '../auth';
import { IAssetRepository } from './asset.repository';
import { MemoryLaneDto } from './dto';
import { MapMarkerDto } from './dto/map-marker.dto';
import { MapMarkerResponseDto, mapAsset } from './response-dto';
import { mapAsset, MapMarkerResponseDto } from './response-dto';
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
import { DateTime } from 'luxon';
export class AssetService {
constructor(@Inject(IAssetRepository) private assetRepository: IAssetRepository) {}
@ -13,30 +14,22 @@ export class AssetService {
return this.assetRepository.getMapMarkers(authUser.id, options);
}
async getMemoryLane(authUser: AuthUserDto, timezone: string): Promise<MemoryLaneResponseDto[]> {
const result: MemoryLaneResponseDto[] = [];
async getMemoryLane(authUser: AuthUserDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
const target = DateTime.fromJSDate(dto.timestamp);
const luxonDate = DateTime.fromISO(new Date().toISOString(), { zone: timezone });
const today = new Date(luxonDate.year, luxonDate.month - 1, luxonDate.day);
const onRequest = async (yearsAgo: number): Promise<MemoryLaneResponseDto> => {
const assets = await this.assetRepository.getByDate(authUser.id, target.minus({ years: yearsAgo }).toJSDate());
return {
title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`,
assets: assets.map((a) => mapAsset(a)),
};
};
const years = Array.from({ length: 30 }, (_, i) => {
const year = today.getFullYear() - i - 1;
return new Date(year, today.getMonth(), today.getDate());
});
for (const year of years) {
const assets = await this.assetRepository.getByDate(authUser.id, year);
if (assets.length > 0) {
const yearGap = today.getFullYear() - year.getFullYear();
const memory = new MemoryLaneResponseDto();
memory.title = `${yearGap} year${yearGap > 1 && 's'} since...`;
memory.assets = assets.map((a) => mapAsset(a));
result.push(memory);
}
const requests: Promise<MemoryLaneResponseDto>[] = [];
for (let i = 1; i <= dto.years; i++) {
requests.push(onRequest(i));
}
return result;
return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0));
}
}

View File

@ -1,2 +1,3 @@
export * from './asset-ids.dto';
export * from './map-marker.dto';
export * from './memory-lane.dto';

View File

@ -0,0 +1,14 @@
import { Type } from 'class-transformer';
import { IsDate, IsNumber, IsPositive } from 'class-validator';
export class MemoryLaneDto {
/** Get pictures for +24 hours from this time going back x years */
@IsDate()
@Type(() => Date)
timestamp!: Date;
@IsNumber()
@IsPositive()
@Type(() => Number)
years = 30;
}

View File

@ -2,6 +2,5 @@ import { AssetResponseDto } from './asset-response.dto';
export class MemoryLaneResponseDto {
title!: string;
assets!: AssetResponseDto[];
}

View File

@ -1,4 +1,4 @@
import { AssetService, AuthUserDto, MapMarkerResponseDto } from '@app/domain';
import { AssetService, AuthUserDto, MapMarkerResponseDto, MemoryLaneDto } from '@app/domain';
import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto';
import { Controller, Get, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
@ -20,10 +20,7 @@ export class AssetController {
}
@Get('memory-lane')
getMemoryLane(
@GetAuthUser() authUser: AuthUserDto,
@Query('timezone') timezone: string,
): Promise<MemoryLaneResponseDto[]> {
return this.service.getMemoryLane(authUser, timezone);
getMemoryLane(@GetAuthUser() authUser: AuthUserDto, @Query() dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
return this.service.getMemoryLane(authUser, dto);
}
}

View File

@ -11,6 +11,7 @@ import {
} from '@app/domain';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DateTime } from 'luxon';
import { FindOptionsRelations, FindOptionsWhere, In, IsNull, Not, Repository } from 'typeorm';
import { AssetEntity, AssetType } from '../entities';
import OptionalBetween from '../utils/optional-between.util';
@ -21,7 +22,7 @@ export class AssetRepository implements IAssetRepository {
constructor(@InjectRepository(AssetEntity) private repository: Repository<AssetEntity>) {}
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]> {
// For reference of a correct approach althought slower
// For reference of a correct approach although slower
// let builder = this.repository
// .createQueryBuilder('asset')
@ -36,14 +37,13 @@ export class AssetRepository implements IAssetRepository {
// .orderBy('asset.fileCreatedAt', 'DESC');
// return builder.getMany();
const tomorrow = new Date(date.getTime() + 24 * 60 * 60 * 1000);
return this.repository.find({
where: {
ownerId,
isVisible: true,
isArchived: false,
fileCreatedAt: OptionalBetween(date, tomorrow),
fileCreatedAt: OptionalBetween(date, DateTime.fromJSDate(date).plus({ day: 1 }).toJSDate()),
},
relations: {
exifInfo: true,

View File

@ -5523,13 +5523,13 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
* @param {string} timezone
* @param {string} timestamp Get pictures for +24 hours from this time going back x years
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMemoryLane: async (timezone: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'timezone' is not null or undefined
assertParamExists('getMemoryLane', 'timezone', timezone)
getMemoryLane: async (timestamp: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'timestamp' is not null or undefined
assertParamExists('getMemoryLane', 'timestamp', timestamp)
const localVarPath = `/asset/memory-lane`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@ -5551,8 +5551,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (timezone !== undefined) {
localVarQueryParameter['timezone'] = timezone;
if (timestamp !== undefined) {
localVarQueryParameter['timestamp'] = (timestamp as any instanceof Date) ?
(timestamp as any).toISOString() :
timestamp;
}
@ -6157,12 +6159,12 @@ export const AssetApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {string} timezone
* @param {string} timestamp Get pictures for +24 hours from this time going back x years
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getMemoryLane(timezone: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timezone, options);
async getMemoryLane(timestamp: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
@ -6446,12 +6448,12 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
},
/**
*
* @param {string} timezone
* @param {string} timestamp Get pictures for +24 hours from this time going back x years
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMemoryLane(timezone: string, options?: any): AxiosPromise<Array<MemoryLaneResponseDto>> {
return localVarFp.getMemoryLane(timezone, options).then((request) => request(axios, basePath));
getMemoryLane(timestamp: string, options?: any): AxiosPromise<Array<MemoryLaneResponseDto>> {
return localVarFp.getMemoryLane(timestamp, options).then((request) => request(axios, basePath));
},
/**
* Get all asset of a device that are in the database, ID only.
@ -6857,11 +6859,11 @@ export interface AssetApiGetMapMarkersRequest {
*/
export interface AssetApiGetMemoryLaneRequest {
/**
*
* Get pictures for +24 hours from this time going back x years
* @type {string}
* @memberof AssetApiGetMemoryLane
*/
readonly timezone: string
readonly timestamp: string
}
/**
@ -7304,7 +7306,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).getMemoryLane(requestParameters.timezone, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).getMemoryLane(requestParameters.timestamp, options).then((request) => request(this.axios, this.basePath));
}
/**

View File

@ -35,8 +35,9 @@
onMount(async () => {
if (!$memoryStore) {
const timezone = DateTime.local().zoneName;
const { data } = await api.assetApi.getMemoryLane({ timezone });
const { data } = await api.assetApi.getMemoryLane({
timestamp: DateTime.local().startOf('day').toISO()
});
$memoryStore = data;
}

View File

@ -11,8 +11,9 @@
$: shouldRender = memoryLane.length > 0;
onMount(async () => {
const timezone = DateTime.local().zoneName;
const { data } = await api.assetApi.getMemoryLane({ timezone });
const { data } = await api.assetApi.getMemoryLane({
timestamp: DateTime.local().startOf('day').toISO()
});
memoryLane = data;
$memoryStore = data;