mirror of
https://github.com/immich-app/immich.git
synced 2025-01-21 00:52:43 -05:00
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 <jonathan@jogenfors.se>
This commit is contained in:
parent
126dd45751
commit
192e950567
32 changed files with 337 additions and 147 deletions
51
cli/src/api/open-api/api.ts
generated
51
cli/src/api/open-api/api.ts
generated
|
@ -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<RequestArgs> => {
|
||||
// verify required parameter 'timestamp' is not null or undefined
|
||||
assertParamExists('getMemoryLane', 'timestamp', timestamp)
|
||||
getMemoryLane: async (day: number, month: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// 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<Array<MemoryLaneResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options);
|
||||
async getMemoryLane(day: number, month: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
|
||||
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<Array<MemoryLaneResponseDto>> {
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,9 +22,9 @@ class MemoryService {
|
|||
Future<List<Memory>?> 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) {
|
||||
|
|
10
mobile/openapi/doc/AssetApi.md
generated
10
mobile/openapi/doc/AssetApi.md
generated
|
@ -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<MemoryLaneResponseDto> getMemoryLane(timestamp)
|
||||
> List<MemoryLaneResponseDto> getMemoryLane(day, month)
|
||||
|
||||
|
||||
|
||||
|
@ -986,10 +986,11 @@ import 'package:openapi/api.dart';
|
|||
//defaultApiClient.getAuthentication<HttpBearerAuth>('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
|
||||
|
||||
|
|
1
mobile/openapi/doc/AssetResponseDto.md
generated
1
mobile/openapi/doc/AssetResponseDto.md
generated
|
@ -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]
|
||||
|
|
19
mobile/openapi/lib/api/asset_api.dart
generated
19
mobile/openapi/lib/api/asset_api.dart
generated
|
@ -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<Response> getMemoryLaneWithHttpInfo(DateTime timestamp,) async {
|
||||
/// * [int] day (required):
|
||||
///
|
||||
/// * [int] month (required):
|
||||
Future<Response> getMemoryLaneWithHttpInfo(int day, int month,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/asset/memory-lane';
|
||||
|
||||
|
@ -997,7 +998,8 @@ class AssetApi {
|
|||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
queryParams.addAll(_queryParams('', 'timestamp', timestamp));
|
||||
queryParams.addAll(_queryParams('', 'day', day));
|
||||
queryParams.addAll(_queryParams('', 'month', month));
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
@ -1015,10 +1017,11 @@ class AssetApi {
|
|||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DateTime] timestamp (required):
|
||||
/// Get pictures for +24 hours from this time going back x years
|
||||
Future<List<MemoryLaneResponseDto>?> getMemoryLane(DateTime timestamp,) async {
|
||||
final response = await getMemoryLaneWithHttpInfo(timestamp,);
|
||||
/// * [int] day (required):
|
||||
///
|
||||
/// * [int] month (required):
|
||||
Future<List<MemoryLaneResponseDto>?> getMemoryLane(int day, int month,) async {
|
||||
final response = await getMemoryLaneWithHttpInfo(day, month,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
|
|
10
mobile/openapi/lib/model/asset_response_dto.dart
generated
10
mobile/openapi/lib/model/asset_response_dto.dart
generated
|
@ -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<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
|
@ -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<bool>(json, r'isReadOnly')!,
|
||||
libraryId: mapValueOfType<String>(json, r'libraryId')!,
|
||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||
localDateTime: mapDateTime(json, r'localDateTime', '')!,
|
||||
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
|
||||
originalPath: mapValueOfType<String>(json, r'originalPath')!,
|
||||
owner: UserResponseDto.fromJson(json[r'owner']),
|
||||
|
@ -320,6 +327,7 @@ class AssetResponseDto {
|
|||
'isOffline',
|
||||
'isReadOnly',
|
||||
'libraryId',
|
||||
'localDateTime',
|
||||
'originalFileName',
|
||||
'originalPath',
|
||||
'ownerId',
|
||||
|
|
2
mobile/openapi/test/asset_api_test.dart
generated
2
mobile/openapi/test/asset_api_test.dart
generated
|
@ -107,7 +107,7 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<MemoryLaneResponseDto>> getMemoryLane(DateTime timestamp) async
|
||||
//Future<List<MemoryLaneResponseDto>> getMemoryLane(int day, int month) async
|
||||
test('test getMemoryLane', () async {
|
||||
// TODO
|
||||
});
|
||||
|
|
5
mobile/openapi/test/asset_response_dto_test.dart
generated
5
mobile/openapi/test/asset_response_dto_test.dart
generated
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<AssetEntity>;
|
||||
|
||||
export interface MonthDay {
|
||||
day: number;
|
||||
month: number;
|
||||
}
|
||||
|
||||
export const IAssetRepository = 'IAssetRepository';
|
||||
|
||||
export interface IAssetRepository {
|
||||
create(asset: Partial<AssetEntity>): Promise<AssetEntity>;
|
||||
create(asset: AssetCreate): Promise<AssetEntity>;
|
||||
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]>;
|
||||
getByIds(ids: string[]): Promise<AssetEntity[]>;
|
||||
getByDayOfYear(ownerId: string, monthDay: MonthDay): Promise<AssetEntity[]>;
|
||||
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null>;
|
||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
||||
getByUserId(pagination: PaginationOptions, userId: string): Paginated<AssetEntity>;
|
||||
|
@ -87,7 +109,7 @@ export interface IAssetRepository {
|
|||
deleteAll(ownerId: string): Promise<void>;
|
||||
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void>;
|
||||
save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
|
||||
save(asset: Pick<AssetEntity, 'id'> & Partial<AssetEntity>): Promise<AssetEntity>;
|
||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
|
||||
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
||||
|
|
|
@ -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 }]]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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<MemoryLaneResponseDto[]> {
|
||||
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<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)),
|
||||
};
|
||||
};
|
||||
return _.chain(assets)
|
||||
.filter((asset) => asset.localDateTime.getFullYear() < currentYear)
|
||||
.map((asset) => {
|
||||
const years = currentYear - asset.localDateTime.getFullYear();
|
||||
|
||||
const requests: Promise<MemoryLaneResponseDto>[] = [];
|
||||
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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 = <T>(value: T): NonNullable<T> | 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`),
|
||||
|
|
|
@ -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<AssetEntity | null>;
|
||||
create(asset: AssetCreate): Promise<AssetEntity>;
|
||||
|
|
|
@ -29,6 +29,7 @@ export class AssetCore {
|
|||
|
||||
fileCreatedAt: dto.fileCreatedAt,
|
||||
fileModifiedAt: dto.fileModifiedAt,
|
||||
localDateTime: dto.fileCreatedAt,
|
||||
|
||||
type: mimeTypes.assetType(file.originalPath),
|
||||
isFavorite: dto.isFavorite,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddLocalDateTime1694525143117 implements MigrationInterface {
|
||||
name = 'AddLocalDateTime1694525143117';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
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"`);
|
||||
}
|
||||
}
|
|
@ -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<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
|
||||
): Promise<AssetEntity> {
|
||||
create(asset: AssetCreate): Promise<AssetEntity> {
|
||||
return this.repository.save(asset);
|
||||
}
|
||||
|
||||
|
@ -78,6 +78,26 @@ export class AssetRepository implements IAssetRepository {
|
|||
});
|
||||
}
|
||||
|
||||
getByDayOfYear(ownerId: string, { day, month }: MonthDay): Promise<AssetEntity[]> {
|
||||
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<AssetEntity[]> {
|
||||
return this.repository.find({
|
||||
where: { id: In(ids) },
|
||||
|
@ -454,8 +474,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ const createAsset = (
|
|||
createdAt: Date,
|
||||
): Promise<AssetEntity> => {
|
||||
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}`,
|
||||
});
|
||||
|
|
48
server/test/fixtures/asset.stub.ts
vendored
48
server/test/fixtures/asset.stub.ts
vendored
|
@ -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<AssetEntity>({
|
||||
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,
|
||||
|
|
2
server/test/fixtures/shared-link.stub.ts
vendored
2
server/test/fixtures/shared-link.stub.ts
vendored
|
@ -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,
|
||||
|
|
|
@ -5,6 +5,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
|||
create: jest.fn(),
|
||||
upsertExif: jest.fn(),
|
||||
getByDate: jest.fn(),
|
||||
getByDayOfYear: jest.fn(),
|
||||
getByIds: jest.fn().mockResolvedValue([]),
|
||||
getByAlbumId: jest.fn(),
|
||||
getByUserId: jest.fn(),
|
||||
|
|
51
web/src/api/open-api/api.ts
generated
51
web/src/api/open-api/api.ts
generated
|
@ -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<RequestArgs> => {
|
||||
// verify required parameter 'timestamp' is not null or undefined
|
||||
assertParamExists('getMemoryLane', 'timestamp', timestamp)
|
||||
getMemoryLane: async (day: number, month: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// 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<Array<MemoryLaneResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options);
|
||||
async getMemoryLane(day: number, month: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
|
||||
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<Array<MemoryLaneResponseDto>> {
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 @@
|
|||
|
||||
<div class="absolute left-8 top-4 text-sm font-medium text-white">
|
||||
<p>
|
||||
{DateTime.fromISO(currentMemory.assets[0].fileCreatedAt).toLocaleString(DateTime.DATE_FULL)}
|
||||
{DateTime.fromISO(currentMemory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)}
|
||||
</p>
|
||||
<p>
|
||||
{currentAsset.exifInfo?.city || ''}
|
||||
|
|
|
@ -126,7 +126,8 @@
|
|||
|
||||
<section id="asset-group-by-date" class="flex flex-wrap gap-x-12" bind:clientHeight={actualBucketHeight}>
|
||||
{#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'))}
|
||||
<!-- Asset Group By Date -->
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { DateTime } from 'luxon';
|
||||
import { api } from '@api';
|
||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||
|
@ -11,8 +10,10 @@
|
|||
$: shouldRender = $memoryStore?.length > 0;
|
||||
|
||||
onMount(async () => {
|
||||
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;
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ export function splitBucketIntoDateGroups(
|
|||
): AssetResponseDto[][] {
|
||||
return lodash
|
||||
.chain(assets)
|
||||
.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString(locale, groupDateFormat))
|
||||
.groupBy((asset) => new Date(asset.localDateTime).toLocaleDateString(locale, groupDateFormat))
|
||||
.sortBy((group) => assets.indexOf(group[0]))
|
||||
.value();
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue