mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04: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
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
|
* @memberof AssetResponseDto
|
||||||
*/
|
*/
|
||||||
'livePhotoVideoId'?: string | null;
|
'livePhotoVideoId'?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetResponseDto
|
||||||
|
*/
|
||||||
|
'localDateTime': string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {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.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMemoryLane: async (timestamp: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getMemoryLane: async (day: number, month: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'timestamp' is not null or undefined
|
// verify required parameter 'day' is not null or undefined
|
||||||
assertParamExists('getMemoryLane', 'timestamp', timestamp)
|
assertParamExists('getMemoryLane', 'day', day)
|
||||||
|
// verify required parameter 'month' is not null or undefined
|
||||||
|
assertParamExists('getMemoryLane', 'month', month)
|
||||||
const localVarPath = `/asset/memory-lane`;
|
const localVarPath = `/asset/memory-lane`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
@ -6368,10 +6377,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
// http bearer authentication required
|
// http bearer authentication required
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
if (timestamp !== undefined) {
|
if (day !== undefined) {
|
||||||
localVarQueryParameter['timestamp'] = (timestamp as any instanceof Date) ?
|
localVarQueryParameter['day'] = day;
|
||||||
(timestamp as any).toISOString() :
|
}
|
||||||
timestamp;
|
|
||||||
|
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.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getMemoryLane(timestamp: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
|
async getMemoryLane(day: number, month: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(day, month, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -7443,7 +7455,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig): AxiosPromise<Array<MemoryLaneResponseDto>> {
|
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 {
|
export interface AssetApiGetMemoryLaneRequest {
|
||||||
/**
|
/**
|
||||||
* Get pictures for +24 hours from this time going back x years
|
*
|
||||||
* @type {string}
|
* @type {number}
|
||||||
* @memberof AssetApiGetMemoryLane
|
* @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
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig) {
|
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 {
|
Future<List<Memory>?> getMemoryLane() async {
|
||||||
try {
|
try {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final beginningOfDate = DateTime(now.year, now.month, now.day);
|
|
||||||
final data = await _apiService.assetApi.getMemoryLane(
|
final data = await _apiService.assetApi.getMemoryLane(
|
||||||
beginningOfDate,
|
now.day,
|
||||||
|
now.month,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (data == null) {
|
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)
|
[[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**
|
# **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);
|
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||||
|
|
||||||
final api_instance = AssetApi();
|
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 {
|
try {
|
||||||
final result = api_instance.getMemoryLane(timestamp);
|
final result = api_instance.getMemoryLane(day, month);
|
||||||
print(result);
|
print(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Exception when calling AssetApi->getMemoryLane: $e\n');
|
print('Exception when calling AssetApi->getMemoryLane: $e\n');
|
||||||
@ -1000,7 +1001,8 @@ try {
|
|||||||
|
|
||||||
Name | Type | Description | Notes
|
Name | Type | Description | Notes
|
||||||
------------- | ------------- | ------------- | -------------
|
------------- | ------------- | ------------- | -------------
|
||||||
**timestamp** | **DateTime**| Get pictures for +24 hours from this time going back x years |
|
**day** | **int**| |
|
||||||
|
**month** | **int**| |
|
||||||
|
|
||||||
### Return type
|
### 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** | |
|
**isReadOnly** | **bool** | |
|
||||||
**libraryId** | **String** | |
|
**libraryId** | **String** | |
|
||||||
**livePhotoVideoId** | **String** | | [optional]
|
**livePhotoVideoId** | **String** | | [optional]
|
||||||
|
**localDateTime** | [**DateTime**](DateTime.md) | |
|
||||||
**originalFileName** | **String** | |
|
**originalFileName** | **String** | |
|
||||||
**originalPath** | **String** | |
|
**originalPath** | **String** | |
|
||||||
**owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional]
|
**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].
|
/// Performs an HTTP 'GET /asset/memory-lane' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [DateTime] timestamp (required):
|
/// * [int] day (required):
|
||||||
/// Get pictures for +24 hours from this time going back x years
|
///
|
||||||
Future<Response> getMemoryLaneWithHttpInfo(DateTime timestamp,) async {
|
/// * [int] month (required):
|
||||||
|
Future<Response> getMemoryLaneWithHttpInfo(int day, int month,) async {
|
||||||
// ignore: prefer_const_declarations
|
// ignore: prefer_const_declarations
|
||||||
final path = r'/asset/memory-lane';
|
final path = r'/asset/memory-lane';
|
||||||
|
|
||||||
@ -997,7 +998,8 @@ class AssetApi {
|
|||||||
final headerParams = <String, String>{};
|
final headerParams = <String, String>{};
|
||||||
final formParams = <String, String>{};
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
queryParams.addAll(_queryParams('', 'timestamp', timestamp));
|
queryParams.addAll(_queryParams('', 'day', day));
|
||||||
|
queryParams.addAll(_queryParams('', 'month', month));
|
||||||
|
|
||||||
const contentTypes = <String>[];
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
@ -1015,10 +1017,11 @@ class AssetApi {
|
|||||||
|
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
/// * [DateTime] timestamp (required):
|
/// * [int] day (required):
|
||||||
/// Get pictures for +24 hours from this time going back x years
|
///
|
||||||
Future<List<MemoryLaneResponseDto>?> getMemoryLane(DateTime timestamp,) async {
|
/// * [int] month (required):
|
||||||
final response = await getMemoryLaneWithHttpInfo(timestamp,);
|
Future<List<MemoryLaneResponseDto>?> getMemoryLane(int day, int month,) async {
|
||||||
|
final response = await getMemoryLaneWithHttpInfo(day, month,);
|
||||||
if (response.statusCode >= HttpStatus.badRequest) {
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
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.isReadOnly,
|
||||||
required this.libraryId,
|
required this.libraryId,
|
||||||
this.livePhotoVideoId,
|
this.livePhotoVideoId,
|
||||||
|
required this.localDateTime,
|
||||||
required this.originalFileName,
|
required this.originalFileName,
|
||||||
required this.originalPath,
|
required this.originalPath,
|
||||||
this.owner,
|
this.owner,
|
||||||
@ -78,6 +79,8 @@ class AssetResponseDto {
|
|||||||
|
|
||||||
String? livePhotoVideoId;
|
String? livePhotoVideoId;
|
||||||
|
|
||||||
|
DateTime localDateTime;
|
||||||
|
|
||||||
String originalFileName;
|
String originalFileName;
|
||||||
|
|
||||||
String originalPath;
|
String originalPath;
|
||||||
@ -130,6 +133,7 @@ class AssetResponseDto {
|
|||||||
other.isReadOnly == isReadOnly &&
|
other.isReadOnly == isReadOnly &&
|
||||||
other.libraryId == libraryId &&
|
other.libraryId == libraryId &&
|
||||||
other.livePhotoVideoId == livePhotoVideoId &&
|
other.livePhotoVideoId == livePhotoVideoId &&
|
||||||
|
other.localDateTime == localDateTime &&
|
||||||
other.originalFileName == originalFileName &&
|
other.originalFileName == originalFileName &&
|
||||||
other.originalPath == originalPath &&
|
other.originalPath == originalPath &&
|
||||||
other.owner == owner &&
|
other.owner == owner &&
|
||||||
@ -160,6 +164,7 @@ class AssetResponseDto {
|
|||||||
(isReadOnly.hashCode) +
|
(isReadOnly.hashCode) +
|
||||||
(libraryId.hashCode) +
|
(libraryId.hashCode) +
|
||||||
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||||
|
(localDateTime.hashCode) +
|
||||||
(originalFileName.hashCode) +
|
(originalFileName.hashCode) +
|
||||||
(originalPath.hashCode) +
|
(originalPath.hashCode) +
|
||||||
(owner == null ? 0 : owner!.hashCode) +
|
(owner == null ? 0 : owner!.hashCode) +
|
||||||
@ -173,7 +178,7 @@ class AssetResponseDto {
|
|||||||
(updatedAt.hashCode);
|
(updatedAt.hashCode);
|
||||||
|
|
||||||
@override
|
@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() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -200,6 +205,7 @@ class AssetResponseDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'livePhotoVideoId'] = null;
|
// json[r'livePhotoVideoId'] = null;
|
||||||
}
|
}
|
||||||
|
json[r'localDateTime'] = this.localDateTime.toUtc().toIso8601String();
|
||||||
json[r'originalFileName'] = this.originalFileName;
|
json[r'originalFileName'] = this.originalFileName;
|
||||||
json[r'originalPath'] = this.originalPath;
|
json[r'originalPath'] = this.originalPath;
|
||||||
if (this.owner != null) {
|
if (this.owner != null) {
|
||||||
@ -249,6 +255,7 @@ class AssetResponseDto {
|
|||||||
isReadOnly: mapValueOfType<bool>(json, r'isReadOnly')!,
|
isReadOnly: mapValueOfType<bool>(json, r'isReadOnly')!,
|
||||||
libraryId: mapValueOfType<String>(json, r'libraryId')!,
|
libraryId: mapValueOfType<String>(json, r'libraryId')!,
|
||||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||||
|
localDateTime: mapDateTime(json, r'localDateTime', '')!,
|
||||||
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
|
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
|
||||||
originalPath: mapValueOfType<String>(json, r'originalPath')!,
|
originalPath: mapValueOfType<String>(json, r'originalPath')!,
|
||||||
owner: UserResponseDto.fromJson(json[r'owner']),
|
owner: UserResponseDto.fromJson(json[r'owner']),
|
||||||
@ -320,6 +327,7 @@ class AssetResponseDto {
|
|||||||
'isOffline',
|
'isOffline',
|
||||||
'isReadOnly',
|
'isReadOnly',
|
||||||
'libraryId',
|
'libraryId',
|
||||||
|
'localDateTime',
|
||||||
'originalFileName',
|
'originalFileName',
|
||||||
'originalPath',
|
'originalPath',
|
||||||
'ownerId',
|
'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
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
//Future<List<MemoryLaneResponseDto>> getMemoryLane(DateTime timestamp) async
|
//Future<List<MemoryLaneResponseDto>> getMemoryLane(int day, int month) async
|
||||||
test('test getMemoryLane', () async {
|
test('test getMemoryLane', () async {
|
||||||
// TODO
|
// 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
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// DateTime localDateTime
|
||||||
|
test('to test the property `localDateTime`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// String originalFileName
|
// String originalFileName
|
||||||
test('to test the property `originalFileName`', () async {
|
test('to test the property `originalFileName`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
@ -1477,13 +1477,19 @@
|
|||||||
"operationId": "getMemoryLane",
|
"operationId": "getMemoryLane",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "timestamp",
|
"name": "day",
|
||||||
"required": true,
|
"required": true,
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "Get pictures for +24 hours from this time going back x years",
|
|
||||||
"schema": {
|
"schema": {
|
||||||
"format": "date-time",
|
"type": "integer"
|
||||||
"type": "string"
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "month",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -5617,6 +5623,10 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"localDateTime": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"originalFileName": {
|
"originalFileName": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -5676,6 +5686,7 @@
|
|||||||
"updatedAt",
|
"updatedAt",
|
||||||
"isFavorite",
|
"isFavorite",
|
||||||
"isArchived",
|
"isArchived",
|
||||||
|
"localDateTime",
|
||||||
"isOffline",
|
"isOffline",
|
||||||
"isExternal",
|
"isExternal",
|
||||||
"isReadOnly",
|
"isReadOnly",
|
||||||
|
@ -68,12 +68,34 @@ export interface TimeBucketItem {
|
|||||||
count: number;
|
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 const IAssetRepository = 'IAssetRepository';
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
create(asset: Partial<AssetEntity>): Promise<AssetEntity>;
|
create(asset: AssetCreate): Promise<AssetEntity>;
|
||||||
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]>;
|
getByDate(ownerId: string, date: Date): Promise<AssetEntity[]>;
|
||||||
getByIds(ids: string[]): Promise<AssetEntity[]>;
|
getByIds(ids: string[]): Promise<AssetEntity[]>;
|
||||||
|
getByDayOfYear(ownerId: string, monthDay: MonthDay): Promise<AssetEntity[]>;
|
||||||
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null>;
|
getByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity | null>;
|
||||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
||||||
getByUserId(pagination: PaginationOptions, userId: string): Paginated<AssetEntity>;
|
getByUserId(pagination: PaginationOptions, userId: string): Paginated<AssetEntity>;
|
||||||
@ -87,7 +109,7 @@ export interface IAssetRepository {
|
|||||||
deleteAll(ownerId: string): Promise<void>;
|
deleteAll(ownerId: string): Promise<void>;
|
||||||
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||||
updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void>;
|
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>;
|
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
|
||||||
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||||
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
||||||
|
@ -274,60 +274,24 @@ describe(AssetService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getMemoryLane', () => {
|
describe('getMemoryLane', () => {
|
||||||
it('should get pictures for each year', async () => {
|
beforeAll(() => {
|
||||||
assetMock.getByDate.mockResolvedValue([]);
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(new Date('2024-01-15'));
|
||||||
await expect(sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15), years: 10 })).resolves.toEqual(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(assetMock.getByDate).toHaveBeenCalledTimes(10);
|
|
||||||
expect(assetMock.getByDate.mock.calls).toEqual([
|
|
||||||
[authStub.admin.id, new Date('2022-06-15T00:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2021-06-15T00:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2020-06-15T00:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2019-06-15T00:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2018-06-15T00:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2017-06-15T00:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2016-06-15T00:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2015-06-15T00:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2014-06-15T00:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2013-06-15T00:00:00.000Z')],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should keep hours from the date', async () => {
|
afterAll(() => {
|
||||||
assetMock.getByDate.mockResolvedValue([]);
|
jest.useRealTimers();
|
||||||
|
|
||||||
await expect(
|
|
||||||
sut.getMemoryLane(authStub.admin, { timestamp: new Date(2023, 5, 15, 5), years: 2 }),
|
|
||||||
).resolves.toEqual([]);
|
|
||||||
|
|
||||||
expect(assetMock.getByDate).toHaveBeenCalledTimes(2);
|
|
||||||
expect(assetMock.getByDate.mock.calls).toEqual([
|
|
||||||
[authStub.admin.id, new Date('2022-06-15T05:00:00.000Z')],
|
|
||||||
[authStub.admin.id, new Date('2021-06-15T05:00:00.000Z')],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the title correctly', async () => {
|
it('should set the title correctly', async () => {
|
||||||
when(assetMock.getByDate)
|
assetMock.getByDayOfYear.mockResolvedValue([assetStub.image, assetStub.imageFrom2015]);
|
||||||
.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]);
|
|
||||||
|
|
||||||
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: '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.getByDayOfYear.mock.calls).toEqual([[authStub.admin.id, { day: 15, month: 1 }]]);
|
||||||
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')],
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { AssetEntity } from '@app/infra/entities';
|
import { AssetEntity } from '@app/infra/entities';
|
||||||
import { BadRequestException, Inject, Logger } from '@nestjs/common';
|
import { BadRequestException, Inject, Logger } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import _ from 'lodash';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import { AccessCore, IAccessRepository, Permission } from '../access';
|
import { AccessCore, IAccessRepository, Permission } from '../access';
|
||||||
@ -138,22 +138,22 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getMemoryLane(authUser: AuthUserDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
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> => {
|
return _.chain(assets)
|
||||||
const assets = await this.assetRepository.getByDate(authUser.id, target.minus({ years: yearsAgo }).toJSDate());
|
.filter((asset) => asset.localDateTime.getFullYear() < currentYear)
|
||||||
return {
|
.map((asset) => {
|
||||||
title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`,
|
const years = currentYear - asset.localDateTime.getFullYear();
|
||||||
assets: assets.map((a) => mapAsset(a)),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const requests: Promise<MemoryLaneResponseDto>[] = [];
|
return {
|
||||||
for (let i = 1; i <= dto.years; i++) {
|
title: `${years} year${years > 1 ? 's' : ''} since...`,
|
||||||
requests.push(onRequest(i));
|
asset: mapAsset(asset),
|
||||||
}
|
};
|
||||||
|
})
|
||||||
return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0));
|
.groupBy((asset) => asset.title)
|
||||||
|
.map((items, title) => ({ title, assets: items.map(({ asset }) => asset) }))
|
||||||
|
.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async timeBucketChecks(authUser: AuthUserDto, dto: TimeBucketDto) {
|
private async timeBucketChecks(authUser: AuthUserDto, dto: TimeBucketDto) {
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsDate, IsNumber, IsPositive } from 'class-validator';
|
import { IsInt, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
export class MemoryLaneDto {
|
export class MemoryLaneDto {
|
||||||
/** Get pictures for +24 hours from this time going back x years */
|
@IsInt()
|
||||||
@IsDate()
|
|
||||||
@Type(() => Date)
|
|
||||||
timestamp!: Date;
|
|
||||||
|
|
||||||
@IsNumber()
|
|
||||||
@IsPositive()
|
|
||||||
@Type(() => Number)
|
@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;
|
updatedAt!: Date;
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
isArchived!: boolean;
|
isArchived!: boolean;
|
||||||
|
localDateTime!: Date;
|
||||||
isOffline!: boolean;
|
isOffline!: boolean;
|
||||||
isExternal!: boolean;
|
isExternal!: boolean;
|
||||||
isReadOnly!: boolean;
|
isReadOnly!: boolean;
|
||||||
@ -54,6 +55,7 @@ function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto {
|
|||||||
thumbhash: entity.thumbhash?.toString('base64') ?? null,
|
thumbhash: entity.thumbhash?.toString('base64') ?? null,
|
||||||
fileCreatedAt: entity.fileCreatedAt,
|
fileCreatedAt: entity.fileCreatedAt,
|
||||||
fileModifiedAt: entity.fileModifiedAt,
|
fileModifiedAt: entity.fileModifiedAt,
|
||||||
|
localDateTime: entity.localDateTime,
|
||||||
updatedAt: entity.updatedAt,
|
updatedAt: entity.updatedAt,
|
||||||
isFavorite: entity.isFavorite,
|
isFavorite: entity.isFavorite,
|
||||||
isArchived: entity.isArchived,
|
isArchived: entity.isArchived,
|
||||||
|
@ -217,6 +217,7 @@ describe(LibraryService.name, () => {
|
|||||||
deviceId: 'Library Import',
|
deviceId: 'Library Import',
|
||||||
fileCreatedAt: expect.any(Date),
|
fileCreatedAt: expect.any(Date),
|
||||||
fileModifiedAt: expect.any(Date),
|
fileModifiedAt: expect.any(Date),
|
||||||
|
localDateTime: expect.any(Date),
|
||||||
type: AssetType.IMAGE,
|
type: AssetType.IMAGE,
|
||||||
originalFileName: 'photo',
|
originalFileName: 'photo',
|
||||||
sidecarPath: null,
|
sidecarPath: null,
|
||||||
@ -264,6 +265,7 @@ describe(LibraryService.name, () => {
|
|||||||
deviceId: 'Library Import',
|
deviceId: 'Library Import',
|
||||||
fileCreatedAt: expect.any(Date),
|
fileCreatedAt: expect.any(Date),
|
||||||
fileModifiedAt: expect.any(Date),
|
fileModifiedAt: expect.any(Date),
|
||||||
|
localDateTime: expect.any(Date),
|
||||||
type: AssetType.IMAGE,
|
type: AssetType.IMAGE,
|
||||||
originalFileName: 'photo',
|
originalFileName: 'photo',
|
||||||
sidecarPath: '/data/user1/photo.jpg.xmp',
|
sidecarPath: '/data/user1/photo.jpg.xmp',
|
||||||
@ -310,6 +312,7 @@ describe(LibraryService.name, () => {
|
|||||||
deviceId: 'Library Import',
|
deviceId: 'Library Import',
|
||||||
fileCreatedAt: expect.any(Date),
|
fileCreatedAt: expect.any(Date),
|
||||||
fileModifiedAt: expect.any(Date),
|
fileModifiedAt: expect.any(Date),
|
||||||
|
localDateTime: expect.any(Date),
|
||||||
type: AssetType.VIDEO,
|
type: AssetType.VIDEO,
|
||||||
originalFileName: 'video',
|
originalFileName: 'video',
|
||||||
sidecarPath: null,
|
sidecarPath: null,
|
||||||
|
@ -251,6 +251,7 @@ export class LibraryService {
|
|||||||
deviceId: 'Library Import',
|
deviceId: 'Library Import',
|
||||||
fileCreatedAt: stats.mtime,
|
fileCreatedAt: stats.mtime,
|
||||||
fileModifiedAt: stats.mtime,
|
fileModifiedAt: stats.mtime,
|
||||||
|
localDateTime: stats.mtime,
|
||||||
type: assetType,
|
type: assetType,
|
||||||
originalFileName: parse(assetPath).name,
|
originalFileName: parse(assetPath).name,
|
||||||
sidecarPath,
|
sidecarPath,
|
||||||
|
@ -231,6 +231,7 @@ describe(MetadataService.name, () => {
|
|||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: assetStub.image.createdAt,
|
fileCreatedAt: assetStub.image.createdAt,
|
||||||
|
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -252,6 +253,7 @@ describe(MetadataService.name, () => {
|
|||||||
id: assetStub.withLocation.id,
|
id: assetStub.withLocation.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: assetStub.withLocation.createdAt,
|
fileCreatedAt: assetStub.withLocation.createdAt,
|
||||||
|
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -299,16 +301,13 @@ describe(MetadataService.name, () => {
|
|||||||
const video = randomBytes(512);
|
const video = randomBytes(512);
|
||||||
storageMock.readFile.mockResolvedValue(video);
|
storageMock.readFile.mockResolvedValue(video);
|
||||||
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
cryptoRepository.hashSha1.mockReturnValue(randomBytes(512));
|
||||||
|
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||||
assetMock.save.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
assetMock.save.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||||
|
|
||||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||||
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id]);
|
||||||
expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object));
|
expect(storageMock.readFile).toHaveBeenCalledWith(assetStub.livePhotoStillAsset.originalPath, expect.any(Object));
|
||||||
expect(assetMock.save).toHaveBeenCalledWith({
|
expect(assetMock.create).toHaveBeenCalledWith(
|
||||||
id: assetStub.livePhotoStillAsset.id,
|
|
||||||
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
|
||||||
});
|
|
||||||
expect(assetMock.save).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
type: AssetType.VIDEO,
|
type: AssetType.VIDEO,
|
||||||
originalFileName: assetStub.livePhotoStillAsset.originalFileName,
|
originalFileName: assetStub.livePhotoStillAsset.originalFileName,
|
||||||
@ -316,6 +315,10 @@ describe(MetadataService.name, () => {
|
|||||||
isReadOnly: true,
|
isReadOnly: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
expect(assetMock.save).toHaveBeenCalledWith({
|
||||||
|
id: assetStub.livePhotoStillAsset.id,
|
||||||
|
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
|
||||||
|
});
|
||||||
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video);
|
||||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||||
name: JobName.METADATA_EXTRACTION,
|
name: JobName.METADATA_EXTRACTION,
|
||||||
@ -379,6 +382,7 @@ describe(MetadataService.name, () => {
|
|||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: new Date('1970-01-01'),
|
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 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 => {
|
const validate = <T>(value: T): NonNullable<T> | null => {
|
||||||
// handle lists of numbers
|
// handle lists of numbers
|
||||||
@ -156,9 +157,18 @@ export class MetadataService {
|
|||||||
await this.applyMotionPhotos(asset, tags);
|
await this.applyMotionPhotos(asset, tags);
|
||||||
await this.applyReverseGeocoding(asset, exifData);
|
await this.applyReverseGeocoding(asset, exifData);
|
||||||
await this.assetRepository.upsertExif(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({
|
await this.assetRepository.save({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
duration: tags.Duration ? this.getDuration(tags.Duration) : null,
|
duration: tags.Duration ? this.getDuration(tags.Duration) : null,
|
||||||
|
localDateTime,
|
||||||
fileCreatedAt: exifData.dateTimeOriginal ?? undefined,
|
fileCreatedAt: exifData.dateTimeOriginal ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -268,11 +278,13 @@ export class MetadataService {
|
|||||||
|
|
||||||
let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
|
let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
|
||||||
if (!motionAsset) {
|
if (!motionAsset) {
|
||||||
motionAsset = await this.assetRepository.save({
|
const createdAt = asset.fileCreatedAt ?? asset.createdAt;
|
||||||
|
motionAsset = await this.assetRepository.create({
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
type: AssetType.VIDEO,
|
type: AssetType.VIDEO,
|
||||||
fileCreatedAt: asset.fileCreatedAt ?? asset.createdAt,
|
fileCreatedAt: createdAt,
|
||||||
fileModifiedAt: asset.fileModifiedAt,
|
fileModifiedAt: asset.fileModifiedAt,
|
||||||
|
localDateTime: createdAt,
|
||||||
checksum,
|
checksum,
|
||||||
ownerId: asset.ownerId,
|
ownerId: asset.ownerId,
|
||||||
originalPath: this.storageCore.ensurePath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}-MP.mp4`),
|
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 { AssetEntity } from '@app/infra/entities';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
@ -19,11 +20,6 @@ export interface AssetOwnerCheck extends AssetCheck {
|
|||||||
ownerId: string;
|
ownerId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AssetCreate = Omit<
|
|
||||||
AssetEntity,
|
|
||||||
'id' | 'createdAt' | 'updatedAt' | 'owner' | 'livePhotoVideoId' | 'library'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
get(id: string): Promise<AssetEntity | null>;
|
get(id: string): Promise<AssetEntity | null>;
|
||||||
create(asset: AssetCreate): Promise<AssetEntity>;
|
create(asset: AssetCreate): Promise<AssetEntity>;
|
||||||
|
@ -29,6 +29,7 @@ export class AssetCore {
|
|||||||
|
|
||||||
fileCreatedAt: dto.fileCreatedAt,
|
fileCreatedAt: dto.fileCreatedAt,
|
||||||
fileModifiedAt: dto.fileModifiedAt,
|
fileModifiedAt: dto.fileModifiedAt,
|
||||||
|
localDateTime: dto.fileCreatedAt,
|
||||||
|
|
||||||
type: mimeTypes.assetType(file.originalPath),
|
type: mimeTypes.assetType(file.originalPath),
|
||||||
isFavorite: dto.isFavorite,
|
isFavorite: dto.isFavorite,
|
||||||
|
@ -28,6 +28,8 @@ export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum';
|
|||||||
@Index(ASSET_CHECKSUM_CONSTRAINT, ['owner', 'library', 'checksum'], {
|
@Index(ASSET_CHECKSUM_CONSTRAINT, ['owner', 'library', 'checksum'], {
|
||||||
unique: true,
|
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
|
// For all assets, each originalpath must be unique per user and library
|
||||||
export class AssetEntity {
|
export class AssetEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
@ -78,6 +80,9 @@ export class AssetEntity {
|
|||||||
@Column({ type: 'timestamptz' })
|
@Column({ type: 'timestamptz' })
|
||||||
fileCreatedAt!: Date;
|
fileCreatedAt!: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp' })
|
||||||
|
localDateTime!: Date;
|
||||||
|
|
||||||
@Column({ type: 'timestamptz' })
|
@Column({ type: 'timestamptz' })
|
||||||
fileModifiedAt!: Date;
|
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 {
|
import {
|
||||||
|
AssetCreate,
|
||||||
AssetSearchOptions,
|
AssetSearchOptions,
|
||||||
AssetStats,
|
AssetStats,
|
||||||
AssetStatsOptions,
|
AssetStatsOptions,
|
||||||
@ -6,6 +7,7 @@ import {
|
|||||||
LivePhotoSearchOptions,
|
LivePhotoSearchOptions,
|
||||||
MapMarker,
|
MapMarker,
|
||||||
MapMarkerSearchOptions,
|
MapMarkerSearchOptions,
|
||||||
|
MonthDay,
|
||||||
Paginated,
|
Paginated,
|
||||||
PaginationOptions,
|
PaginationOptions,
|
||||||
TimeBucketItem,
|
TimeBucketItem,
|
||||||
@ -38,9 +40,7 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] });
|
await this.exifRepository.upsert(exif, { conflictPaths: ['assetId'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
create(
|
create(asset: AssetCreate): Promise<AssetEntity> {
|
||||||
asset: Omit<AssetEntity, 'id' | 'createdAt' | 'updatedAt' | 'ownerId' | 'livePhotoVideoId'>,
|
|
||||||
): Promise<AssetEntity> {
|
|
||||||
return this.repository.save(asset);
|
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[]> {
|
getByIds(ids: string[]): Promise<AssetEntity[]> {
|
||||||
return this.repository.find({
|
return this.repository.find({
|
||||||
where: { id: In(ids) },
|
where: { id: In(ids) },
|
||||||
@ -454,8 +474,9 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
|
getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
|
||||||
const truncateValue = truncateMap[options.size];
|
const truncateValue = truncateMap[options.size];
|
||||||
return this.getBuilder(options)
|
return this.getBuilder(options)
|
||||||
.andWhere(`date_trunc('${truncateValue}', "fileCreatedAt") = :timeBucket`, { timeBucket })
|
.andWhere(`date_trunc('${truncateValue}', "localDateTime") = :timeBucket`, { timeBucket })
|
||||||
.orderBy('asset.fileCreatedAt', 'DESC')
|
.orderBy(`date_trunc('day', "localDateTime")`, 'DESC')
|
||||||
|
.addOrderBy('asset.fileCreatedAt', 'DESC')
|
||||||
.getMany();
|
.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ const createAsset = (
|
|||||||
createdAt: Date,
|
createdAt: Date,
|
||||||
): Promise<AssetEntity> => {
|
): Promise<AssetEntity> => {
|
||||||
const id = assetCount++;
|
const id = assetCount++;
|
||||||
return repository.save({
|
return repository.create({
|
||||||
ownerId: loginResponse.userId,
|
ownerId: loginResponse.userId,
|
||||||
checksum: randomBytes(20),
|
checksum: randomBytes(20),
|
||||||
originalPath: `/tests/test_${id}`,
|
originalPath: `/tests/test_${id}`,
|
||||||
@ -66,6 +66,7 @@ const createAsset = (
|
|||||||
isVisible: true,
|
isVisible: true,
|
||||||
fileCreatedAt: createdAt,
|
fileCreatedAt: createdAt,
|
||||||
fileModifiedAt: new Date(),
|
fileModifiedAt: new Date(),
|
||||||
|
localDateTime: createdAt,
|
||||||
type: AssetType.IMAGE,
|
type: AssetType.IMAGE,
|
||||||
originalFileName: `test_${id}`,
|
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,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
duration: null,
|
duration: null,
|
||||||
@ -56,6 +57,7 @@ export const assetStub = {
|
|||||||
encodedVideoPath: null,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
duration: null,
|
duration: null,
|
||||||
@ -93,6 +95,7 @@ export const assetStub = {
|
|||||||
encodedVideoPath: null,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
@ -127,6 +130,7 @@ export const assetStub = {
|
|||||||
encodedVideoPath: null,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
@ -164,6 +168,7 @@ export const assetStub = {
|
|||||||
encodedVideoPath: null,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
@ -201,6 +206,7 @@ export const assetStub = {
|
|||||||
encodedVideoPath: null,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
@ -238,6 +244,45 @@ export const assetStub = {
|
|||||||
encodedVideoPath: null,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isExternal: false,
|
isExternal: false,
|
||||||
@ -276,6 +321,7 @@ export const assetStub = {
|
|||||||
encodedVideoPath: null,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
@ -344,6 +390,7 @@ export const assetStub = {
|
|||||||
encodedVideoPath: null,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
@ -382,6 +429,7 @@ export const assetStub = {
|
|||||||
encodedVideoPath: null,
|
encodedVideoPath: null,
|
||||||
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
createdAt: new Date('2023-02-23T05:06:29.716Z'),
|
||||||
updatedAt: 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,
|
isFavorite: true,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isReadOnly: 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,
|
isReadOnly: false,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
fileCreatedAt: today,
|
fileCreatedAt: today,
|
||||||
|
localDateTime: today,
|
||||||
updatedAt: today,
|
updatedAt: today,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
@ -174,6 +175,7 @@ export const sharedLinkStub = {
|
|||||||
checksum: Buffer.from('file hash', 'utf8'),
|
checksum: Buffer.from('file hash', 'utf8'),
|
||||||
fileModifiedAt: today,
|
fileModifiedAt: today,
|
||||||
fileCreatedAt: today,
|
fileCreatedAt: today,
|
||||||
|
localDateTime: today,
|
||||||
createdAt: today,
|
createdAt: today,
|
||||||
updatedAt: today,
|
updatedAt: today,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
|
@ -5,6 +5,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
|||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
upsertExif: jest.fn(),
|
upsertExif: jest.fn(),
|
||||||
getByDate: jest.fn(),
|
getByDate: jest.fn(),
|
||||||
|
getByDayOfYear: jest.fn(),
|
||||||
getByIds: jest.fn().mockResolvedValue([]),
|
getByIds: jest.fn().mockResolvedValue([]),
|
||||||
getByAlbumId: jest.fn(),
|
getByAlbumId: jest.fn(),
|
||||||
getByUserId: 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
|
* @memberof AssetResponseDto
|
||||||
*/
|
*/
|
||||||
'livePhotoVideoId'?: string | null;
|
'livePhotoVideoId'?: string | null;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AssetResponseDto
|
||||||
|
*/
|
||||||
|
'localDateTime': string;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {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.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMemoryLane: async (timestamp: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getMemoryLane: async (day: number, month: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'timestamp' is not null or undefined
|
// verify required parameter 'day' is not null or undefined
|
||||||
assertParamExists('getMemoryLane', 'timestamp', timestamp)
|
assertParamExists('getMemoryLane', 'day', day)
|
||||||
|
// verify required parameter 'month' is not null or undefined
|
||||||
|
assertParamExists('getMemoryLane', 'month', month)
|
||||||
const localVarPath = `/asset/memory-lane`;
|
const localVarPath = `/asset/memory-lane`;
|
||||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||||
@ -6368,10 +6377,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
// http bearer authentication required
|
// http bearer authentication required
|
||||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||||
|
|
||||||
if (timestamp !== undefined) {
|
if (day !== undefined) {
|
||||||
localVarQueryParameter['timestamp'] = (timestamp as any instanceof Date) ?
|
localVarQueryParameter['day'] = day;
|
||||||
(timestamp as any).toISOString() :
|
}
|
||||||
timestamp;
|
|
||||||
|
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.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getMemoryLane(timestamp: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
|
async getMemoryLane(day: number, month: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<MemoryLaneResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(timestamp, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getMemoryLane(day, month, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -7443,7 +7455,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig): AxiosPromise<Array<MemoryLaneResponseDto>> {
|
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 {
|
export interface AssetApiGetMemoryLaneRequest {
|
||||||
/**
|
/**
|
||||||
* Get pictures for +24 hours from this time going back x years
|
*
|
||||||
* @type {string}
|
* @type {number}
|
||||||
* @memberof AssetApiGetMemoryLane
|
* @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
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getMemoryLane(requestParameters: AssetApiGetMemoryLaneRequest, options?: AxiosRequestConfig) {
|
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 () => {
|
onMount(async () => {
|
||||||
if (!$memoryStore) {
|
if (!$memoryStore) {
|
||||||
|
const localTime = new Date();
|
||||||
const { data } = await api.assetApi.getMemoryLane({
|
const { data } = await api.assetApi.getMemoryLane({
|
||||||
timestamp: DateTime.local().startOf('day').toISO() || '',
|
month: localTime.getMonth() + 1,
|
||||||
|
day: localTime.getDate(),
|
||||||
});
|
});
|
||||||
$memoryStore = data;
|
$memoryStore = data;
|
||||||
}
|
}
|
||||||
@ -212,7 +214,7 @@
|
|||||||
|
|
||||||
<div class="absolute left-8 top-4 text-sm font-medium text-white">
|
<div class="absolute left-8 top-4 text-sm font-medium text-white">
|
||||||
<p>
|
<p>
|
||||||
{DateTime.fromISO(currentMemory.assets[0].fileCreatedAt).toLocaleString(DateTime.DATE_FULL)}
|
{DateTime.fromISO(currentMemory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{currentAsset.exifInfo?.city || ''}
|
{currentAsset.exifInfo?.city || ''}
|
||||||
|
@ -126,7 +126,8 @@
|
|||||||
|
|
||||||
<section id="asset-group-by-date" class="flex flex-wrap gap-x-12" bind:clientHeight={actualBucketHeight}>
|
<section id="asset-group-by-date" class="flex flex-wrap gap-x-12" bind:clientHeight={actualBucketHeight}>
|
||||||
{#each assetsGroupByDate as groupAssets, groupIndex (groupAssets[0].id)}
|
{#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 -->
|
<!-- Asset Group By Date -->
|
||||||
|
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { api } from '@api';
|
import { api } from '@api';
|
||||||
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
|
||||||
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
|
||||||
@ -11,8 +10,10 @@
|
|||||||
$: shouldRender = $memoryStore?.length > 0;
|
$: shouldRender = $memoryStore?.length > 0;
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
const localTime = new Date();
|
||||||
const { data } = await api.assetApi.getMemoryLane({
|
const { data } = await api.assetApi.getMemoryLane({
|
||||||
timestamp: DateTime.local().startOf('day').toISO() || '',
|
month: localTime.getMonth() + 1,
|
||||||
|
day: localTime.getDate(),
|
||||||
});
|
});
|
||||||
$memoryStore = data;
|
$memoryStore = data;
|
||||||
});
|
});
|
||||||
|
@ -45,7 +45,7 @@ export function splitBucketIntoDateGroups(
|
|||||||
): AssetResponseDto[][] {
|
): AssetResponseDto[][] {
|
||||||
return lodash
|
return lodash
|
||||||
.chain(assets)
|
.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]))
|
.sortBy((group) => assets.indexOf(group[0]))
|
||||||
.value();
|
.value();
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user