From 192e9505671633de8f7f7e8a713ff6b234230765 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 4 Oct 2023 18:11:11 -0400 Subject: [PATCH] fix: use local time for time buckets and improve memories (#4072) * fix: timezone bucket timezones * chore: open api * fix: interpret local time in utc * fix: tests * fix: refactor memory lane * fix(web): use local date in memory viewer * chore: set localDateTime non-null * fix: filter out memories from the current year * wip: move localDateTime to asset * fix: correct sorting from db * fix: migration * fix: web typo * fix: formatting * fix: e2e * chore: localDateTime is non-null * chore: more non-nulliness * fix: asset stub * fix: tests * fix: use extract and index for day of year * fix: don't show memories before today * fix: cleanup * fix: tests * fix: only use localtime for tz * fix: display memories in client timezone * fix: tests * fix: svelte tests * fix: bugs * chore: open api --------- Co-authored-by: Jonathan Jogenfors --- cli/src/api/open-api/api.ts | 51 ++++++++++++------ .../memories/services/memory.service.dart | 4 +- mobile/openapi/doc/AssetApi.md | 10 ++-- mobile/openapi/doc/AssetResponseDto.md | 1 + mobile/openapi/lib/api/asset_api.dart | 19 ++++--- .../openapi/lib/model/asset_response_dto.dart | 10 +++- mobile/openapi/test/asset_api_test.dart | 2 +- .../openapi/test/asset_response_dto_test.dart | 5 ++ server/immich-openapi-specs.json | 19 +++++-- server/src/domain/asset/asset.repository.ts | 26 ++++++++- server/src/domain/asset/asset.service.spec.ts | 54 ++++--------------- server/src/domain/asset/asset.service.ts | 30 +++++------ .../src/domain/asset/dto/memory-lane.dto.ts | 23 ++++---- .../asset/response-dto/asset-response.dto.ts | 2 + .../domain/library/library.service.spec.ts | 3 ++ server/src/domain/library/library.service.ts | 1 + .../domain/metadata/metadata.service.spec.ts | 14 +++-- .../src/domain/metadata/metadata.service.ts | 16 +++++- .../immich/api-v1/asset/asset-repository.ts | 6 +-- server/src/immich/api-v1/asset/asset.core.ts | 1 + server/src/infra/entities/asset.entity.ts | 5 ++ .../1694525143117-AddLocalDateTime.ts | 30 +++++++++++ .../infra/repositories/asset.repository.ts | 31 +++++++++-- server/test/e2e/asset.e2e-spec.ts | 3 +- server/test/fixtures/asset.stub.ts | 48 +++++++++++++++++ server/test/fixtures/shared-link.stub.ts | 2 + .../repositories/asset.repository.mock.ts | 1 + web/src/api/open-api/api.ts | 51 ++++++++++++------ .../memory-page/memory-viewer.svelte | 6 ++- .../photos-page/asset-date-group.svelte | 3 +- .../components/photos-page/memory-lane.svelte | 5 +- web/src/lib/utils/timeline-util.ts | 2 +- 32 files changed, 337 insertions(+), 147 deletions(-) create mode 100644 server/src/infra/migrations/1694525143117-AddLocalDateTime.ts diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 9bd1f4b1c9..c71b4705db 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -669,6 +669,12 @@ export interface AssetResponseDto { * @memberof AssetResponseDto */ 'livePhotoVideoId'?: string | null; + /** + * + * @type {string} + * @memberof AssetResponseDto + */ + 'localDateTime': string; /** * * @type {string} @@ -6340,13 +6346,16 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * - * @param {string} timestamp Get pictures for +24 hours from this time going back x years + * @param {number} day + * @param {number} month * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getMemoryLane: async (timestamp: string, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'timestamp' is not null or undefined - assertParamExists('getMemoryLane', 'timestamp', timestamp) + getMemoryLane: async (day: number, month: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'day' is not null or undefined + assertParamExists('getMemoryLane', 'day', day) + // verify required parameter 'month' is not null or undefined + assertParamExists('getMemoryLane', 'month', month) 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); @@ -6368,10 +6377,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (timestamp !== undefined) { - localVarQueryParameter['timestamp'] = (timestamp as any instanceof Date) ? - (timestamp as any).toISOString() : - timestamp; + if (day !== undefined) { + localVarQueryParameter['day'] = day; + } + + if (month !== undefined) { + localVarQueryParameter['month'] = month; } @@ -7152,12 +7163,13 @@ export const AssetApiFp = function(configuration?: Configuration) { }, /** * - * @param {string} timestamp Get pictures for +24 hours from this time going back x years + * @param {number} day + * @param {number} month * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getMemoryLane(timestamp: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options); + async getMemoryLane(day: number, month: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(day, month, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -7443,7 +7455,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getMemoryLane(requestParameters.timestamp, options).then((request) => request(axios, basePath)); + return localVarFp.getMemoryLane(requestParameters.day, requestParameters.month, options).then((request) => request(axios, basePath)); }, /** * @@ -7888,11 +7900,18 @@ export interface AssetApiGetMapMarkersRequest { */ export interface AssetApiGetMemoryLaneRequest { /** - * Get pictures for +24 hours from this time going back x years - * @type {string} + * + * @type {number} * @memberof AssetApiGetMemoryLane */ - readonly timestamp: string + readonly day: number + + /** + * + * @type {number} + * @memberof AssetApiGetMemoryLane + */ + readonly month: number } /** @@ -8398,7 +8417,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getMemoryLane(requestParameters.timestamp, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getMemoryLane(requestParameters.day, requestParameters.month, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/mobile/lib/modules/memories/services/memory.service.dart b/mobile/lib/modules/memories/services/memory.service.dart index ab38c0694d..338c79e553 100644 --- a/mobile/lib/modules/memories/services/memory.service.dart +++ b/mobile/lib/modules/memories/services/memory.service.dart @@ -22,9 +22,9 @@ class MemoryService { Future?> getMemoryLane() async { try { final now = DateTime.now(); - final beginningOfDate = DateTime(now.year, now.month, now.day); final data = await _apiService.assetApi.getMemoryLane( - beginningOfDate, + now.day, + now.month, ); if (data == null) { diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index f7511c8144..1affd29f6c 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -963,7 +963,7 @@ 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) # **getMemoryLane** -> List getMemoryLane(timestamp) +> List getMemoryLane(day, month) @@ -986,10 +986,11 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = AssetApi(); -final timestamp = 2013-10-20T19:20:30+01:00; // DateTime | Get pictures for +24 hours from this time going back x years +final day = 56; // int | +final month = 56; // int | try { - final result = api_instance.getMemoryLane(timestamp); + final result = api_instance.getMemoryLane(day, month); print(result); } catch (e) { print('Exception when calling AssetApi->getMemoryLane: $e\n'); @@ -1000,7 +1001,8 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **timestamp** | **DateTime**| Get pictures for +24 hours from this time going back x years | + **day** | **int**| | + **month** | **int**| | ### Return type diff --git a/mobile/openapi/doc/AssetResponseDto.md b/mobile/openapi/doc/AssetResponseDto.md index 34d7e444cc..ccefa2f722 100644 --- a/mobile/openapi/doc/AssetResponseDto.md +++ b/mobile/openapi/doc/AssetResponseDto.md @@ -23,6 +23,7 @@ Name | Type | Description | Notes **isReadOnly** | **bool** | | **libraryId** | **String** | | **livePhotoVideoId** | **String** | | [optional] +**localDateTime** | [**DateTime**](DateTime.md) | | **originalFileName** | **String** | | **originalPath** | **String** | | **owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional] diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index cdab82f741..7ddbe437ab 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -984,9 +984,10 @@ class AssetApi { /// Performs an HTTP 'GET /asset/memory-lane' operation and returns the [Response]. /// Parameters: /// - /// * [DateTime] timestamp (required): - /// Get pictures for +24 hours from this time going back x years - Future getMemoryLaneWithHttpInfo(DateTime timestamp,) async { + /// * [int] day (required): + /// + /// * [int] month (required): + Future getMemoryLaneWithHttpInfo(int day, int month,) async { // ignore: prefer_const_declarations final path = r'/asset/memory-lane'; @@ -997,7 +998,8 @@ class AssetApi { final headerParams = {}; final formParams = {}; - queryParams.addAll(_queryParams('', 'timestamp', timestamp)); + queryParams.addAll(_queryParams('', 'day', day)); + queryParams.addAll(_queryParams('', 'month', month)); const contentTypes = []; @@ -1015,10 +1017,11 @@ class AssetApi { /// Parameters: /// - /// * [DateTime] timestamp (required): - /// Get pictures for +24 hours from this time going back x years - Future?> getMemoryLane(DateTime timestamp,) async { - final response = await getMemoryLaneWithHttpInfo(timestamp,); + /// * [int] day (required): + /// + /// * [int] month (required): + Future?> getMemoryLane(int day, int month,) async { + final response = await getMemoryLaneWithHttpInfo(day, month,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 69f36eef01..762b23cfa8 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -28,6 +28,7 @@ class AssetResponseDto { required this.isReadOnly, required this.libraryId, this.livePhotoVideoId, + required this.localDateTime, required this.originalFileName, required this.originalPath, this.owner, @@ -78,6 +79,8 @@ class AssetResponseDto { String? livePhotoVideoId; + DateTime localDateTime; + String originalFileName; String originalPath; @@ -130,6 +133,7 @@ class AssetResponseDto { other.isReadOnly == isReadOnly && other.libraryId == libraryId && other.livePhotoVideoId == livePhotoVideoId && + other.localDateTime == localDateTime && other.originalFileName == originalFileName && other.originalPath == originalPath && other.owner == owner && @@ -160,6 +164,7 @@ class AssetResponseDto { (isReadOnly.hashCode) + (libraryId.hashCode) + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + + (localDateTime.hashCode) + (originalFileName.hashCode) + (originalPath.hashCode) + (owner == null ? 0 : owner!.hashCode) + @@ -173,7 +178,7 @@ class AssetResponseDto { (updatedAt.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]'; + String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -200,6 +205,7 @@ class AssetResponseDto { } else { // json[r'livePhotoVideoId'] = null; } + json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String(); json[r'originalFileName'] = this.originalFileName; json[r'originalPath'] = this.originalPath; if (this.owner != null) { @@ -249,6 +255,7 @@ class AssetResponseDto { isReadOnly: mapValueOfType(json, r'isReadOnly')!, libraryId: mapValueOfType(json, r'libraryId')!, livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), + localDateTime: mapDateTime(json, r'localDateTime', '')!, originalFileName: mapValueOfType(json, r'originalFileName')!, originalPath: mapValueOfType(json, r'originalPath')!, owner: UserResponseDto.fromJson(json[r'owner']), @@ -320,6 +327,7 @@ class AssetResponseDto { 'isOffline', 'isReadOnly', 'libraryId', + 'localDateTime', 'originalFileName', 'originalPath', 'ownerId', diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 6098a34d73..32f5474f7f 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -107,7 +107,7 @@ void main() { // TODO }); - //Future> getMemoryLane(DateTime timestamp) async + //Future> getMemoryLane(int day, int month) async test('test getMemoryLane', () async { // TODO }); diff --git a/mobile/openapi/test/asset_response_dto_test.dart b/mobile/openapi/test/asset_response_dto_test.dart index fdbdb97ece..886fe854fa 100644 --- a/mobile/openapi/test/asset_response_dto_test.dart +++ b/mobile/openapi/test/asset_response_dto_test.dart @@ -92,6 +92,11 @@ void main() { // TODO }); + // DateTime localDateTime + test('to test the property `localDateTime`', () async { + // TODO + }); + // String originalFileName test('to test the property `originalFileName`', () async { // TODO diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 3deea17f09..52dd7848d1 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1477,13 +1477,19 @@ "operationId": "getMemoryLane", "parameters": [ { - "name": "timestamp", + "name": "day", "required": true, "in": "query", - "description": "Get pictures for +24 hours from this time going back x years", "schema": { - "format": "date-time", - "type": "string" + "type": "integer" + } + }, + { + "name": "month", + "required": true, + "in": "query", + "schema": { + "type": "integer" } } ], @@ -5617,6 +5623,10 @@ "nullable": true, "type": "string" }, + "localDateTime": { + "format": "date-time", + "type": "string" + }, "originalFileName": { "type": "string" }, @@ -5676,6 +5686,7 @@ "updatedAt", "isFavorite", "isArchived", + "localDateTime", "isOffline", "isExternal", "isReadOnly", diff --git a/server/src/domain/asset/asset.repository.ts b/server/src/domain/asset/asset.repository.ts index 80d0a7a569..442dfc649f 100644 --- a/server/src/domain/asset/asset.repository.ts +++ b/server/src/domain/asset/asset.repository.ts @@ -68,12 +68,34 @@ export interface TimeBucketItem { count: number; } +export type AssetCreate = Pick< + AssetEntity, + | 'deviceAssetId' + | 'ownerId' + | 'libraryId' + | 'deviceId' + | 'type' + | 'originalPath' + | 'fileCreatedAt' + | 'localDateTime' + | 'fileModifiedAt' + | 'checksum' + | 'originalFileName' +> & + Partial; + +export interface MonthDay { + day: number; + month: number; +} + export const IAssetRepository = 'IAssetRepository'; export interface IAssetRepository { - create(asset: Partial): Promise; + create(asset: AssetCreate): Promise; getByDate(ownerId: string, date: Date): Promise; getByIds(ids: string[]): Promise; + getByDayOfYear(ownerId: string, monthDay: MonthDay): Promise; getByChecksum(userId: string, checksum: Buffer): Promise; getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated; getByUserId(pagination: PaginationOptions, userId: string): Paginated; @@ -87,7 +109,7 @@ export interface IAssetRepository { deleteAll(ownerId: string): Promise; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; updateAll(ids: string[], options: Partial): Promise; - save(asset: Partial): Promise; + save(asset: Pick & Partial): Promise; findLivePhotoMatch(options: LivePhotoSearchOptions): Promise; getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise; getStatistics(ownerId: string, options: AssetStatsOptions): Promise; diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 54e093d61f..9640b6cfb3 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -274,60 +274,24 @@ 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')], - ]); + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2024-01-15')); }); - 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')], - ]); + afterAll(() => { + jest.useRealTimers(); }); it('should set the title correctly', async () => { - when(assetMock.getByDate) - .calledWith(authStub.admin.id, new Date('2022-06-15T00:00:00.000Z')) - .mockResolvedValue([assetStub.image]); - when(assetMock.getByDate) - .calledWith(authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')) - .mockResolvedValue([assetStub.video]); + assetMock.getByDayOfYear.mockResolvedValue([assetStub.image, assetStub.imageFrom2015]); - await expect(sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15), years: 2 })).resolves.toEqual([ + await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([ { title: '1 year since...', assets: [mapAsset(assetStub.image)] }, - { title: '2 years since...', assets: [mapAsset(assetStub.video)] }, + { title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] }, ]); - 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')], - ]); + expect(assetMock.getByDayOfYear.mock.calls).toEqual([[authStub.admin.id, { day: 15, month: 1 }]]); }); }); diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 35f8637b9b..90c083d584 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -1,6 +1,6 @@ import { AssetEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Logger } from '@nestjs/common'; -import { DateTime } from 'luxon'; +import _ from 'lodash'; import { extname } from 'path'; import sanitize from 'sanitize-filename'; import { AccessCore, IAccessRepository, Permission } from '../access'; @@ -138,22 +138,22 @@ export class AssetService { } async getMemoryLane(authUser: AuthUserDto, dto: MemoryLaneDto): Promise { - const target = DateTime.fromJSDate(dto.timestamp); + const currentYear = new Date().getFullYear(); + const assets = await this.assetRepository.getByDayOfYear(authUser.id, dto); - const onRequest = async (yearsAgo: number): Promise => { - 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)), - }; - }; + return _.chain(assets) + .filter((asset) => asset.localDateTime.getFullYear() < currentYear) + .map((asset) => { + const years = currentYear - asset.localDateTime.getFullYear(); - const requests: Promise[] = []; - for (let i = 1; i <= dto.years; i++) { - requests.push(onRequest(i)); - } - - return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0)); + return { + title: `${years} year${years > 1 ? 's' : ''} since...`, + asset: mapAsset(asset), + }; + }) + .groupBy((asset) => asset.title) + .map((items, title) => ({ title, assets: items.map(({ asset }) => asset) })) + .value(); } private async timeBucketChecks(authUser: AuthUserDto, dto: TimeBucketDto) { diff --git a/server/src/domain/asset/dto/memory-lane.dto.ts b/server/src/domain/asset/dto/memory-lane.dto.ts index 8309a73cad..43f74aff1e 100644 --- a/server/src/domain/asset/dto/memory-lane.dto.ts +++ b/server/src/domain/asset/dto/memory-lane.dto.ts @@ -1,14 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsDate, IsNumber, IsPositive } from 'class-validator'; +import { IsInt, Max, Min } 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() + @IsInt() @Type(() => Number) - years = 30; + @Max(31) + @Min(1) + @ApiProperty({ type: 'integer' }) + day!: number; + + @IsInt() + @Type(() => Number) + @Max(12) + @Min(1) + @ApiProperty({ type: 'integer' }) + month!: number; } diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts index 6c9adb0532..6d28bac926 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/domain/asset/response-dto/asset-response.dto.ts @@ -26,6 +26,7 @@ export class AssetResponseDto { updatedAt!: Date; isFavorite!: boolean; isArchived!: boolean; + localDateTime!: Date; isOffline!: boolean; isExternal!: boolean; isReadOnly!: boolean; @@ -54,6 +55,7 @@ function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto { thumbhash: entity.thumbhash?.toString('base64') ?? null, fileCreatedAt: entity.fileCreatedAt, fileModifiedAt: entity.fileModifiedAt, + localDateTime: entity.localDateTime, updatedAt: entity.updatedAt, isFavorite: entity.isFavorite, isArchived: entity.isArchived, diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index ad1bf046f6..63dfc376b5 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -217,6 +217,7 @@ describe(LibraryService.name, () => { deviceId: 'Library Import', fileCreatedAt: expect.any(Date), fileModifiedAt: expect.any(Date), + localDateTime: expect.any(Date), type: AssetType.IMAGE, originalFileName: 'photo', sidecarPath: null, @@ -264,6 +265,7 @@ describe(LibraryService.name, () => { deviceId: 'Library Import', fileCreatedAt: expect.any(Date), fileModifiedAt: expect.any(Date), + localDateTime: expect.any(Date), type: AssetType.IMAGE, originalFileName: 'photo', sidecarPath: '/data/user1/photo.jpg.xmp', @@ -310,6 +312,7 @@ describe(LibraryService.name, () => { deviceId: 'Library Import', fileCreatedAt: expect.any(Date), fileModifiedAt: expect.any(Date), + localDateTime: expect.any(Date), type: AssetType.VIDEO, originalFileName: 'video', sidecarPath: null, diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index d4abce3e45..604f37160d 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -251,6 +251,7 @@ export class LibraryService { deviceId: 'Library Import', fileCreatedAt: stats.mtime, fileModifiedAt: stats.mtime, + localDateTime: stats.mtime, type: assetType, originalFileName: parse(assetPath).name, sidecarPath, diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index e5613c2852..478285f272 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -231,6 +231,7 @@ describe(MetadataService.name, () => { id: assetStub.image.id, duration: null, fileCreatedAt: assetStub.image.createdAt, + localDateTime: new Date('2023-02-23T05:06:29.716Z'), }); }); @@ -252,6 +253,7 @@ describe(MetadataService.name, () => { id: assetStub.withLocation.id, duration: null, fileCreatedAt: assetStub.withLocation.createdAt, + localDateTime: new Date('2023-02-23T05:06:29.716Z'), }); }); @@ -299,16 +301,13 @@ describe(MetadataService.name, () => { const video = randomBytes(512); storageMock.readFile.mockResolvedValue(video); cryptoRepository.hashSha1.mockReturnValue(randomBytes(512)); + assetMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); assetMock.save.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]); expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object)); - expect(assetMock.save).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, - }); - expect(assetMock.save).toHaveBeenCalledWith( + expect(assetMock.create).toHaveBeenCalledWith( expect.objectContaining({ type: AssetType.VIDEO, originalFileName: assetStub.livePhotoStillAsset.originalFileName, @@ -316,6 +315,10 @@ describe(MetadataService.name, () => { isReadOnly: true, }), ); + expect(assetMock.save).toHaveBeenCalledWith({ + id: assetStub.livePhotoStillAsset.id, + livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + }); expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.METADATA_EXTRACTION, @@ -379,6 +382,7 @@ describe(MetadataService.name, () => { id: assetStub.image.id, duration: null, fileCreatedAt: new Date('1970-01-01'), + localDateTime: new Date('1970-01-01'), }); }); }); diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 1d0dac9c56..22172221cd 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -30,6 +30,7 @@ type ExifEntityWithoutGeocodeAndTypeOrm = Omit< >; const exifDate = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.toDate() : null); +const tzOffset = (dt: ExifDateTime | string | undefined) => (dt instanceof ExifDateTime ? dt?.tzoffsetMinutes : null); const validate = (value: T): NonNullable | null => { // handle lists of numbers @@ -156,9 +157,18 @@ export class MetadataService { await this.applyMotionPhotos(asset, tags); await this.applyReverseGeocoding(asset, exifData); await this.assetRepository.upsertExif(exifData); + let localDateTime = exifData.dateTimeOriginal ?? undefined; + + const dateTimeOriginal = exifDate(firstDateTime(tags as Tags)) ?? exifData.dateTimeOriginal; + const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0; + + if (dateTimeOriginal && timeZoneOffset) { + localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60000); + } await this.assetRepository.save({ id: asset.id, duration: tags.Duration ? this.getDuration(tags.Duration) : null, + localDateTime, fileCreatedAt: exifData.dateTimeOriginal ?? undefined, }); @@ -268,11 +278,13 @@ export class MetadataService { let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum); if (!motionAsset) { - motionAsset = await this.assetRepository.save({ + const createdAt = asset.fileCreatedAt ?? asset.createdAt; + motionAsset = await this.assetRepository.create({ libraryId: asset.libraryId, type: AssetType.VIDEO, - fileCreatedAt: asset.fileCreatedAt ?? asset.createdAt, + fileCreatedAt: createdAt, fileModifiedAt: asset.fileModifiedAt, + localDateTime: createdAt, checksum, ownerId: asset.ownerId, originalPath: this.storageCore.ensurePath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}-MP.mp4`), diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/immich/api-v1/asset/asset-repository.ts index dcc686c364..0bf46517c6 100644 --- a/server/src/immich/api-v1/asset/asset-repository.ts +++ b/server/src/immich/api-v1/asset/asset-repository.ts @@ -1,3 +1,4 @@ +import { AssetCreate } from '@app/domain'; import { AssetEntity } from '@app/infra/entities'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -19,11 +20,6 @@ export interface AssetOwnerCheck extends AssetCheck { ownerId: string; } -export type AssetCreate = Omit< - AssetEntity, - 'id' | 'createdAt' | 'updatedAt' | 'owner' | 'livePhotoVideoId' | 'library' ->; - export interface IAssetRepository { get(id: string): Promise; create(asset: AssetCreate): Promise; diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts index f25708d645..ac1b4e26fa 100644 --- a/server/src/immich/api-v1/asset/asset.core.ts +++ b/server/src/immich/api-v1/asset/asset.core.ts @@ -29,6 +29,7 @@ export class AssetCore { fileCreatedAt: dto.fileCreatedAt, fileModifiedAt: dto.fileModifiedAt, + localDateTime: dto.fileCreatedAt, type: mimeTypes.assetType(file.originalPath), isFavorite: dto.isFavorite, diff --git a/server/src/infra/entities/asset.entity.ts b/server/src/infra/entities/asset.entity.ts index c9d5743d4f..1b5fd5ca08 100644 --- a/server/src/infra/entities/asset.entity.ts +++ b/server/src/infra/entities/asset.entity.ts @@ -28,6 +28,8 @@ export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum'; @Index(ASSET_CHECKSUM_CONSTRAINT, ['owner', 'library', 'checksum'], { unique: true, }) +@Index('IDX_day_of_month', { synchronize: false }) +@Index('IDX_month', { synchronize: false }) // For all assets, each originalpath must be unique per user and library export class AssetEntity { @PrimaryGeneratedColumn('uuid') @@ -78,6 +80,9 @@ export class AssetEntity { @Column({ type: 'timestamptz' }) fileCreatedAt!: Date; + @Column({ type: 'timestamp' }) + localDateTime!: Date; + @Column({ type: 'timestamptz' }) fileModifiedAt!: Date; diff --git a/server/src/infra/migrations/1694525143117-AddLocalDateTime.ts b/server/src/infra/migrations/1694525143117-AddLocalDateTime.ts new file mode 100644 index 0000000000..3acfdde370 --- /dev/null +++ b/server/src/infra/migrations/1694525143117-AddLocalDateTime.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddLocalDateTime1694525143117 implements MigrationInterface { + name = 'AddLocalDateTime1694525143117'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "assets" ADD "localDateTime" TIMESTAMP`); + await queryRunner.query(` + update "assets" + set "localDateTime" = "fileCreatedAt"`); + + await queryRunner.query(` + update "assets" + set "localDateTime" = "fileCreatedAt" at TIME ZONE "exif"."timeZone" + from "exif" + where + "exif"."assetId" = "assets"."id" and + "exif"."timeZone" is not null`); + + await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" SET NOT NULL`); + await queryRunner.query(`CREATE INDEX "IDX_day_of_month" ON assets (EXTRACT(DAY FROM "localDateTime"))`); + await queryRunner.query(`CREATE INDEX "IDX_month" ON assets (EXTRACT(MONTH FROM "localDateTime"))`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "localDateTime"`); + await queryRunner.query(`DROP INDEX "IDX_day_of_month"`); + await queryRunner.query(`DROP INDEX "IDX_month"`); + } +} diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 62a2a3f649..e2cfb28de5 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -1,4 +1,5 @@ import { + AssetCreate, AssetSearchOptions, AssetStats, AssetStatsOptions, @@ -6,6 +7,7 @@ import { LivePhotoSearchOptions, MapMarker, MapMarkerSearchOptions, + MonthDay, Paginated, PaginationOptions, TimeBucketItem, @@ -38,9 +40,7 @@ export class AssetRepository implements IAssetRepository { await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] }); } - create( - asset: Omit, - ): Promise { + create(asset: AssetCreate): Promise { return this.repository.save(asset); } @@ -78,6 +78,26 @@ export class AssetRepository implements IAssetRepository { }); } + getByDayOfYear(ownerId: string, { day, month }: MonthDay): Promise { + return this.repository + .createQueryBuilder('entity') + .where( + `entity.ownerId = :ownerId + AND entity.isVisible = true + AND entity.isArchived = false + AND entity.resizePath IS NOT NULL + AND EXTRACT(DAY FROM entity.localDateTime) = :day + AND EXTRACT(MONTH FROM entity.localDateTime) = :month`, + { + ownerId, + day, + month, + }, + ) + .orderBy('entity.localDateTime', 'DESC') + .getMany(); + } + getByIds(ids: string[]): Promise { return this.repository.find({ where: { id: In(ids) }, @@ -454,8 +474,9 @@ export class AssetRepository implements IAssetRepository { getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise { const truncateValue = truncateMap[options.size]; return this.getBuilder(options) - .andWhere(`date_trunc('${truncateValue}', "fileCreatedAt") = :timeBucket`, { timeBucket }) - .orderBy('asset.fileCreatedAt', 'DESC') + .andWhere(`date_trunc('${truncateValue}', "localDateTime") = :timeBucket`, { timeBucket }) + .orderBy(`date_trunc('day', "localDateTime")`, 'DESC') + .addOrderBy('asset.fileCreatedAt', 'DESC') .getMany(); } diff --git a/server/test/e2e/asset.e2e-spec.ts b/server/test/e2e/asset.e2e-spec.ts index 5bba7355f0..a247409a9c 100644 --- a/server/test/e2e/asset.e2e-spec.ts +++ b/server/test/e2e/asset.e2e-spec.ts @@ -56,7 +56,7 @@ const createAsset = ( createdAt: Date, ): Promise => { const id = assetCount++; - return repository.save({ + return repository.create({ ownerId: loginResponse.userId, checksum: randomBytes(20), originalPath: `/tests/test_${id}`, @@ -66,6 +66,7 @@ const createAsset = ( isVisible: true, fileCreatedAt: createdAt, fileModifiedAt: new Date(), + localDateTime: createdAt, type: AssetType.IMAGE, originalFileName: `test_${id}`, }); diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index e5e069fe1a..030dabbe3f 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -23,6 +23,7 @@ export const assetStub = { 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, duration: null, @@ -56,6 +57,7 @@ export const assetStub = { 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, duration: null, @@ -93,6 +95,7 @@ export const assetStub = { 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, @@ -127,6 +130,7 @@ export const assetStub = { 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, @@ -164,6 +168,7 @@ export const assetStub = { 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, @@ -201,6 +206,7 @@ export const assetStub = { 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, @@ -238,6 +244,45 @@ export const assetStub = { 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, + duration: null, + isVisible: true, + livePhotoVideo: null, + livePhotoVideoId: null, + libraryId: 'library-id', + library: libraryStub.uploadLibrary1, + isExternal: false, + isOffline: false, + tags: [], + sharedLinks: [], + originalFileName: 'asset-id.ext', + faces: [], + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5_000, + } as ExifEntity, + }), + imageFrom2015: Object.freeze({ + id: 'asset-id-1', + deviceAssetId: 'device-asset-id', + fileModifiedAt: new Date('2015-02-23T05:06:29.716Z'), + fileCreatedAt: new Date('2015-02-23T05:06:29.716Z'), + owner: userStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/original/path.ext', + resizePath: '/uploads/user-id/thumbs/path.ext', + 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('2015-02-23T05:06:29.716Z'), + updatedAt: new Date('2015-02-23T05:06:29.716Z'), + localDateTime: new Date('2015-02-23T05:06:29.716Z'), isFavorite: true, isArchived: false, isExternal: false, @@ -276,6 +321,7 @@ export const assetStub = { 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, @@ -344,6 +390,7 @@ export const assetStub = { 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: false, isArchived: false, isReadOnly: false, @@ -382,6 +429,7 @@ export const assetStub = { 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, diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 82d336a2e3..601c41bc17 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -55,6 +55,7 @@ const assetResponse: AssetResponseDto = { isReadOnly: false, isOffline: false, fileCreatedAt: today, + localDateTime: today, updatedAt: today, isFavorite: false, isArchived: false, @@ -174,6 +175,7 @@ export const sharedLinkStub = { checksum: Buffer.from('file hash', 'utf8'), fileModifiedAt: today, fileCreatedAt: today, + localDateTime: today, createdAt: today, updatedAt: today, isFavorite: false, diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 1a13ef2c57..5af26cad71 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -5,6 +5,7 @@ export const newAssetRepositoryMock = (): jest.Mocked => { create: jest.fn(), upsertExif: jest.fn(), getByDate: jest.fn(), + getByDayOfYear: jest.fn(), getByIds: jest.fn().mockResolvedValue([]), getByAlbumId: jest.fn(), getByUserId: jest.fn(), diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 9bd1f4b1c9..c71b4705db 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -669,6 +669,12 @@ export interface AssetResponseDto { * @memberof AssetResponseDto */ 'livePhotoVideoId'?: string | null; + /** + * + * @type {string} + * @memberof AssetResponseDto + */ + 'localDateTime': string; /** * * @type {string} @@ -6340,13 +6346,16 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }, /** * - * @param {string} timestamp Get pictures for +24 hours from this time going back x years + * @param {number} day + * @param {number} month * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getMemoryLane: async (timestamp: string, options: AxiosRequestConfig = {}): Promise => { - // verify required parameter 'timestamp' is not null or undefined - assertParamExists('getMemoryLane', 'timestamp', timestamp) + getMemoryLane: async (day: number, month: number, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'day' is not null or undefined + assertParamExists('getMemoryLane', 'day', day) + // verify required parameter 'month' is not null or undefined + assertParamExists('getMemoryLane', 'month', month) 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); @@ -6368,10 +6377,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration // http bearer authentication required await setBearerAuthToObject(localVarHeaderParameter, configuration) - if (timestamp !== undefined) { - localVarQueryParameter['timestamp'] = (timestamp as any instanceof Date) ? - (timestamp as any).toISOString() : - timestamp; + if (day !== undefined) { + localVarQueryParameter['day'] = day; + } + + if (month !== undefined) { + localVarQueryParameter['month'] = month; } @@ -7152,12 +7163,13 @@ export const AssetApiFp = function(configuration?: Configuration) { }, /** * - * @param {string} timestamp Get pictures for +24 hours from this time going back x years + * @param {number} day + * @param {number} month * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getMemoryLane(timestamp: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options); + async getMemoryLane(day: number, month: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(day, month, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** @@ -7443,7 +7455,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @throws {RequiredError} */ getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig): AxiosPromise> { - return localVarFp.getMemoryLane(requestParameters.timestamp, options).then((request) => request(axios, basePath)); + return localVarFp.getMemoryLane(requestParameters.day, requestParameters.month, options).then((request) => request(axios, basePath)); }, /** * @@ -7888,11 +7900,18 @@ export interface AssetApiGetMapMarkersRequest { */ export interface AssetApiGetMemoryLaneRequest { /** - * Get pictures for +24 hours from this time going back x years - * @type {string} + * + * @type {number} * @memberof AssetApiGetMemoryLane */ - readonly timestamp: string + readonly day: number + + /** + * + * @type {number} + * @memberof AssetApiGetMemoryLane + */ + readonly month: number } /** @@ -8398,7 +8417,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).getMemoryLane(requestParameters.timestamp, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).getMemoryLane(requestParameters.day, requestParameters.month, options).then((request) => request(this.axios, this.basePath)); } /** diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 25dc189b8c..06e3421d70 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -87,8 +87,10 @@ onMount(async () => { if (!$memoryStore) { + const localTime = new Date(); const { data } = await api.assetApi.getMemoryLane({ - timestamp: DateTime.local().startOf('day').toISO() || '', + month: localTime.getMonth() + 1, + day: localTime.getDate(), }); $memoryStore = data; } @@ -212,7 +214,7 @@

- {DateTime.fromISO(currentMemory.assets[0].fileCreatedAt).toLocaleString(DateTime.DATE_FULL)} + {DateTime.fromISO(currentMemory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)}

{currentAsset.exifInfo?.city || ''} diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index e3610edadb..b2c42cebd8 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -126,7 +126,8 @@

{#each assetsGroupByDate as groupAssets, groupIndex (groupAssets[0].id)} - {@const groupTitle = formatGroupTitle(DateTime.fromISO(groupAssets[0].fileCreatedAt).startOf('day'))} + {@const asset = groupAssets[0]} + {@const groupTitle = formatGroupTitle(DateTime.fromISO(asset.localDateTime).startOf('day'))} diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index df842c9e27..1ade070eb8 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -1,6 +1,5 @@