mirror of
https://github.com/immich-app/immich.git
synced 2026-04-28 20:18:48 -07:00
Compare commits
9 Commits
fix/map-si
...
chore/dura
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f91baea924 | ||
|
|
5c484fc01a | ||
|
|
eed6e3c354 | ||
|
|
fe0f088307 | ||
|
|
2098da6a90 | ||
|
|
f2b161f893 | ||
|
|
e8b717ee79 | ||
|
|
1b7fbbedc6 | ||
|
|
0bcd85eb2c |
@@ -32,8 +32,12 @@ export function generateThumbhash(rng: SeededRandom): string {
|
||||
return Array.from({ length: 10 }, () => rng.nextInt(0, 256).toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export function generateDuration(rng: SeededRandom): string {
|
||||
return `${rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS)}.${rng.nextInt(0, 1000).toString().padStart(3, '0')}`;
|
||||
export function generateDuration(rng: SeededRandom): number {
|
||||
return (
|
||||
rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS) *
|
||||
1000 +
|
||||
rng.nextInt(0, 1000)
|
||||
);
|
||||
}
|
||||
|
||||
export function generateUUID(): string {
|
||||
|
||||
@@ -43,7 +43,7 @@ export type MockTimelineAsset = {
|
||||
isTrashed: boolean;
|
||||
isVideo: boolean;
|
||||
isImage: boolean;
|
||||
duration: string | null;
|
||||
duration: number | null;
|
||||
projectionType: string | null;
|
||||
livePhotoVideoId: string | null;
|
||||
city: string | null;
|
||||
|
||||
@@ -192,23 +192,23 @@ class SyncStreamService {
|
||||
case SyncEntityType.assetV1:
|
||||
final remoteSyncAssets = data.cast<SyncAssetV1>();
|
||||
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
|
||||
await _runWithManageMediaPermission(
|
||||
logContext: "Trashed Assets",
|
||||
action: () async {
|
||||
await _handleRemoteDeleted(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id));
|
||||
await _applyRemoteRestoreToLocal();
|
||||
},
|
||||
);
|
||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
|
||||
}
|
||||
return;
|
||||
case SyncEntityType.assetV2:
|
||||
final remoteSyncAssets = data.cast<SyncAssetV2>();
|
||||
await _syncStreamRepository.updateAssetsV2(remoteSyncAssets);
|
||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||
await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
|
||||
}
|
||||
return;
|
||||
case SyncEntityType.assetDeleteV1:
|
||||
await _runWithManageMediaPermission(
|
||||
logContext: "Deleted Assets",
|
||||
action: () async {
|
||||
final remoteSyncAssets = data.cast<SyncAssetDeleteV1>();
|
||||
await _handleRemoteDeleted(remoteSyncAssets.map((e) => e.assetId));
|
||||
},
|
||||
);
|
||||
return _syncStreamRepository.deleteAssetsV1(data.cast());
|
||||
final remoteSyncAssets = data.cast<SyncAssetDeleteV1>();
|
||||
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||
await _syncAssetDeletion(remoteSyncAssets.map((e) => e.assetId).toList());
|
||||
}
|
||||
return _syncStreamRepository.deleteAssetsV1(remoteSyncAssets);
|
||||
case SyncEntityType.assetExifV1:
|
||||
return _syncStreamRepository.updateAssetsExifV1(data.cast());
|
||||
case SyncEntityType.assetEditV1:
|
||||
@@ -221,8 +221,12 @@ class SyncStreamService {
|
||||
return _syncStreamRepository.deleteAssetsMetadataV1(data.cast());
|
||||
case SyncEntityType.partnerAssetV1:
|
||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner');
|
||||
case SyncEntityType.partnerAssetV2:
|
||||
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'partner');
|
||||
case SyncEntityType.partnerAssetBackfillV1:
|
||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner backfill');
|
||||
case SyncEntityType.partnerAssetBackfillV2:
|
||||
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'partner backfill');
|
||||
case SyncEntityType.partnerAssetDeleteV1:
|
||||
return _syncStreamRepository.deleteAssetsV1(data.cast(), debugLabel: "partner");
|
||||
case SyncEntityType.partnerAssetExifV1:
|
||||
@@ -243,10 +247,16 @@ class SyncStreamService {
|
||||
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
|
||||
case SyncEntityType.albumAssetCreateV1:
|
||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset create');
|
||||
case SyncEntityType.albumAssetCreateV2:
|
||||
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset create');
|
||||
case SyncEntityType.albumAssetUpdateV1:
|
||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset update');
|
||||
case SyncEntityType.albumAssetUpdateV2:
|
||||
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset update');
|
||||
case SyncEntityType.albumAssetBackfillV1:
|
||||
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset backfill');
|
||||
case SyncEntityType.albumAssetBackfillV2:
|
||||
return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset backfill');
|
||||
case SyncEntityType.albumAssetExifCreateV1:
|
||||
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif create');
|
||||
case SyncEntityType.albumAssetExifUpdateV1:
|
||||
@@ -348,6 +358,47 @@ class SyncStreamService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) async {
|
||||
if (batchData.isEmpty) return;
|
||||
|
||||
_logger.info('Processing batch of ${batchData.length} AssetUploadReadyV2 events');
|
||||
|
||||
final List<SyncAssetV2> assets = [];
|
||||
final List<SyncAssetExifV1> exifs = [];
|
||||
|
||||
try {
|
||||
for (final data in batchData) {
|
||||
if (data is! Map<String, dynamic>) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final payload = data;
|
||||
final assetData = payload['asset'];
|
||||
final exifData = payload['exif'];
|
||||
|
||||
if (assetData == null || exifData == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final asset = SyncAssetV2.fromJson(assetData);
|
||||
final exif = SyncAssetExifV1.fromJson(exifData);
|
||||
|
||||
if (asset != null && exif != null) {
|
||||
assets.add(asset);
|
||||
exifs.add(exif);
|
||||
}
|
||||
}
|
||||
|
||||
if (assets.isNotEmpty && exifs.isNotEmpty) {
|
||||
await _syncStreamRepository.updateAssetsV2(assets, debugLabel: 'websocket-batch');
|
||||
await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
|
||||
_logger.info('Successfully processed ${assets.length} assets in batch');
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe("Error processing AssetUploadReadyV2 websocket batch events", error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleWsAssetEditReadyV1(dynamic data) async {
|
||||
_logger.info('Processing AssetEditReadyV1 event');
|
||||
|
||||
@@ -388,6 +439,41 @@ class SyncStreamService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleWsAssetEditReadyV2(dynamic data) async {
|
||||
_logger.info('Processing AssetEditReadyV2 event');
|
||||
|
||||
try {
|
||||
if (data is! Map<String, dynamic>) {
|
||||
throw ArgumentError("Invalid data format for AssetEditReadyV2 event");
|
||||
}
|
||||
|
||||
final payload = data;
|
||||
|
||||
if (payload['asset'] == null) {
|
||||
throw ArgumentError("Missing 'asset' field in AssetEditReadyV2 event data");
|
||||
}
|
||||
|
||||
final asset = SyncAssetV2.fromJson(payload['asset']);
|
||||
if (asset == null) {
|
||||
throw ArgumentError("Failed to parse 'asset' field in AssetEditReadyV2 event data");
|
||||
}
|
||||
|
||||
final assetEdits = (payload['edit'] as List<dynamic>)
|
||||
.map((e) => SyncAssetEditV1.fromJson(e))
|
||||
.whereType<SyncAssetEditV1>()
|
||||
.toList();
|
||||
|
||||
await _syncStreamRepository.updateAssetsV2([asset], debugLabel: 'websocket-edit');
|
||||
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
|
||||
|
||||
_logger.info(
|
||||
'Successfully processed AssetEditReadyV2 event for asset ${asset.id} with ${assetEdits.length} edits',
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe("Error processing AssetEditReadyV2 websocket event", error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
|
||||
if (remoteIds.isEmpty) {
|
||||
return Future.value();
|
||||
@@ -424,20 +510,22 @@ class SyncStreamService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _runWithManageMediaPermission({
|
||||
required String logContext,
|
||||
required Future<void> Function() action,
|
||||
}) async {
|
||||
if (!CurrentPlatform.isAndroid || !Store.get(StoreKey.manageLocalMediaAndroid, false)) {
|
||||
Future<void> _syncAssetTrashStatus(List<String> remoteIds) async {
|
||||
if (!(await _localFilesManager.hasManageMediaPermission())) {
|
||||
_logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
final hasPermission = await _localFilesManager.hasManageMediaPermission();
|
||||
if (!hasPermission) {
|
||||
_logger.warning("sync $logContext cannot proceed because MANAGE_MEDIA permission is missing");
|
||||
await _handleRemoteDeleted(remoteIds);
|
||||
await _applyRemoteRestoreToLocal();
|
||||
}
|
||||
|
||||
Future<void> _syncAssetDeletion(List<String> remoteIds) async {
|
||||
if (!(await _localFilesManager.hasManageMediaPermission())) {
|
||||
_logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
await action();
|
||||
await _handleRemoteDeleted(remoteIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ class BackgroundSyncManager {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncWebsocketBatch(List<dynamic> batchData) {
|
||||
Future<void> syncWebsocketBatchV1(List<dynamic> batchData) {
|
||||
if (_syncWebsocketTask != null) {
|
||||
return _syncWebsocketTask!.future;
|
||||
}
|
||||
@@ -196,7 +196,17 @@ class BackgroundSyncManager {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncWebsocketEdit(dynamic data) {
|
||||
Future<void> syncWebsocketBatchV2(List<dynamic> batchData) {
|
||||
if (_syncWebsocketTask != null) {
|
||||
return _syncWebsocketTask!.future;
|
||||
}
|
||||
_syncWebsocketTask = _handleWsAssetUploadReadyV2Batch(batchData);
|
||||
return _syncWebsocketTask!.whenComplete(() {
|
||||
_syncWebsocketTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncWebsocketEditV1(dynamic data) {
|
||||
if (_syncWebsocketTask != null) {
|
||||
return _syncWebsocketTask!.future;
|
||||
}
|
||||
@@ -206,6 +216,16 @@ class BackgroundSyncManager {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncWebsocketEditV2(dynamic data) {
|
||||
if (_syncWebsocketTask != null) {
|
||||
return _syncWebsocketTask!.future;
|
||||
}
|
||||
_syncWebsocketTask = _handleWsAssetEditReadyV2(data);
|
||||
return _syncWebsocketTask!.whenComplete(() {
|
||||
_syncWebsocketTask = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> syncLinkedAlbum() {
|
||||
if (_linkedAlbumSyncTask != null) {
|
||||
return _linkedAlbumSyncTask!.future;
|
||||
@@ -242,7 +262,17 @@ Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => ru
|
||||
debugLabel: 'websocket-batch',
|
||||
);
|
||||
|
||||
Cancelable<void> _handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) => runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV2Batch(batchData),
|
||||
debugLabel: 'websocket-batch',
|
||||
);
|
||||
|
||||
Cancelable<void> _handleWsAssetEditReadyV1(dynamic data) => runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1(data),
|
||||
debugLabel: 'websocket-edit',
|
||||
);
|
||||
|
||||
Cancelable<void> _handleWsAssetEditReadyV2(dynamic data) => runInIsolateGentle(
|
||||
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV2(data),
|
||||
debugLabel: 'websocket-edit',
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
import 'package:openapi/api.dart' as api;
|
||||
|
||||
@@ -14,7 +13,7 @@ extension DTOToAsset on api.AssetResponseDto {
|
||||
updatedAt: updatedAt,
|
||||
ownerId: ownerId,
|
||||
visibility: visibility.toAssetVisibility(),
|
||||
durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
|
||||
durationMs: duration,
|
||||
height: height?.toInt(),
|
||||
width: width?.toInt(),
|
||||
isFavorite: isFavorite,
|
||||
@@ -36,7 +35,7 @@ extension DTOToAsset on api.AssetResponseDto {
|
||||
updatedAt: updatedAt,
|
||||
ownerId: ownerId,
|
||||
visibility: visibility.toAssetVisibility(),
|
||||
durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
|
||||
durationMs: duration,
|
||||
height: height?.toInt(),
|
||||
width: width?.toInt(),
|
||||
isFavorite: isFavorite,
|
||||
|
||||
@@ -46,19 +46,25 @@ class SyncApiRepository {
|
||||
types: [
|
||||
SyncRequestType.authUsersV1,
|
||||
SyncRequestType.usersV1,
|
||||
SyncRequestType.assetsV1,
|
||||
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)
|
||||
? SyncRequestType.assetsV2
|
||||
: SyncRequestType.assetsV1,
|
||||
SyncRequestType.assetExifsV1,
|
||||
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetEditsV1,
|
||||
SyncRequestType.assetMetadataV1,
|
||||
SyncRequestType.partnersV1,
|
||||
SyncRequestType.partnerAssetsV1,
|
||||
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)
|
||||
? SyncRequestType.partnerAssetsV2
|
||||
: SyncRequestType.partnerAssetsV1,
|
||||
SyncRequestType.partnerAssetExifsV1,
|
||||
if (serverVersion < const SemVer(major: 3, minor: 0, patch: 0))
|
||||
SyncRequestType.albumsV1
|
||||
else
|
||||
SyncRequestType.albumsV2,
|
||||
SyncRequestType.albumUsersV1,
|
||||
SyncRequestType.albumAssetsV1,
|
||||
serverVersion >= const SemVer(major: 3, minor: 0, patch: 0)
|
||||
? SyncRequestType.albumAssetsV2
|
||||
: SyncRequestType.albumAssetsV1,
|
||||
SyncRequestType.albumAssetExifsV1,
|
||||
SyncRequestType.albumToAssetsV1,
|
||||
SyncRequestType.memoriesV1,
|
||||
@@ -67,8 +73,9 @@ class SyncApiRepository {
|
||||
SyncRequestType.partnerStacksV1,
|
||||
SyncRequestType.userMetadataV1,
|
||||
SyncRequestType.peopleV1,
|
||||
if (serverVersion < const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV1,
|
||||
if (serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)) SyncRequestType.assetFacesV2,
|
||||
serverVersion >= const SemVer(major: 2, minor: 6, patch: 0)
|
||||
? SyncRequestType.assetFacesV2
|
||||
: SyncRequestType.assetFacesV1,
|
||||
],
|
||||
reset: shouldReset,
|
||||
).toJson(),
|
||||
@@ -153,6 +160,7 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
|
||||
SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson,
|
||||
SyncEntityType.assetV1: SyncAssetV1.fromJson,
|
||||
SyncEntityType.assetV2: SyncAssetV2.fromJson,
|
||||
SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||
SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson,
|
||||
SyncEntityType.assetEditV1: SyncAssetEditV1.fromJson,
|
||||
@@ -160,7 +168,9 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.assetMetadataV1: SyncAssetMetadataV1.fromJson,
|
||||
SyncEntityType.assetMetadataDeleteV1: SyncAssetMetadataDeleteV1.fromJson,
|
||||
SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson,
|
||||
SyncEntityType.partnerAssetV2: SyncAssetV2.fromJson,
|
||||
SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson,
|
||||
SyncEntityType.partnerAssetBackfillV2: SyncAssetV2.fromJson,
|
||||
SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson,
|
||||
SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson,
|
||||
SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
||||
@@ -171,8 +181,11 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
||||
SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson,
|
||||
SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson,
|
||||
SyncEntityType.albumAssetCreateV1: SyncAssetV1.fromJson,
|
||||
SyncEntityType.albumAssetCreateV2: SyncAssetV2.fromJson,
|
||||
SyncEntityType.albumAssetUpdateV1: SyncAssetV1.fromJson,
|
||||
SyncEntityType.albumAssetUpdateV2: SyncAssetV2.fromJson,
|
||||
SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson,
|
||||
SyncEntityType.albumAssetBackfillV2: SyncAssetV2.fromJson,
|
||||
SyncEntityType.albumAssetExifCreateV1: SyncAssetExifV1.fromJson,
|
||||
SyncEntityType.albumAssetExifUpdateV1: SyncAssetExifV1.fromJson,
|
||||
SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson,
|
||||
|
||||
@@ -220,6 +220,44 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAssetsV2(Iterable<SyncAssetV2> data, {String debugLabel = 'user'}) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
for (final asset in data) {
|
||||
final companion = RemoteAssetEntityCompanion(
|
||||
name: Value(asset.originalFileName),
|
||||
type: Value(asset.type.toAssetType()),
|
||||
createdAt: Value.absentIfNull(asset.fileCreatedAt),
|
||||
updatedAt: Value.absentIfNull(asset.fileModifiedAt),
|
||||
durationMs: Value(asset.duration),
|
||||
checksum: Value(asset.checksum),
|
||||
isFavorite: Value(asset.isFavorite),
|
||||
ownerId: Value(asset.ownerId),
|
||||
localDateTime: Value(asset.localDateTime),
|
||||
thumbHash: Value(asset.thumbhash),
|
||||
deletedAt: Value(asset.deletedAt),
|
||||
visibility: Value(asset.visibility.toAssetVisibility()),
|
||||
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
||||
stackId: Value(asset.stackId),
|
||||
libraryId: Value(asset.libraryId),
|
||||
width: Value(asset.width),
|
||||
height: Value(asset.height),
|
||||
isEdited: Value(asset.isEdited),
|
||||
);
|
||||
|
||||
batch.insert(
|
||||
_db.remoteAssetEntity,
|
||||
companion.copyWith(id: Value(asset.id)),
|
||||
onConflict: DoUpdate((_) => companion),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: updateAssetsV2 - $debugLabel', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateAssetsExifV1(Iterable<SyncAssetExifV1> data, {String debugLabel = 'user'}) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
|
||||
@@ -94,8 +94,10 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
state = const WebsocketState(isConnected: false, socket: null);
|
||||
});
|
||||
|
||||
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReady);
|
||||
socket.on('AssetEditReadyV1', _handleSyncAssetEditReady);
|
||||
socket.on('AssetUploadReadyV1', _handleSyncAssetUploadReadyV1);
|
||||
socket.on('AssetUploadReadyV2', _handleSyncAssetUploadReadyV2);
|
||||
socket.on('AssetEditReadyV1', _handleSyncAssetEditReadyV1);
|
||||
socket.on('AssetEditReadyV2', _handleSyncAssetEditReadyV2);
|
||||
socket.on('on_config_update', _handleOnConfigUpdate);
|
||||
socket.on('on_new_release', _handleReleaseUpdates);
|
||||
} catch (e) {
|
||||
@@ -163,16 +165,25 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
_ref.read(serverInfoProvider.notifier).handleReleaseInfo(serverVersion, releaseVersion);
|
||||
}
|
||||
|
||||
void _handleSyncAssetUploadReady(dynamic data) {
|
||||
void _handleSyncAssetUploadReadyV1(dynamic data) {
|
||||
_batchedAssetUploadReady.add(data);
|
||||
_batchDebouncer.run(_processBatchedAssetUploadReady);
|
||||
_batchDebouncer.run(_processBatchedAssetUploadReadyV1);
|
||||
}
|
||||
|
||||
void _handleSyncAssetEditReady(dynamic data) {
|
||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEdit(data));
|
||||
void _handleSyncAssetUploadReadyV2(dynamic data) {
|
||||
_batchedAssetUploadReady.add(data);
|
||||
_batchDebouncer.run(_processBatchedAssetUploadReadyV2);
|
||||
}
|
||||
|
||||
void _processBatchedAssetUploadReady() {
|
||||
void _handleSyncAssetEditReadyV1(dynamic data) {
|
||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data));
|
||||
}
|
||||
|
||||
void _handleSyncAssetEditReadyV2(dynamic data) {
|
||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV2(data));
|
||||
}
|
||||
|
||||
void _processBatchedAssetUploadReadyV1() {
|
||||
if (_batchedAssetUploadReady.isEmpty) {
|
||||
return;
|
||||
}
|
||||
@@ -180,7 +191,7 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
|
||||
try {
|
||||
unawaited(
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketBatch(_batchedAssetUploadReady.toList()).then((_) {
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketBatchV1(_batchedAssetUploadReady.toList()).then((_) {
|
||||
if (isSyncAlbumEnabled) {
|
||||
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
||||
}
|
||||
@@ -192,6 +203,27 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
|
||||
_batchedAssetUploadReady.clear();
|
||||
}
|
||||
|
||||
void _processBatchedAssetUploadReadyV2() {
|
||||
if (_batchedAssetUploadReady.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final isSyncAlbumEnabled = Store.get(StoreKey.syncAlbums, false);
|
||||
try {
|
||||
unawaited(
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketBatchV2(_batchedAssetUploadReady.toList()).then((_) {
|
||||
if (isSyncAlbumEnabled) {
|
||||
_ref.read(backgroundSyncProvider).syncLinkedAlbum();
|
||||
}
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
_log.severe("Error processing batched AssetUploadReadyV2 events: $error");
|
||||
}
|
||||
|
||||
_batchedAssetUploadReady.clear();
|
||||
}
|
||||
}
|
||||
|
||||
final websocketProvider = StateNotifierProvider<WebsocketNotifier, WebsocketState>((ref) {
|
||||
|
||||
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@@ -579,6 +579,7 @@ Class | Method | HTTP request | Description
|
||||
- [SyncAssetMetadataDeleteV1](doc//SyncAssetMetadataDeleteV1.md)
|
||||
- [SyncAssetMetadataV1](doc//SyncAssetMetadataV1.md)
|
||||
- [SyncAssetV1](doc//SyncAssetV1.md)
|
||||
- [SyncAssetV2](doc//SyncAssetV2.md)
|
||||
- [SyncAuthUserV1](doc//SyncAuthUserV1.md)
|
||||
- [SyncEntityType](doc//SyncEntityType.md)
|
||||
- [SyncMemoryAssetDeleteV1](doc//SyncMemoryAssetDeleteV1.md)
|
||||
|
||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@@ -327,6 +327,7 @@ part 'model/sync_asset_face_v2.dart';
|
||||
part 'model/sync_asset_metadata_delete_v1.dart';
|
||||
part 'model/sync_asset_metadata_v1.dart';
|
||||
part 'model/sync_asset_v1.dart';
|
||||
part 'model/sync_asset_v2.dart';
|
||||
part 'model/sync_auth_user_v1.dart';
|
||||
part 'model/sync_entity_type.dart';
|
||||
part 'model/sync_memory_asset_delete_v1.dart';
|
||||
|
||||
12
mobile/openapi/lib/api/assets_api.dart
generated
12
mobile/openapi/lib/api/assets_api.dart
generated
@@ -1234,8 +1234,8 @@ class AssetsApi {
|
||||
/// * [String] xImmichChecksum:
|
||||
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
||||
///
|
||||
/// * [String] duration:
|
||||
/// Duration (for videos)
|
||||
/// * [int] duration:
|
||||
/// Duration in milliseconds (for videos)
|
||||
///
|
||||
/// * [String] filename:
|
||||
/// Filename
|
||||
@@ -1253,7 +1253,7 @@ class AssetsApi {
|
||||
/// Sidecar file data
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets';
|
||||
|
||||
@@ -1358,8 +1358,8 @@ class AssetsApi {
|
||||
/// * [String] xImmichChecksum:
|
||||
/// sha1 checksum that can be used for duplicate detection before the file is uploaded
|
||||
///
|
||||
/// * [String] duration:
|
||||
/// Duration (for videos)
|
||||
/// * [int] duration:
|
||||
/// Duration in milliseconds (for videos)
|
||||
///
|
||||
/// * [String] filename:
|
||||
/// Filename
|
||||
@@ -1377,7 +1377,7 @@ class AssetsApi {
|
||||
/// Sidecar file data
|
||||
///
|
||||
/// * [AssetVisibility] visibility:
|
||||
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, int? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List<AssetMetadataUpsertItemDto>? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
|
||||
final response = await uploadAssetWithHttpInfo(assetData, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
|
||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@@ -700,6 +700,8 @@ class ApiClient {
|
||||
return SyncAssetMetadataV1.fromJson(value);
|
||||
case 'SyncAssetV1':
|
||||
return SyncAssetV1.fromJson(value);
|
||||
case 'SyncAssetV2':
|
||||
return SyncAssetV2.fromJson(value);
|
||||
case 'SyncAuthUserV1':
|
||||
return SyncAuthUserV1.fromJson(value);
|
||||
case 'SyncEntityType':
|
||||
|
||||
9
mobile/openapi/lib/model/asset_response_dto.dart
generated
9
mobile/openapi/lib/model/asset_response_dto.dart
generated
@@ -57,8 +57,11 @@ class AssetResponseDto {
|
||||
/// Duplicate group ID
|
||||
String? duplicateId;
|
||||
|
||||
/// Video/gif duration in hh:mm:ss.SSS format (null for static images)
|
||||
String? duration;
|
||||
/// Video/gif duration in milliseconds (null for static images)
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 2147483647
|
||||
int? duration;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
@@ -341,7 +344,7 @@ class AssetResponseDto {
|
||||
checksum: mapValueOfType<String>(json, r'checksum')!,
|
||||
createdAt: mapDateTime(json, r'createdAt', r'')!,
|
||||
duplicateId: mapValueOfType<String>(json, r'duplicateId'),
|
||||
duration: mapValueOfType<String>(json, r'duration'),
|
||||
duration: mapValueOfType<int>(json, r'duration'),
|
||||
exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
|
||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
|
||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
|
||||
|
||||
321
mobile/openapi/lib/model/sync_asset_v2.dart
generated
Normal file
321
mobile/openapi/lib/model/sync_asset_v2.dart
generated
Normal file
@@ -0,0 +1,321 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class SyncAssetV2 {
|
||||
/// Returns a new [SyncAssetV2] instance.
|
||||
SyncAssetV2({
|
||||
required this.checksum,
|
||||
required this.deletedAt,
|
||||
required this.duration,
|
||||
required this.fileCreatedAt,
|
||||
required this.fileModifiedAt,
|
||||
required this.height,
|
||||
required this.id,
|
||||
required this.isEdited,
|
||||
required this.isFavorite,
|
||||
required this.libraryId,
|
||||
required this.livePhotoVideoId,
|
||||
required this.localDateTime,
|
||||
required this.originalFileName,
|
||||
required this.ownerId,
|
||||
required this.stackId,
|
||||
required this.thumbhash,
|
||||
required this.type,
|
||||
required this.visibility,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
/// Checksum
|
||||
String checksum;
|
||||
|
||||
/// Deleted at
|
||||
DateTime? deletedAt;
|
||||
|
||||
/// Duration
|
||||
///
|
||||
/// Minimum value: 0
|
||||
/// Maximum value: 2147483647
|
||||
int? duration;
|
||||
|
||||
/// File created at
|
||||
DateTime? fileCreatedAt;
|
||||
|
||||
/// File modified at
|
||||
DateTime? fileModifiedAt;
|
||||
|
||||
/// Asset height
|
||||
///
|
||||
/// Minimum value: -9007199254740991
|
||||
/// Maximum value: 9007199254740991
|
||||
int? height;
|
||||
|
||||
/// Asset ID
|
||||
String id;
|
||||
|
||||
/// Is edited
|
||||
bool isEdited;
|
||||
|
||||
/// Is favorite
|
||||
bool isFavorite;
|
||||
|
||||
/// Library ID
|
||||
String? libraryId;
|
||||
|
||||
/// Live photo video ID
|
||||
String? livePhotoVideoId;
|
||||
|
||||
/// Local date time
|
||||
DateTime? localDateTime;
|
||||
|
||||
/// Original file name
|
||||
String originalFileName;
|
||||
|
||||
/// Owner ID
|
||||
String ownerId;
|
||||
|
||||
/// Stack ID
|
||||
String? stackId;
|
||||
|
||||
/// Thumbhash
|
||||
String? thumbhash;
|
||||
|
||||
AssetTypeEnum type;
|
||||
|
||||
AssetVisibility visibility;
|
||||
|
||||
/// Asset width
|
||||
///
|
||||
/// Minimum value: -9007199254740991
|
||||
/// Maximum value: 9007199254740991
|
||||
int? width;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV2 &&
|
||||
other.checksum == checksum &&
|
||||
other.deletedAt == deletedAt &&
|
||||
other.duration == duration &&
|
||||
other.fileCreatedAt == fileCreatedAt &&
|
||||
other.fileModifiedAt == fileModifiedAt &&
|
||||
other.height == height &&
|
||||
other.id == id &&
|
||||
other.isEdited == isEdited &&
|
||||
other.isFavorite == isFavorite &&
|
||||
other.libraryId == libraryId &&
|
||||
other.livePhotoVideoId == livePhotoVideoId &&
|
||||
other.localDateTime == localDateTime &&
|
||||
other.originalFileName == originalFileName &&
|
||||
other.ownerId == ownerId &&
|
||||
other.stackId == stackId &&
|
||||
other.thumbhash == thumbhash &&
|
||||
other.type == type &&
|
||||
other.visibility == visibility &&
|
||||
other.width == width;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(checksum.hashCode) +
|
||||
(deletedAt == null ? 0 : deletedAt!.hashCode) +
|
||||
(duration == null ? 0 : duration!.hashCode) +
|
||||
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
|
||||
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
||||
(height == null ? 0 : height!.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isEdited.hashCode) +
|
||||
(isFavorite.hashCode) +
|
||||
(libraryId == null ? 0 : libraryId!.hashCode) +
|
||||
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||
(localDateTime == null ? 0 : localDateTime!.hashCode) +
|
||||
(originalFileName.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
(stackId == null ? 0 : stackId!.hashCode) +
|
||||
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||
(type.hashCode) +
|
||||
(visibility.hashCode) +
|
||||
(width == null ? 0 : width!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SyncAssetV2[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, isEdited=$isEdited, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility, width=$width]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'checksum'] = this.checksum;
|
||||
if (this.deletedAt != null) {
|
||||
json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.deletedAt!.millisecondsSinceEpoch
|
||||
: this.deletedAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'deletedAt'] = null;
|
||||
}
|
||||
if (this.duration != null) {
|
||||
json[r'duration'] = this.duration;
|
||||
} else {
|
||||
// json[r'duration'] = null;
|
||||
}
|
||||
if (this.fileCreatedAt != null) {
|
||||
json[r'fileCreatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.fileCreatedAt!.millisecondsSinceEpoch
|
||||
: this.fileCreatedAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'fileCreatedAt'] = null;
|
||||
}
|
||||
if (this.fileModifiedAt != null) {
|
||||
json[r'fileModifiedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.fileModifiedAt!.millisecondsSinceEpoch
|
||||
: this.fileModifiedAt!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'fileModifiedAt'] = null;
|
||||
}
|
||||
if (this.height != null) {
|
||||
json[r'height'] = this.height;
|
||||
} else {
|
||||
// json[r'height'] = null;
|
||||
}
|
||||
json[r'id'] = this.id;
|
||||
json[r'isEdited'] = this.isEdited;
|
||||
json[r'isFavorite'] = this.isFavorite;
|
||||
if (this.libraryId != null) {
|
||||
json[r'libraryId'] = this.libraryId;
|
||||
} else {
|
||||
// json[r'libraryId'] = null;
|
||||
}
|
||||
if (this.livePhotoVideoId != null) {
|
||||
json[r'livePhotoVideoId'] = this.livePhotoVideoId;
|
||||
} else {
|
||||
// json[r'livePhotoVideoId'] = null;
|
||||
}
|
||||
if (this.localDateTime != null) {
|
||||
json[r'localDateTime'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')
|
||||
? this.localDateTime!.millisecondsSinceEpoch
|
||||
: this.localDateTime!.toUtc().toIso8601String();
|
||||
} else {
|
||||
// json[r'localDateTime'] = null;
|
||||
}
|
||||
json[r'originalFileName'] = this.originalFileName;
|
||||
json[r'ownerId'] = this.ownerId;
|
||||
if (this.stackId != null) {
|
||||
json[r'stackId'] = this.stackId;
|
||||
} else {
|
||||
// json[r'stackId'] = null;
|
||||
}
|
||||
if (this.thumbhash != null) {
|
||||
json[r'thumbhash'] = this.thumbhash;
|
||||
} else {
|
||||
// json[r'thumbhash'] = null;
|
||||
}
|
||||
json[r'type'] = this.type;
|
||||
json[r'visibility'] = this.visibility;
|
||||
if (this.width != null) {
|
||||
json[r'width'] = this.width;
|
||||
} else {
|
||||
// json[r'width'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SyncAssetV2] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SyncAssetV2? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SyncAssetV2");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SyncAssetV2(
|
||||
checksum: mapValueOfType<String>(json, r'checksum')!,
|
||||
deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
duration: mapValueOfType<int>(json, r'duration'),
|
||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
height: mapValueOfType<int>(json, r'height'),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isEdited: mapValueOfType<bool>(json, r'isEdited')!,
|
||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||
localDateTime: mapDateTime(json, r'localDateTime', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'),
|
||||
originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
stackId: mapValueOfType<String>(json, r'stackId'),
|
||||
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
||||
width: mapValueOfType<int>(json, r'width'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SyncAssetV2> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SyncAssetV2>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SyncAssetV2.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SyncAssetV2> mapFromJson(dynamic json) {
|
||||
final map = <String, SyncAssetV2>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SyncAssetV2.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SyncAssetV2-objects as value to a dart map
|
||||
static Map<String, List<SyncAssetV2>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SyncAssetV2>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SyncAssetV2.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'checksum',
|
||||
'deletedAt',
|
||||
'duration',
|
||||
'fileCreatedAt',
|
||||
'fileModifiedAt',
|
||||
'height',
|
||||
'id',
|
||||
'isEdited',
|
||||
'isFavorite',
|
||||
'libraryId',
|
||||
'livePhotoVideoId',
|
||||
'localDateTime',
|
||||
'originalFileName',
|
||||
'ownerId',
|
||||
'stackId',
|
||||
'thumbhash',
|
||||
'type',
|
||||
'visibility',
|
||||
'width',
|
||||
};
|
||||
}
|
||||
|
||||
18
mobile/openapi/lib/model/sync_entity_type.dart
generated
18
mobile/openapi/lib/model/sync_entity_type.dart
generated
@@ -27,6 +27,7 @@ class SyncEntityType {
|
||||
static const userV1 = SyncEntityType._(r'UserV1');
|
||||
static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1');
|
||||
static const assetV1 = SyncEntityType._(r'AssetV1');
|
||||
static const assetV2 = SyncEntityType._(r'AssetV2');
|
||||
static const assetDeleteV1 = SyncEntityType._(r'AssetDeleteV1');
|
||||
static const assetExifV1 = SyncEntityType._(r'AssetExifV1');
|
||||
static const assetEditV1 = SyncEntityType._(r'AssetEditV1');
|
||||
@@ -36,7 +37,9 @@ class SyncEntityType {
|
||||
static const partnerV1 = SyncEntityType._(r'PartnerV1');
|
||||
static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1');
|
||||
static const partnerAssetV1 = SyncEntityType._(r'PartnerAssetV1');
|
||||
static const partnerAssetV2 = SyncEntityType._(r'PartnerAssetV2');
|
||||
static const partnerAssetBackfillV1 = SyncEntityType._(r'PartnerAssetBackfillV1');
|
||||
static const partnerAssetBackfillV2 = SyncEntityType._(r'PartnerAssetBackfillV2');
|
||||
static const partnerAssetDeleteV1 = SyncEntityType._(r'PartnerAssetDeleteV1');
|
||||
static const partnerAssetExifV1 = SyncEntityType._(r'PartnerAssetExifV1');
|
||||
static const partnerAssetExifBackfillV1 = SyncEntityType._(r'PartnerAssetExifBackfillV1');
|
||||
@@ -50,8 +53,11 @@ class SyncEntityType {
|
||||
static const albumUserBackfillV1 = SyncEntityType._(r'AlbumUserBackfillV1');
|
||||
static const albumUserDeleteV1 = SyncEntityType._(r'AlbumUserDeleteV1');
|
||||
static const albumAssetCreateV1 = SyncEntityType._(r'AlbumAssetCreateV1');
|
||||
static const albumAssetCreateV2 = SyncEntityType._(r'AlbumAssetCreateV2');
|
||||
static const albumAssetUpdateV1 = SyncEntityType._(r'AlbumAssetUpdateV1');
|
||||
static const albumAssetUpdateV2 = SyncEntityType._(r'AlbumAssetUpdateV2');
|
||||
static const albumAssetBackfillV1 = SyncEntityType._(r'AlbumAssetBackfillV1');
|
||||
static const albumAssetBackfillV2 = SyncEntityType._(r'AlbumAssetBackfillV2');
|
||||
static const albumAssetExifCreateV1 = SyncEntityType._(r'AlbumAssetExifCreateV1');
|
||||
static const albumAssetExifUpdateV1 = SyncEntityType._(r'AlbumAssetExifUpdateV1');
|
||||
static const albumAssetExifBackfillV1 = SyncEntityType._(r'AlbumAssetExifBackfillV1');
|
||||
@@ -81,6 +87,7 @@ class SyncEntityType {
|
||||
userV1,
|
||||
userDeleteV1,
|
||||
assetV1,
|
||||
assetV2,
|
||||
assetDeleteV1,
|
||||
assetExifV1,
|
||||
assetEditV1,
|
||||
@@ -90,7 +97,9 @@ class SyncEntityType {
|
||||
partnerV1,
|
||||
partnerDeleteV1,
|
||||
partnerAssetV1,
|
||||
partnerAssetV2,
|
||||
partnerAssetBackfillV1,
|
||||
partnerAssetBackfillV2,
|
||||
partnerAssetDeleteV1,
|
||||
partnerAssetExifV1,
|
||||
partnerAssetExifBackfillV1,
|
||||
@@ -104,8 +113,11 @@ class SyncEntityType {
|
||||
albumUserBackfillV1,
|
||||
albumUserDeleteV1,
|
||||
albumAssetCreateV1,
|
||||
albumAssetCreateV2,
|
||||
albumAssetUpdateV1,
|
||||
albumAssetUpdateV2,
|
||||
albumAssetBackfillV1,
|
||||
albumAssetBackfillV2,
|
||||
albumAssetExifCreateV1,
|
||||
albumAssetExifUpdateV1,
|
||||
albumAssetExifBackfillV1,
|
||||
@@ -170,6 +182,7 @@ class SyncEntityTypeTypeTransformer {
|
||||
case r'UserV1': return SyncEntityType.userV1;
|
||||
case r'UserDeleteV1': return SyncEntityType.userDeleteV1;
|
||||
case r'AssetV1': return SyncEntityType.assetV1;
|
||||
case r'AssetV2': return SyncEntityType.assetV2;
|
||||
case r'AssetDeleteV1': return SyncEntityType.assetDeleteV1;
|
||||
case r'AssetExifV1': return SyncEntityType.assetExifV1;
|
||||
case r'AssetEditV1': return SyncEntityType.assetEditV1;
|
||||
@@ -179,7 +192,9 @@ class SyncEntityTypeTypeTransformer {
|
||||
case r'PartnerV1': return SyncEntityType.partnerV1;
|
||||
case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1;
|
||||
case r'PartnerAssetV1': return SyncEntityType.partnerAssetV1;
|
||||
case r'PartnerAssetV2': return SyncEntityType.partnerAssetV2;
|
||||
case r'PartnerAssetBackfillV1': return SyncEntityType.partnerAssetBackfillV1;
|
||||
case r'PartnerAssetBackfillV2': return SyncEntityType.partnerAssetBackfillV2;
|
||||
case r'PartnerAssetDeleteV1': return SyncEntityType.partnerAssetDeleteV1;
|
||||
case r'PartnerAssetExifV1': return SyncEntityType.partnerAssetExifV1;
|
||||
case r'PartnerAssetExifBackfillV1': return SyncEntityType.partnerAssetExifBackfillV1;
|
||||
@@ -193,8 +208,11 @@ class SyncEntityTypeTypeTransformer {
|
||||
case r'AlbumUserBackfillV1': return SyncEntityType.albumUserBackfillV1;
|
||||
case r'AlbumUserDeleteV1': return SyncEntityType.albumUserDeleteV1;
|
||||
case r'AlbumAssetCreateV1': return SyncEntityType.albumAssetCreateV1;
|
||||
case r'AlbumAssetCreateV2': return SyncEntityType.albumAssetCreateV2;
|
||||
case r'AlbumAssetUpdateV1': return SyncEntityType.albumAssetUpdateV1;
|
||||
case r'AlbumAssetUpdateV2': return SyncEntityType.albumAssetUpdateV2;
|
||||
case r'AlbumAssetBackfillV1': return SyncEntityType.albumAssetBackfillV1;
|
||||
case r'AlbumAssetBackfillV2': return SyncEntityType.albumAssetBackfillV2;
|
||||
case r'AlbumAssetExifCreateV1': return SyncEntityType.albumAssetExifCreateV1;
|
||||
case r'AlbumAssetExifUpdateV1': return SyncEntityType.albumAssetExifUpdateV1;
|
||||
case r'AlbumAssetExifBackfillV1': return SyncEntityType.albumAssetExifBackfillV1;
|
||||
|
||||
9
mobile/openapi/lib/model/sync_request_type.dart
generated
9
mobile/openapi/lib/model/sync_request_type.dart
generated
@@ -28,8 +28,10 @@ class SyncRequestType {
|
||||
static const albumUsersV1 = SyncRequestType._(r'AlbumUsersV1');
|
||||
static const albumToAssetsV1 = SyncRequestType._(r'AlbumToAssetsV1');
|
||||
static const albumAssetsV1 = SyncRequestType._(r'AlbumAssetsV1');
|
||||
static const albumAssetsV2 = SyncRequestType._(r'AlbumAssetsV2');
|
||||
static const albumAssetExifsV1 = SyncRequestType._(r'AlbumAssetExifsV1');
|
||||
static const assetsV1 = SyncRequestType._(r'AssetsV1');
|
||||
static const assetsV2 = SyncRequestType._(r'AssetsV2');
|
||||
static const assetExifsV1 = SyncRequestType._(r'AssetExifsV1');
|
||||
static const assetEditsV1 = SyncRequestType._(r'AssetEditsV1');
|
||||
static const assetMetadataV1 = SyncRequestType._(r'AssetMetadataV1');
|
||||
@@ -38,6 +40,7 @@ class SyncRequestType {
|
||||
static const memoryToAssetsV1 = SyncRequestType._(r'MemoryToAssetsV1');
|
||||
static const partnersV1 = SyncRequestType._(r'PartnersV1');
|
||||
static const partnerAssetsV1 = SyncRequestType._(r'PartnerAssetsV1');
|
||||
static const partnerAssetsV2 = SyncRequestType._(r'PartnerAssetsV2');
|
||||
static const partnerAssetExifsV1 = SyncRequestType._(r'PartnerAssetExifsV1');
|
||||
static const partnerStacksV1 = SyncRequestType._(r'PartnerStacksV1');
|
||||
static const stacksV1 = SyncRequestType._(r'StacksV1');
|
||||
@@ -54,8 +57,10 @@ class SyncRequestType {
|
||||
albumUsersV1,
|
||||
albumToAssetsV1,
|
||||
albumAssetsV1,
|
||||
albumAssetsV2,
|
||||
albumAssetExifsV1,
|
||||
assetsV1,
|
||||
assetsV2,
|
||||
assetExifsV1,
|
||||
assetEditsV1,
|
||||
assetMetadataV1,
|
||||
@@ -64,6 +69,7 @@ class SyncRequestType {
|
||||
memoryToAssetsV1,
|
||||
partnersV1,
|
||||
partnerAssetsV1,
|
||||
partnerAssetsV2,
|
||||
partnerAssetExifsV1,
|
||||
partnerStacksV1,
|
||||
stacksV1,
|
||||
@@ -115,8 +121,10 @@ class SyncRequestTypeTypeTransformer {
|
||||
case r'AlbumUsersV1': return SyncRequestType.albumUsersV1;
|
||||
case r'AlbumToAssetsV1': return SyncRequestType.albumToAssetsV1;
|
||||
case r'AlbumAssetsV1': return SyncRequestType.albumAssetsV1;
|
||||
case r'AlbumAssetsV2': return SyncRequestType.albumAssetsV2;
|
||||
case r'AlbumAssetExifsV1': return SyncRequestType.albumAssetExifsV1;
|
||||
case r'AssetsV1': return SyncRequestType.assetsV1;
|
||||
case r'AssetsV2': return SyncRequestType.assetsV2;
|
||||
case r'AssetExifsV1': return SyncRequestType.assetExifsV1;
|
||||
case r'AssetEditsV1': return SyncRequestType.assetEditsV1;
|
||||
case r'AssetMetadataV1': return SyncRequestType.assetMetadataV1;
|
||||
@@ -125,6 +133,7 @@ class SyncRequestTypeTypeTransformer {
|
||||
case r'MemoryToAssetsV1': return SyncRequestType.memoryToAssetsV1;
|
||||
case r'PartnersV1': return SyncRequestType.partnersV1;
|
||||
case r'PartnerAssetsV1': return SyncRequestType.partnerAssetsV1;
|
||||
case r'PartnerAssetsV2': return SyncRequestType.partnerAssetsV2;
|
||||
case r'PartnerAssetExifsV1': return SyncRequestType.partnerAssetExifsV1;
|
||||
case r'PartnerStacksV1': return SyncRequestType.partnerStacksV1;
|
||||
case r'StacksV1': return SyncRequestType.stacksV1;
|
||||
|
||||
@@ -39,8 +39,8 @@ class TimeBucketAssetResponseDto {
|
||||
/// Array of country names extracted from EXIF GPS data
|
||||
List<String?> country;
|
||||
|
||||
/// Array of video/gif durations in hh:mm:ss.SSS format (null for static images)
|
||||
List<String?> duration;
|
||||
/// Array of video/gif durations in milliseconds (null for static images)
|
||||
List<int?> duration;
|
||||
|
||||
/// Array of file creation timestamps in UTC
|
||||
List<String> fileCreatedAt;
|
||||
@@ -172,7 +172,7 @@ class TimeBucketAssetResponseDto {
|
||||
? (json[r'country'] as Iterable).cast<String>().toList(growable: false)
|
||||
: const [],
|
||||
duration: json[r'duration'] is Iterable
|
||||
? (json[r'duration'] as Iterable).cast<String>().toList(growable: false)
|
||||
? (json[r'duration'] as Iterable).cast<int>().toList(growable: false)
|
||||
: const [],
|
||||
fileCreatedAt: json[r'fileCreatedAt'] is Iterable
|
||||
? (json[r'fileCreatedAt'] as Iterable).cast<String>().toList(growable: false)
|
||||
|
||||
@@ -16258,8 +16258,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Duration (for videos)",
|
||||
"type": "string"
|
||||
"description": "Duration in milliseconds (for videos)",
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"fileCreatedAt": {
|
||||
"description": "File creation date",
|
||||
@@ -16627,9 +16629,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Video/gif duration in hh:mm:ss.SSS format (null for static images)",
|
||||
"description": "Video/gif duration in milliseconds (null for static images)",
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0,
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"exifInfo": {
|
||||
"$ref": "#/components/schemas/ExifResponseDto"
|
||||
@@ -23146,6 +23150,135 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAssetV2": {
|
||||
"properties": {
|
||||
"checksum": {
|
||||
"description": "Checksum",
|
||||
"type": "string"
|
||||
},
|
||||
"deletedAt": {
|
||||
"description": "Deleted at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Duration",
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"fileCreatedAt": {
|
||||
"description": "File created at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"fileModifiedAt": {
|
||||
"description": "File modified at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"description": "Asset height",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"description": "Asset ID",
|
||||
"type": "string"
|
||||
},
|
||||
"isEdited": {
|
||||
"description": "Is edited",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFavorite": {
|
||||
"description": "Is favorite",
|
||||
"type": "boolean"
|
||||
},
|
||||
"libraryId": {
|
||||
"description": "Library ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"livePhotoVideoId": {
|
||||
"description": "Live photo video ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"localDateTime": {
|
||||
"description": "Local date time",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"originalFileName": {
|
||||
"description": "Original file name",
|
||||
"type": "string"
|
||||
},
|
||||
"ownerId": {
|
||||
"description": "Owner ID",
|
||||
"type": "string"
|
||||
},
|
||||
"stackId": {
|
||||
"description": "Stack ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"thumbhash": {
|
||||
"description": "Thumbhash",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||
},
|
||||
"visibility": {
|
||||
"$ref": "#/components/schemas/AssetVisibility"
|
||||
},
|
||||
"width": {
|
||||
"description": "Asset width",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"checksum",
|
||||
"deletedAt",
|
||||
"duration",
|
||||
"fileCreatedAt",
|
||||
"fileModifiedAt",
|
||||
"height",
|
||||
"id",
|
||||
"isEdited",
|
||||
"isFavorite",
|
||||
"libraryId",
|
||||
"livePhotoVideoId",
|
||||
"localDateTime",
|
||||
"originalFileName",
|
||||
"ownerId",
|
||||
"stackId",
|
||||
"thumbhash",
|
||||
"type",
|
||||
"visibility",
|
||||
"width"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAuthUserV1": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
@@ -23246,6 +23379,7 @@
|
||||
"UserV1",
|
||||
"UserDeleteV1",
|
||||
"AssetV1",
|
||||
"AssetV2",
|
||||
"AssetDeleteV1",
|
||||
"AssetExifV1",
|
||||
"AssetEditV1",
|
||||
@@ -23255,7 +23389,9 @@
|
||||
"PartnerV1",
|
||||
"PartnerDeleteV1",
|
||||
"PartnerAssetV1",
|
||||
"PartnerAssetV2",
|
||||
"PartnerAssetBackfillV1",
|
||||
"PartnerAssetBackfillV2",
|
||||
"PartnerAssetDeleteV1",
|
||||
"PartnerAssetExifV1",
|
||||
"PartnerAssetExifBackfillV1",
|
||||
@@ -23269,8 +23405,11 @@
|
||||
"AlbumUserBackfillV1",
|
||||
"AlbumUserDeleteV1",
|
||||
"AlbumAssetCreateV1",
|
||||
"AlbumAssetCreateV2",
|
||||
"AlbumAssetUpdateV1",
|
||||
"AlbumAssetUpdateV2",
|
||||
"AlbumAssetBackfillV1",
|
||||
"AlbumAssetBackfillV2",
|
||||
"AlbumAssetExifCreateV1",
|
||||
"AlbumAssetExifUpdateV1",
|
||||
"AlbumAssetExifBackfillV1",
|
||||
@@ -23562,8 +23701,10 @@
|
||||
"AlbumUsersV1",
|
||||
"AlbumToAssetsV1",
|
||||
"AlbumAssetsV1",
|
||||
"AlbumAssetsV2",
|
||||
"AlbumAssetExifsV1",
|
||||
"AssetsV1",
|
||||
"AssetsV2",
|
||||
"AssetExifsV1",
|
||||
"AssetEditsV1",
|
||||
"AssetMetadataV1",
|
||||
@@ -23572,6 +23713,7 @@
|
||||
"MemoryToAssetsV1",
|
||||
"PartnersV1",
|
||||
"PartnerAssetsV1",
|
||||
"PartnerAssetsV2",
|
||||
"PartnerAssetExifsV1",
|
||||
"PartnerStacksV1",
|
||||
"StacksV1",
|
||||
@@ -24964,10 +25106,12 @@
|
||||
"type": "array"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Array of video/gif durations in hh:mm:ss.SSS format (null for static images)",
|
||||
"description": "Array of video/gif durations in milliseconds (null for static images)",
|
||||
"items": {
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0,
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
||||
@@ -614,8 +614,8 @@ export type AssetMetadataUpsertItemDto = {
|
||||
export type AssetMediaCreateDto = {
|
||||
/** Asset file data */
|
||||
assetData: Blob;
|
||||
/** Duration (for videos) */
|
||||
duration?: string;
|
||||
/** Duration in milliseconds (for videos) */
|
||||
duration?: number;
|
||||
/** File creation date */
|
||||
fileCreatedAt: string;
|
||||
/** File modification date */
|
||||
@@ -854,8 +854,8 @@ export type AssetResponseDto = {
|
||||
createdAt: string;
|
||||
/** Duplicate group ID */
|
||||
duplicateId?: string | null;
|
||||
/** Video/gif duration in hh:mm:ss.SSS format (null for static images) */
|
||||
duration: string | null;
|
||||
/** Video/gif duration in milliseconds (null for static images) */
|
||||
duration: number | null;
|
||||
exifInfo?: ExifResponseDto;
|
||||
/** The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken. */
|
||||
fileCreatedAt: string;
|
||||
@@ -2673,8 +2673,8 @@ export type TimeBucketAssetResponseDto = {
|
||||
city: (string | null)[];
|
||||
/** Array of country names extracted from EXIF GPS data */
|
||||
country: (string | null)[];
|
||||
/** Array of video/gif durations in hh:mm:ss.SSS format (null for static images) */
|
||||
duration: (string | null)[];
|
||||
/** Array of video/gif durations in milliseconds (null for static images) */
|
||||
duration: (number | null)[];
|
||||
/** Array of file creation timestamps in UTC */
|
||||
fileCreatedAt: string[];
|
||||
/** Array of asset IDs in the time bucket */
|
||||
@@ -3075,6 +3075,44 @@ export type SyncAssetV1 = {
|
||||
/** Asset width */
|
||||
width: number | null;
|
||||
};
|
||||
export type SyncAssetV2 = {
|
||||
/** Checksum */
|
||||
checksum: string;
|
||||
/** Deleted at */
|
||||
deletedAt: string | null;
|
||||
/** Duration */
|
||||
duration: number | null;
|
||||
/** File created at */
|
||||
fileCreatedAt: string | null;
|
||||
/** File modified at */
|
||||
fileModifiedAt: string | null;
|
||||
/** Asset height */
|
||||
height: number | null;
|
||||
/** Asset ID */
|
||||
id: string;
|
||||
/** Is edited */
|
||||
isEdited: boolean;
|
||||
/** Is favorite */
|
||||
isFavorite: boolean;
|
||||
/** Library ID */
|
||||
libraryId: string | null;
|
||||
/** Live photo video ID */
|
||||
livePhotoVideoId: string | null;
|
||||
/** Local date time */
|
||||
localDateTime: string | null;
|
||||
/** Original file name */
|
||||
originalFileName: string;
|
||||
/** Owner ID */
|
||||
ownerId: string;
|
||||
/** Stack ID */
|
||||
stackId: string | null;
|
||||
/** Thumbhash */
|
||||
thumbhash: string | null;
|
||||
"type": AssetTypeEnum;
|
||||
visibility: AssetVisibility;
|
||||
/** Asset width */
|
||||
width: number | null;
|
||||
};
|
||||
export type SyncAuthUserV1 = {
|
||||
avatarColor?: (UserAvatarColor) | null;
|
||||
/** User deleted at */
|
||||
@@ -7109,6 +7147,7 @@ export enum SyncEntityType {
|
||||
UserV1 = "UserV1",
|
||||
UserDeleteV1 = "UserDeleteV1",
|
||||
AssetV1 = "AssetV1",
|
||||
AssetV2 = "AssetV2",
|
||||
AssetDeleteV1 = "AssetDeleteV1",
|
||||
AssetExifV1 = "AssetExifV1",
|
||||
AssetEditV1 = "AssetEditV1",
|
||||
@@ -7118,7 +7157,9 @@ export enum SyncEntityType {
|
||||
PartnerV1 = "PartnerV1",
|
||||
PartnerDeleteV1 = "PartnerDeleteV1",
|
||||
PartnerAssetV1 = "PartnerAssetV1",
|
||||
PartnerAssetV2 = "PartnerAssetV2",
|
||||
PartnerAssetBackfillV1 = "PartnerAssetBackfillV1",
|
||||
PartnerAssetBackfillV2 = "PartnerAssetBackfillV2",
|
||||
PartnerAssetDeleteV1 = "PartnerAssetDeleteV1",
|
||||
PartnerAssetExifV1 = "PartnerAssetExifV1",
|
||||
PartnerAssetExifBackfillV1 = "PartnerAssetExifBackfillV1",
|
||||
@@ -7132,8 +7173,11 @@ export enum SyncEntityType {
|
||||
AlbumUserBackfillV1 = "AlbumUserBackfillV1",
|
||||
AlbumUserDeleteV1 = "AlbumUserDeleteV1",
|
||||
AlbumAssetCreateV1 = "AlbumAssetCreateV1",
|
||||
AlbumAssetCreateV2 = "AlbumAssetCreateV2",
|
||||
AlbumAssetUpdateV1 = "AlbumAssetUpdateV1",
|
||||
AlbumAssetUpdateV2 = "AlbumAssetUpdateV2",
|
||||
AlbumAssetBackfillV1 = "AlbumAssetBackfillV1",
|
||||
AlbumAssetBackfillV2 = "AlbumAssetBackfillV2",
|
||||
AlbumAssetExifCreateV1 = "AlbumAssetExifCreateV1",
|
||||
AlbumAssetExifUpdateV1 = "AlbumAssetExifUpdateV1",
|
||||
AlbumAssetExifBackfillV1 = "AlbumAssetExifBackfillV1",
|
||||
@@ -7163,8 +7207,10 @@ export enum SyncRequestType {
|
||||
AlbumUsersV1 = "AlbumUsersV1",
|
||||
AlbumToAssetsV1 = "AlbumToAssetsV1",
|
||||
AlbumAssetsV1 = "AlbumAssetsV1",
|
||||
AlbumAssetsV2 = "AlbumAssetsV2",
|
||||
AlbumAssetExifsV1 = "AlbumAssetExifsV1",
|
||||
AssetsV1 = "AssetsV1",
|
||||
AssetsV2 = "AssetsV2",
|
||||
AssetExifsV1 = "AssetExifsV1",
|
||||
AssetEditsV1 = "AssetEditsV1",
|
||||
AssetMetadataV1 = "AssetMetadataV1",
|
||||
@@ -7173,6 +7219,7 @@ export enum SyncRequestType {
|
||||
MemoryToAssetsV1 = "MemoryToAssetsV1",
|
||||
PartnersV1 = "PartnersV1",
|
||||
PartnerAssetsV1 = "PartnerAssetsV1",
|
||||
PartnerAssetsV2 = "PartnerAssetsV2",
|
||||
PartnerAssetExifsV1 = "PartnerAssetExifsV1",
|
||||
PartnerStacksV1 = "PartnerStacksV1",
|
||||
StacksV1 = "StacksV1",
|
||||
|
||||
@@ -38,7 +38,7 @@ export enum UploadFieldName {
|
||||
const AssetMediaBaseSchema = z.object({
|
||||
fileCreatedAt: isoDatetimeToDate.describe('File creation date'),
|
||||
fileModifiedAt: isoDatetimeToDate.describe('File modification date'),
|
||||
duration: z.string().optional().describe('Duration (for videos)'),
|
||||
duration: z.int32().min(0).optional().describe('Duration in milliseconds (for videos)'),
|
||||
filename: z.string().optional().describe('Filename'),
|
||||
/** The properties below are added to correctly generate the API docs and client SDKs. Validation should be handled in the controller. */
|
||||
[UploadFieldName.ASSET_DATA]: z.any().describe('Asset file data').meta({ type: 'string', format: 'binary' }),
|
||||
|
||||
@@ -47,7 +47,7 @@ const SanitizedAssetResponseSchema = z
|
||||
.describe(
|
||||
'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.',
|
||||
),
|
||||
duration: z.string().nullable().describe('Video/gif duration in hh:mm:ss.SSS format (null for static images)'),
|
||||
duration: z.int32().min(0).nullable().describe('Video/gif duration in milliseconds (null for static images)'),
|
||||
livePhotoVideoId: z.string().nullish().describe('Live photo video ID'),
|
||||
hasMetadata: z.boolean().describe('Whether asset has metadata'),
|
||||
width: z.number().min(0).nullable().describe('Asset width'),
|
||||
@@ -136,7 +136,7 @@ export type MapAsset = {
|
||||
checksum: Buffer<ArrayBufferLike>;
|
||||
checksumAlgorithm: ChecksumAlgorithm;
|
||||
duplicateId: string | null;
|
||||
duration: string | null;
|
||||
duration: number | null;
|
||||
edits?: ShallowDehydrateObject<AssetEditActionItem>[];
|
||||
exifInfo?: ShallowDehydrateObject<Selectable<Exif>> | null;
|
||||
faces?: ShallowDehydrateObject<AssetFace>[];
|
||||
|
||||
@@ -90,6 +90,30 @@ const SyncAssetV1Schema = z
|
||||
})
|
||||
.meta({ id: 'SyncAssetV1' });
|
||||
|
||||
const SyncAssetV2Schema = z
|
||||
.object({
|
||||
id: z.string().describe('Asset ID'),
|
||||
ownerId: z.string().describe('Owner ID'),
|
||||
originalFileName: z.string().describe('Original file name'),
|
||||
thumbhash: z.string().nullable().describe('Thumbhash'),
|
||||
checksum: z.string().describe('Checksum'),
|
||||
fileCreatedAt: isoDatetimeToDate.nullable().describe('File created at'),
|
||||
fileModifiedAt: isoDatetimeToDate.nullable().describe('File modified at'),
|
||||
localDateTime: isoDatetimeToDate.nullable().describe('Local date time'),
|
||||
duration: z.int32().min(0).nullable().describe('Duration'),
|
||||
type: AssetTypeSchema,
|
||||
deletedAt: isoDatetimeToDate.nullable().describe('Deleted at'),
|
||||
isFavorite: z.boolean().describe('Is favorite'),
|
||||
visibility: AssetVisibilitySchema,
|
||||
livePhotoVideoId: z.string().nullable().describe('Live photo video ID'),
|
||||
stackId: z.string().nullable().describe('Stack ID'),
|
||||
libraryId: z.string().nullable().describe('Library ID'),
|
||||
width: z.int().nullable().describe('Asset width'),
|
||||
height: z.int().nullable().describe('Asset height'),
|
||||
isEdited: z.boolean().describe('Is edited'),
|
||||
})
|
||||
.meta({ id: 'SyncAssetV2' });
|
||||
|
||||
@ExtraModel()
|
||||
class SyncUserV1 extends createZodDto(SyncUserV1Schema) {}
|
||||
@ExtraModel()
|
||||
@@ -102,6 +126,8 @@ class SyncPartnerV1 extends createZodDto(SyncPartnerV1Schema) {}
|
||||
class SyncPartnerDeleteV1 extends createZodDto(SyncPartnerDeleteV1Schema) {}
|
||||
@ExtraModel()
|
||||
export class SyncAssetV1 extends createZodDto(SyncAssetV1Schema) {}
|
||||
@ExtraModel()
|
||||
export class SyncAssetV2 extends createZodDto(SyncAssetV2Schema) {}
|
||||
|
||||
const SyncAssetDeleteV1Schema = z
|
||||
.object({ assetId: z.string().describe('Asset ID') })
|
||||
@@ -394,12 +420,6 @@ class SyncPersonDeleteV1 extends createZodDto(SyncPersonDeleteV1Schema) {}
|
||||
class SyncAssetFaceV1 extends createZodDto(SyncAssetFaceV1Schema) {}
|
||||
@ExtraModel()
|
||||
class SyncAssetFaceV2 extends createZodDto(SyncAssetFaceV2Schema) {}
|
||||
|
||||
export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 {
|
||||
const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2;
|
||||
|
||||
return faceV1;
|
||||
}
|
||||
@ExtraModel()
|
||||
class SyncAssetFaceDeleteV1 extends createZodDto(SyncAssetFaceDeleteV1Schema) {}
|
||||
@ExtraModel()
|
||||
@@ -419,15 +439,15 @@ export type SyncItem = {
|
||||
[SyncEntityType.UserDeleteV1]: SyncUserDeleteV1;
|
||||
[SyncEntityType.PartnerV1]: SyncPartnerV1;
|
||||
[SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1;
|
||||
[SyncEntityType.AssetV1]: SyncAssetV1;
|
||||
[SyncEntityType.AssetV2]: SyncAssetV2;
|
||||
[SyncEntityType.AssetDeleteV1]: SyncAssetDeleteV1;
|
||||
[SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1;
|
||||
[SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1;
|
||||
[SyncEntityType.AssetExifV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AssetEditV1]: SyncAssetEditV1;
|
||||
[SyncEntityType.AssetEditDeleteV1]: SyncAssetEditDeleteV1;
|
||||
[SyncEntityType.PartnerAssetV1]: SyncAssetV1;
|
||||
[SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1;
|
||||
[SyncEntityType.PartnerAssetV2]: SyncAssetV2;
|
||||
[SyncEntityType.PartnerAssetBackfillV2]: SyncAssetV2;
|
||||
[SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1;
|
||||
[SyncEntityType.PartnerAssetExifV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.PartnerAssetExifBackfillV1]: SyncAssetExifV1;
|
||||
@@ -437,9 +457,9 @@ export type SyncItem = {
|
||||
[SyncEntityType.AlbumUserV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
||||
[SyncEntityType.AlbumAssetCreateV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetUpdateV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetBackfillV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetCreateV2]: SyncAssetV2;
|
||||
[SyncEntityType.AlbumAssetUpdateV2]: SyncAssetV2;
|
||||
[SyncEntityType.AlbumAssetBackfillV2]: SyncAssetV2;
|
||||
[SyncEntityType.AlbumAssetExifCreateV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumAssetExifUpdateV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumAssetExifBackfillV1]: SyncAssetExifV1;
|
||||
|
||||
@@ -89,8 +89,8 @@ const TimeBucketAssetResponseSchema = z
|
||||
"Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.",
|
||||
),
|
||||
duration: z
|
||||
.array(z.string().nullable())
|
||||
.describe('Array of video/gif durations in hh:mm:ss.SSS format (null for static images)'),
|
||||
.array(z.int32().min(0).nullable())
|
||||
.describe('Array of video/gif durations in milliseconds (null for static images)'),
|
||||
stack: z
|
||||
.array(stackTupleSchema)
|
||||
.optional()
|
||||
|
||||
@@ -801,9 +801,13 @@ export enum SyncRequestType {
|
||||
AlbumsV2 = 'AlbumsV2',
|
||||
AlbumUsersV1 = 'AlbumUsersV1',
|
||||
AlbumToAssetsV1 = 'AlbumToAssetsV1',
|
||||
/** @deprecated */
|
||||
AlbumAssetsV1 = 'AlbumAssetsV1',
|
||||
AlbumAssetsV2 = 'AlbumAssetsV2',
|
||||
AlbumAssetExifsV1 = 'AlbumAssetExifsV1',
|
||||
/** @deprecated */
|
||||
AssetsV1 = 'AssetsV1',
|
||||
AssetsV2 = 'AssetsV2',
|
||||
AssetExifsV1 = 'AssetExifsV1',
|
||||
AssetEditsV1 = 'AssetEditsV1',
|
||||
AssetMetadataV1 = 'AssetMetadataV1',
|
||||
@@ -811,12 +815,15 @@ export enum SyncRequestType {
|
||||
MemoriesV1 = 'MemoriesV1',
|
||||
MemoryToAssetsV1 = 'MemoryToAssetsV1',
|
||||
PartnersV1 = 'PartnersV1',
|
||||
/** @deprecated */
|
||||
PartnerAssetsV1 = 'PartnerAssetsV1',
|
||||
PartnerAssetsV2 = 'PartnerAssetsV2',
|
||||
PartnerAssetExifsV1 = 'PartnerAssetExifsV1',
|
||||
PartnerStacksV1 = 'PartnerStacksV1',
|
||||
StacksV1 = 'StacksV1',
|
||||
UsersV1 = 'UsersV1',
|
||||
PeopleV1 = 'PeopleV1',
|
||||
/** @deprecated */
|
||||
AssetFacesV1 = 'AssetFacesV1',
|
||||
AssetFacesV2 = 'AssetFacesV2',
|
||||
UserMetadataV1 = 'UserMetadataV1',
|
||||
@@ -833,7 +840,9 @@ export enum SyncEntityType {
|
||||
UserV1 = 'UserV1',
|
||||
UserDeleteV1 = 'UserDeleteV1',
|
||||
|
||||
/** @deprecated */
|
||||
AssetV1 = 'AssetV1',
|
||||
AssetV2 = 'AssetV2',
|
||||
AssetDeleteV1 = 'AssetDeleteV1',
|
||||
AssetExifV1 = 'AssetExifV1',
|
||||
AssetEditV1 = 'AssetEditV1',
|
||||
@@ -844,8 +853,12 @@ export enum SyncEntityType {
|
||||
PartnerV1 = 'PartnerV1',
|
||||
PartnerDeleteV1 = 'PartnerDeleteV1',
|
||||
|
||||
/** @deprecated */
|
||||
PartnerAssetV1 = 'PartnerAssetV1',
|
||||
PartnerAssetV2 = 'PartnerAssetV2',
|
||||
/** @deprecated */
|
||||
PartnerAssetBackfillV1 = 'PartnerAssetBackfillV1',
|
||||
PartnerAssetBackfillV2 = 'PartnerAssetBackfillV2',
|
||||
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
||||
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
||||
PartnerAssetExifBackfillV1 = 'PartnerAssetExifBackfillV1',
|
||||
@@ -861,9 +874,15 @@ export enum SyncEntityType {
|
||||
AlbumUserBackfillV1 = 'AlbumUserBackfillV1',
|
||||
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
||||
|
||||
/** @deprecated */
|
||||
AlbumAssetCreateV1 = 'AlbumAssetCreateV1',
|
||||
AlbumAssetCreateV2 = 'AlbumAssetCreateV2',
|
||||
/** @deprecated */
|
||||
AlbumAssetUpdateV1 = 'AlbumAssetUpdateV1',
|
||||
AlbumAssetUpdateV2 = 'AlbumAssetUpdateV2',
|
||||
/** @deprecated */
|
||||
AlbumAssetBackfillV1 = 'AlbumAssetBackfillV1',
|
||||
AlbumAssetBackfillV2 = 'AlbumAssetBackfillV2',
|
||||
AlbumAssetExifCreateV1 = 'AlbumAssetExifCreateV1',
|
||||
AlbumAssetExifUpdateV1 = 'AlbumAssetExifUpdateV1',
|
||||
AlbumAssetExifBackfillV1 = 'AlbumAssetExifBackfillV1',
|
||||
|
||||
@@ -11,7 +11,7 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto';
|
||||
import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV2 } from 'src/dtos/sync.dto';
|
||||
import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
@@ -35,9 +35,9 @@ export interface ClientEventMap {
|
||||
on_notification: [NotificationDto];
|
||||
on_session_delete: [string];
|
||||
|
||||
AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }];
|
||||
AssetUploadReadyV2: [{ asset: SyncAssetV2; exif: SyncAssetExifV1 }];
|
||||
AppRestartV1: [AppRestartEvent];
|
||||
AssetEditReadyV1: [{ asset: SyncAssetV1; edit: SyncAssetEditV1[] }];
|
||||
AssetEditReadyV2: [{ asset: SyncAssetV2; edit: SyncAssetEditV1[] }];
|
||||
}
|
||||
|
||||
export type AuthFn = (client: Socket) => Promise<AuthDto>;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`
|
||||
ALTER TABLE asset
|
||||
ALTER COLUMN duration TYPE integer
|
||||
USING (
|
||||
CASE
|
||||
WHEN duration ~ '^\\d{2}:\\d{2}:\\d{2}\\.\\d{3}$'
|
||||
THEN substr(duration, 1, 2)::int * 3600000
|
||||
+ substr(duration, 4, 2)::int * 60000
|
||||
+ substr(duration, 7, 2)::int * 1000
|
||||
+ substr(duration, 10, 3)::int
|
||||
END
|
||||
);`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`
|
||||
ALTER TABLE asset
|
||||
ALTER COLUMN duration TYPE varchar
|
||||
USING (
|
||||
CASE
|
||||
WHEN duration IS NULL THEN NULL
|
||||
ELSE lpad((duration / 3600000)::text, 2, '0')
|
||||
|| ':' || lpad(((duration / 60000) % 60)::text, 2, '0')
|
||||
|| ':' || lpad(((duration / 1000) % 60)::text, 2, '0')
|
||||
|| '.' || lpad((duration % 1000)::text, 3, '0')
|
||||
END
|
||||
);`.execute(db);
|
||||
}
|
||||
@@ -83,8 +83,8 @@ export class AssetTable {
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isFavorite!: Generated<boolean>;
|
||||
|
||||
@Column({ type: 'character varying', nullable: true })
|
||||
duration!: string | null;
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
duration!: number | null;
|
||||
|
||||
@Column({ type: 'bytea', index: true })
|
||||
checksum!: Buffer; // sha1 checksum
|
||||
|
||||
@@ -101,7 +101,7 @@ export class JobService extends BaseService {
|
||||
const edits = await this.assetEditRepository.getWithSyncInfo(item.data.id);
|
||||
|
||||
if (asset) {
|
||||
this.websocketRepository.clientSend('AssetEditReadyV1', asset.ownerId, {
|
||||
this.websocketRepository.clientSend('AssetEditReadyV2', asset.ownerId, {
|
||||
asset: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
@@ -156,7 +156,7 @@ export class JobService extends BaseService {
|
||||
this.websocketRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
|
||||
if (asset.exifInfo) {
|
||||
const exif = asset.exifInfo;
|
||||
this.websocketRepository.clientSend('AssetUploadReadyV1', asset.ownerId, {
|
||||
this.websocketRepository.clientSend('AssetUploadReadyV2', asset.ownerId, {
|
||||
// TODO remove `on_upload_success` and then modify the query to select only the required fields)
|
||||
asset: {
|
||||
id: asset.id,
|
||||
|
||||
@@ -999,7 +999,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: asset.id,
|
||||
duration: '00:00:06.210',
|
||||
duration: 6210,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1067,7 +1067,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: asset.id,
|
||||
duration: '168:00:00.000',
|
||||
duration: 604_800_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -1080,7 +1080,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: 123_000 }));
|
||||
});
|
||||
|
||||
it('should prefer Duration from exif over sidecar', async () => {
|
||||
@@ -1092,7 +1092,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: 123_000 }));
|
||||
});
|
||||
|
||||
it('should ignore all Duration tags for definitely static images', async () => {
|
||||
@@ -1121,7 +1121,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' }));
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: 456_000 }));
|
||||
});
|
||||
|
||||
it('should trim whitespace from description', async () => {
|
||||
|
||||
@@ -1001,18 +1001,10 @@ export class MetadataService extends BaseService {
|
||||
return bitsPerSample;
|
||||
}
|
||||
|
||||
private getDuration(tags: ImmichTags): string | null {
|
||||
private getDuration(tags: ImmichTags): number | null {
|
||||
const duration = tags.Duration;
|
||||
|
||||
if (typeof duration === 'string') {
|
||||
return duration;
|
||||
}
|
||||
|
||||
if (typeof duration === 'number') {
|
||||
return Duration.fromObject({ seconds: duration }).toFormat('hh:mm:ss.SSS');
|
||||
}
|
||||
|
||||
return null;
|
||||
const seconds = typeof duration === 'number' ? duration : Number.parseFloat(duration as string);
|
||||
return Number.isFinite(seconds) ? Math.round(Duration.fromObject({ seconds }).toMillis()) : null;
|
||||
}
|
||||
|
||||
private async getVideoTags(originalPath: string) {
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
SyncAckDeleteDto,
|
||||
SyncAckSetDto,
|
||||
syncAlbumV2ToV1,
|
||||
syncAssetFaceV2ToV1,
|
||||
SyncAssetV1,
|
||||
SyncAssetV2,
|
||||
SyncItem,
|
||||
SyncStreamDto,
|
||||
} from 'src/dtos/sync.dto';
|
||||
@@ -22,7 +21,7 @@ import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync';
|
||||
|
||||
type CheckpointMap = Partial<Record<SyncEntityType, SyncAck>>;
|
||||
type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash'> & {
|
||||
type AssetLike = Omit<SyncAssetV2, 'checksum' | 'thumbhash'> & {
|
||||
checksum: Buffer<ArrayBufferLike>;
|
||||
thumbhash: Buffer<ArrayBufferLike> | null;
|
||||
};
|
||||
@@ -31,7 +30,7 @@ const COMPLETE_ID = 'complete';
|
||||
const MAX_DAYS = 30;
|
||||
const MAX_DURATION = Duration.fromObject({ days: MAX_DAYS });
|
||||
|
||||
const mapSyncAssetV1 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV1 => ({
|
||||
const mapSyncAssetV2 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV2 => ({
|
||||
...data,
|
||||
checksum: hexOrBufferToBase64(checksum),
|
||||
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
||||
@@ -56,10 +55,13 @@ export const SYNC_TYPES_ORDER = [
|
||||
SyncRequestType.UsersV1,
|
||||
SyncRequestType.PartnersV1,
|
||||
SyncRequestType.AssetsV1,
|
||||
SyncRequestType.AssetsV2,
|
||||
SyncRequestType.StacksV1,
|
||||
SyncRequestType.PartnerAssetsV1,
|
||||
SyncRequestType.PartnerAssetsV2,
|
||||
SyncRequestType.PartnerStacksV1,
|
||||
SyncRequestType.AlbumAssetsV1,
|
||||
SyncRequestType.AlbumAssetsV2,
|
||||
SyncRequestType.AlbumsV1,
|
||||
SyncRequestType.AlbumsV2,
|
||||
SyncRequestType.AlbumUsersV1,
|
||||
@@ -156,20 +158,26 @@ export class SyncService extends BaseService {
|
||||
const options: SyncQueryOptions = { nowId, userId: auth.user.id };
|
||||
|
||||
const handlers: Record<SyncRequestType, () => Promise<void>> = {
|
||||
// deprecated handlers
|
||||
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(),
|
||||
[SyncRequestType.AssetFacesV1]: () => this.syncAssetFacesV1(),
|
||||
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(),
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(),
|
||||
|
||||
[SyncRequestType.AuthUsersV1]: () => this.syncAuthUsersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetsV2]: () => this.syncAssetsV2(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetEditsV1]: () => this.syncAssetEditsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.PartnerAssetsV2]: () => this.syncPartnerAssetsV2(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AssetMetadataV1]: () => this.syncAssetMetadataV1(options, response, checkpointMap, auth),
|
||||
[SyncRequestType.PartnerAssetExifsV1]: () =>
|
||||
this.syncPartnerAssetExifsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumsV1]: () => this.syncAlbumsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AlbumsV2]: () => this.syncAlbumsV2(options, response, checkpointMap),
|
||||
[SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetsV2]: () => this.syncAlbumAssetsV2(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetExifsV1]: () =>
|
||||
this.syncAlbumAssetExifsV1(options, response, checkpointMap, session.id),
|
||||
@@ -178,13 +186,12 @@ export class SyncService extends BaseService {
|
||||
[SyncRequestType.StacksV1]: () => this.syncStackV1(options, response, checkpointMap),
|
||||
[SyncRequestType.PartnerStacksV1]: () => this.syncPartnerStackV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.PeopleV1]: () => this.syncPeopleV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetFacesV1]: async () => this.syncAssetFacesV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetFacesV2]: async () => this.syncAssetFacesV2(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetFacesV2]: () => this.syncAssetFacesV2(options, response, checkpointMap),
|
||||
[SyncRequestType.UserMetadataV1]: () => this.syncUserMetadataV1(options, response, checkpointMap),
|
||||
};
|
||||
} as const;
|
||||
|
||||
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
|
||||
const handler = handlers[type];
|
||||
const handler = handlers[type as keyof typeof handlers];
|
||||
await handler();
|
||||
}
|
||||
|
||||
@@ -260,21 +267,31 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAssetsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
private syncAssetsV1(): Promise<void> {
|
||||
throw new BadRequestException('SyncRequestType.AssetsV1 is deprecated, use SyncRequestType.AssetsV2 instead');
|
||||
}
|
||||
|
||||
private async syncAssetsV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
const deleteType = SyncEntityType.AssetDeleteV1;
|
||||
const deletes = this.syncRepository.asset.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upsertType = SyncEntityType.AssetV1;
|
||||
const upsertType = SyncEntityType.AssetV2;
|
||||
const upserts = this.syncRepository.asset.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncPartnerAssetsV1(
|
||||
private syncPartnerAssetsV1(): Promise<void> {
|
||||
throw new BadRequestException(
|
||||
'SyncRequestType.PartnerAssetsV1 is deprecated, use SyncRequestType.PartnerAssetsV2 instead',
|
||||
);
|
||||
}
|
||||
|
||||
private async syncPartnerAssetsV2(
|
||||
options: SyncQueryOptions,
|
||||
response: Writable,
|
||||
checkpointMap: CheckpointMap,
|
||||
@@ -286,13 +303,13 @@ export class SyncService extends BaseService {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const backfillType = SyncEntityType.PartnerAssetBackfillV1;
|
||||
const backfillType = SyncEntityType.PartnerAssetBackfillV2;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const partners = await this.syncRepository.partner.getCreatedAfter({
|
||||
...options,
|
||||
afterCreateId: backfillCheckpoint?.updateId,
|
||||
});
|
||||
const upsertType = SyncEntityType.PartnerAssetV1;
|
||||
const upsertType = SyncEntityType.PartnerAssetV2;
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
@@ -313,7 +330,7 @@ export class SyncService extends BaseService {
|
||||
send(response, {
|
||||
type: backfillType,
|
||||
ids: [createId, updateId],
|
||||
data: mapSyncAssetV1(data),
|
||||
data: mapSyncAssetV2(data),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -329,7 +346,7 @@ export class SyncService extends BaseService {
|
||||
|
||||
const upserts = this.syncRepository.partnerAsset.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,20 +507,26 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumAssetsV1(
|
||||
private syncAlbumAssetsV1(): Promise<void> {
|
||||
throw new BadRequestException(
|
||||
'SyncRequestType.AlbumAssetsV1 is deprecated, use SyncRequestType.AlbumAssetsV2 instead',
|
||||
);
|
||||
}
|
||||
|
||||
private async syncAlbumAssetsV2(
|
||||
options: SyncQueryOptions,
|
||||
response: Writable,
|
||||
checkpointMap: CheckpointMap,
|
||||
sessionId: string,
|
||||
) {
|
||||
const backfillType = SyncEntityType.AlbumAssetBackfillV1;
|
||||
const backfillType = SyncEntityType.AlbumAssetBackfillV2;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const albums = await this.syncRepository.album.getCreatedAfter({
|
||||
...options,
|
||||
afterCreateId: backfillCheckpoint?.updateId,
|
||||
});
|
||||
const updateType = SyncEntityType.AlbumAssetUpdateV1;
|
||||
const createType = SyncEntityType.AlbumAssetCreateV1;
|
||||
const updateType = SyncEntityType.AlbumAssetUpdateV2;
|
||||
const createType = SyncEntityType.AlbumAssetCreateV2;
|
||||
const updateCheckpoint = checkpointMap[updateType];
|
||||
const createCheckpoint = checkpointMap[createType];
|
||||
if (createCheckpoint) {
|
||||
@@ -522,7 +545,7 @@ export class SyncService extends BaseService {
|
||||
);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, { type: backfillType, ids: [createId, updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: backfillType, ids: [createId, updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
|
||||
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||
@@ -541,7 +564,7 @@ export class SyncService extends BaseService {
|
||||
createCheckpoint,
|
||||
);
|
||||
for await (const { updateId, ...data } of updates) {
|
||||
send(response, { type: updateType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: updateType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -552,12 +575,12 @@ export class SyncService extends BaseService {
|
||||
send(response, {
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
data: {},
|
||||
ackType: SyncEntityType.AlbumAssetUpdateV1,
|
||||
ackType: SyncEntityType.AlbumAssetUpdateV2,
|
||||
ids: [options.nowId],
|
||||
});
|
||||
first = false;
|
||||
}
|
||||
send(response, { type: createType, ids: [updateId], data: mapSyncAssetV1(data) });
|
||||
send(response, { type: createType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -802,19 +825,10 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAssetFacesV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
const deleteType = SyncEntityType.AssetFaceDeleteV1;
|
||||
const deletes = this.syncRepository.assetFace.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upsertType = SyncEntityType.AssetFaceV1;
|
||||
const upserts = this.syncRepository.assetFace.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
const v1 = syncAssetFaceV2ToV1(data);
|
||||
send(response, { type: upsertType, ids: [updateId], data: v1 });
|
||||
}
|
||||
private syncAssetFacesV1(): Promise<void> {
|
||||
throw new BadRequestException(
|
||||
'SyncRequestType.AssetFacesV1 is deprecated, use SyncRequestType.AssetFacesV2 instead',
|
||||
);
|
||||
}
|
||||
|
||||
private async syncAssetFacesV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
|
||||
@@ -16,7 +16,7 @@ const createAsset = (
|
||||
type: AssetType.Image,
|
||||
thumbhash: null,
|
||||
localDateTime: new Date().toISOString(),
|
||||
duration: '0:00:00.00000',
|
||||
duration: 0,
|
||||
hasMetadata: true,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
|
||||
@@ -15,13 +15,13 @@ const setup = async (db?: Kysely<DB>) => {
|
||||
};
|
||||
|
||||
const updateSyncAck = {
|
||||
ack: expect.stringContaining(SyncEntityType.AlbumAssetUpdateV1),
|
||||
ack: expect.stringContaining(SyncEntityType.AlbumAssetUpdateV2),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
};
|
||||
|
||||
const backfillSyncAck = {
|
||||
ack: expect.stringContaining(SyncEntityType.AlbumAssetBackfillV1),
|
||||
ack: expect.stringContaining(SyncEntityType.AlbumAssetBackfillV2),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
};
|
||||
@@ -30,7 +30,7 @@ beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
describe(SyncRequestType.AlbumAssetsV2, () => {
|
||||
it('should detect and sync the first album asset', async () => {
|
||||
const originalFileName = 'firstAsset';
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
@@ -48,7 +48,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
duration: '0:10:00.00000',
|
||||
duration: 600_000,
|
||||
livePhotoVideoId: null,
|
||||
stackId: null,
|
||||
libraryId: null,
|
||||
@@ -59,7 +59,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
expect(response).toEqual([
|
||||
updateSyncAck,
|
||||
{
|
||||
@@ -85,13 +85,13 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
height: asset.height,
|
||||
isEdited: asset.isEdited,
|
||||
},
|
||||
type: SyncEntityType.AlbumAssetCreateV1,
|
||||
type: SyncEntityType.AlbumAssetCreateV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
});
|
||||
|
||||
it('should sync album asset for own user', async () => {
|
||||
@@ -100,13 +100,13 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
const { album } = await ctx.newAlbum({ ownerId: auth.user.id });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV1 }),
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV2])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1])).resolves.toEqual([
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV2])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.SyncAckV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.AlbumAssetCreateV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.AlbumAssetCreateV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
});
|
||||
@@ -122,11 +122,11 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
const { session } = await ctx.newSession({ userId: user3.id });
|
||||
const authUser3 = factory.auth({ session, user: user3 });
|
||||
|
||||
await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV1 }),
|
||||
await expect(ctx.syncStream(authUser3, [SyncRequestType.AssetsV2])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
});
|
||||
|
||||
it('should backfill album assets when a user shares an album with you', async () => {
|
||||
@@ -147,7 +147,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
await wait(2);
|
||||
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
expect(response).toEqual([
|
||||
updateSyncAck,
|
||||
{
|
||||
@@ -155,7 +155,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
data: expect.objectContaining({
|
||||
id: asset2User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetCreateV1,
|
||||
type: SyncEntityType.AlbumAssetCreateV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
@@ -166,21 +166,21 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||
|
||||
// should backfill the album user
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: asset1User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||
type: SyncEntityType.AlbumAssetBackfillV2,
|
||||
},
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: asset2User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||
type: SyncEntityType.AlbumAssetBackfillV2,
|
||||
},
|
||||
backfillSyncAck,
|
||||
updateSyncAck,
|
||||
@@ -189,13 +189,13 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
data: expect.objectContaining({
|
||||
id: asset3User2.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetCreateV1,
|
||||
type: SyncEntityType.AlbumAssetCreateV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
});
|
||||
|
||||
it('should sync old assets when a user adds them to an album they share you', async () => {
|
||||
@@ -211,7 +211,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
await ctx.newAlbumAsset({ albumId: album1.id, assetId: album1Asset.id });
|
||||
await ctx.newAlbumUser({ albumId: album1.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||
|
||||
const firstAlbumResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
const firstAlbumResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
expect(firstAlbumResponse).toEqual([
|
||||
updateSyncAck,
|
||||
{
|
||||
@@ -219,7 +219,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
data: expect.objectContaining({
|
||||
id: album1Asset.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetCreateV1,
|
||||
type: SyncEntityType.AlbumAssetCreateV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
@@ -228,14 +228,14 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
|
||||
await ctx.newAlbumUser({ albumId: album2.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: firstAsset.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetBackfillV1,
|
||||
type: SyncEntityType.AlbumAssetBackfillV2,
|
||||
},
|
||||
backfillSyncAck,
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
@@ -248,7 +248,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
await wait(2);
|
||||
|
||||
// should backfill the new asset even though it's older than the first asset
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
expect(newResponse).toEqual([
|
||||
updateSyncAck,
|
||||
{
|
||||
@@ -256,13 +256,13 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
data: expect.objectContaining({
|
||||
id: secondAsset.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetCreateV1,
|
||||
type: SyncEntityType.AlbumAssetCreateV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
});
|
||||
|
||||
it('should sync asset updates for an album shared with you', async () => {
|
||||
@@ -274,7 +274,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await ctx.newAlbumUser({ albumId: album.id, userId: auth.user.id, role: AlbumUserRole.Editor });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
expect(response).toEqual([
|
||||
updateSyncAck,
|
||||
{
|
||||
@@ -282,7 +282,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
data: expect.objectContaining({
|
||||
id: asset.id,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetCreateV1,
|
||||
type: SyncEntityType.AlbumAssetCreateV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
@@ -296,7 +296,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
isFavorite: true,
|
||||
});
|
||||
|
||||
const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV1]);
|
||||
const updateResponse = await ctx.syncStream(auth, [SyncRequestType.AlbumAssetsV2]);
|
||||
expect(updateResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -304,7 +304,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||
id: asset.id,
|
||||
isFavorite: true,
|
||||
}),
|
||||
type: SyncEntityType.AlbumAssetUpdateV1,
|
||||
type: SyncEntityType.AlbumAssetUpdateV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
@@ -18,14 +18,14 @@ beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncEntityType.AssetFaceV1, () => {
|
||||
describe(SyncEntityType.AssetFaceV2, () => {
|
||||
it('should detect and sync the first asset face', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
const { person } = await ctx.newPerson({ ownerId: auth.user.id });
|
||||
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id, personId: person.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -41,13 +41,13 @@ describe(SyncEntityType.AssetFaceV1, () => {
|
||||
boundingBoxY2: assetFace.boundingBoxY2,
|
||||
sourceType: assetFace.sourceType,
|
||||
}),
|
||||
type: 'AssetFaceV1',
|
||||
type: 'AssetFaceV2',
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted asset face', async () => {
|
||||
@@ -57,7 +57,7 @@ describe(SyncEntityType.AssetFaceV1, () => {
|
||||
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id });
|
||||
await personRepo.deleteAssetFace(assetFace.id);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetFacesV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -70,7 +70,7 @@ describe(SyncEntityType.AssetFaceV1, () => {
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
|
||||
});
|
||||
|
||||
it('should not sync an asset face or asset face delete for an unrelated user', async () => {
|
||||
@@ -82,19 +82,19 @@ describe(SyncEntityType.AssetFaceV1, () => {
|
||||
const { assetFace } = await ctx.newAssetFace({ assetId: asset.id });
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetFaceV1 }),
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV2])).toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetFaceV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
|
||||
|
||||
await personRepo.deleteAssetFace(assetFace.id);
|
||||
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV1])).toEqual([
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetFacesV2])).toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetFaceDeleteV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetFacesV2]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncEntityType.AssetV1, () => {
|
||||
describe(SyncEntityType.AssetV2, () => {
|
||||
it('should detect and sync the first asset', async () => {
|
||||
const originalFileName = 'firstAsset';
|
||||
const checksum = '1115vHcVkZzNp3Q9G+FEA0nu6zUbGb4Tj4UOXkN0wRA=';
|
||||
@@ -35,13 +35,13 @@ describe(SyncEntityType.AssetV1, () => {
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
duration: '0:10:00.00000',
|
||||
duration: 600_000,
|
||||
libraryId: null,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
});
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -66,13 +66,13 @@ describe(SyncEntityType.AssetV1, () => {
|
||||
height: asset.height,
|
||||
isEdited: asset.isEdited,
|
||||
},
|
||||
type: 'AssetV1',
|
||||
type: 'AssetV2',
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV2]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted asset', async () => {
|
||||
@@ -81,7 +81,7 @@ describe(SyncEntityType.AssetV1, () => {
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -94,7 +94,7 @@ describe(SyncEntityType.AssetV1, () => {
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV2]);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for an unrelated user', async () => {
|
||||
@@ -105,17 +105,17 @@ describe(SyncEntityType.AssetV1, () => {
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV1 }),
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV2])).toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV2]);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).toEqual([
|
||||
expect(await ctx.syncStream(auth2, [SyncRequestType.AssetsV2])).toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV2]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ describe(SyncEntityType.SyncCompleteV1, () => {
|
||||
it('should work', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV2]);
|
||||
});
|
||||
|
||||
it('should detect an old checkpoint and send back a reset', async () => {
|
||||
@@ -39,7 +39,7 @@ describe(SyncEntityType.SyncCompleteV1, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV2]);
|
||||
expect(response).toEqual([{ type: SyncEntityType.SyncResetV1, data: {}, ack: 'SyncResetV1|reset' }]);
|
||||
});
|
||||
|
||||
@@ -55,6 +55,6 @@ describe(SyncEntityType.SyncCompleteV1, () => {
|
||||
},
|
||||
]);
|
||||
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV2]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ beforeAll(async () => {
|
||||
defaultDatabase = await getKyselyDB();
|
||||
});
|
||||
|
||||
describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
describe(SyncRequestType.PartnerAssetsV2, () => {
|
||||
it('should detect and sync the first partner asset', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
@@ -39,13 +39,13 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
fileModifiedAt: date,
|
||||
localDateTime: date,
|
||||
deletedAt: null,
|
||||
duration: '0:10:00.00000',
|
||||
duration: 600_000,
|
||||
libraryId: null,
|
||||
});
|
||||
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -70,13 +70,13 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
width: null,
|
||||
height: null,
|
||||
},
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
type: SyncEntityType.PartnerAssetV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
|
||||
it('should detect and sync a deleted partner asset', async () => {
|
||||
@@ -88,7 +88,7 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
@@ -101,7 +101,7 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
|
||||
it('should not sync a deleted partner asset due to a user delete', async () => {
|
||||
@@ -112,7 +112,7 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await ctx.newAsset({ ownerId: user2.id });
|
||||
await userRepo.delete({ id: user2.id }, true);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
|
||||
it('should not sync a deleted partner asset due to a partner delete (unshare)', async () => {
|
||||
@@ -122,12 +122,12 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
await ctx.newAsset({ ownerId: user2.id });
|
||||
const { partner } = await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.PartnerAssetV1 }),
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.PartnerAssetV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await partnerRepo.remove(partner);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for own user', async () => {
|
||||
@@ -138,19 +138,19 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
const { asset } = await ctx.newAsset({ ownerId: auth.user.id });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV1 }),
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV2])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV2])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
|
||||
it('should not sync an asset or asset delete for unrelated user', async () => {
|
||||
@@ -162,19 +162,19 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
const auth2 = factory.auth({ session, user: user2 });
|
||||
|
||||
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV1 }),
|
||||
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV2])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
|
||||
await assetRepo.remove(asset);
|
||||
|
||||
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||
await expect(ctx.syncStream(auth2, [SyncRequestType.AssetsV2])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetDeleteV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
|
||||
it('should backfill partner assets when a partner shared their library with you', async () => {
|
||||
@@ -187,14 +187,14 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
const { asset: assetUser2 } = await ctx.newAsset({ ownerId: user2.id });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
type: SyncEntityType.PartnerAssetV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
@@ -202,17 +202,17 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
await ctx.syncAckAll(auth, response);
|
||||
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetBackfillV1,
|
||||
type: SyncEntityType.PartnerAssetBackfillV2,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1),
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV2),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
@@ -220,7 +220,7 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
|
||||
it('should only backfill partner assets created prior to the current partner asset checkpoint', async () => {
|
||||
@@ -235,31 +235,31 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
const { asset: asset2User3 } = await ctx.newAsset({ ownerId: user3.id });
|
||||
await ctx.newPartner({ sharedById: user2.id, sharedWithId: auth.user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
expect(response).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser2.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
type: SyncEntityType.PartnerAssetV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
await ctx.newPartner({ sharedById: user3.id, sharedWithId: auth.user.id });
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.any(String),
|
||||
data: expect.objectContaining({
|
||||
id: assetUser3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetBackfillV1,
|
||||
type: SyncEntityType.PartnerAssetBackfillV2,
|
||||
},
|
||||
{
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV1),
|
||||
ack: expect.stringContaining(SyncEntityType.PartnerAssetBackfillV2),
|
||||
data: {},
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
},
|
||||
@@ -268,12 +268,12 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||
data: expect.objectContaining({
|
||||
id: asset2User3.id,
|
||||
}),
|
||||
type: SyncEntityType.PartnerAssetV1,
|
||||
type: SyncEntityType.PartnerAssetV2,
|
||||
},
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
await ctx.syncAckAll(auth, newResponse);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.PartnerAssetsV2]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ describe(SyncEntityType.SyncResetV1, () => {
|
||||
it('should work', async () => {
|
||||
const { auth, ctx } = await setup();
|
||||
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV1]);
|
||||
await ctx.assertSyncIsComplete(auth, [SyncRequestType.AssetsV2]);
|
||||
});
|
||||
|
||||
it('should detect a pending sync reset', async () => {
|
||||
@@ -31,7 +31,7 @@ describe(SyncEntityType.SyncResetV1, () => {
|
||||
isPendingSyncReset: true,
|
||||
});
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV2]);
|
||||
expect(response).toEqual([{ type: SyncEntityType.SyncResetV1, data: {}, ack: 'SyncResetV1|reset' }]);
|
||||
});
|
||||
|
||||
@@ -40,8 +40,8 @@ describe(SyncEntityType.SyncResetV1, () => {
|
||||
|
||||
await ctx.newAsset({ ownerId: user.id });
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV1 }),
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV2])).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
|
||||
@@ -49,7 +49,7 @@ describe(SyncEntityType.SyncResetV1, () => {
|
||||
isPendingSyncReset: true,
|
||||
});
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1])).resolves.toEqual([
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV2])).resolves.toEqual([
|
||||
{ type: SyncEntityType.SyncResetV1, data: {}, ack: 'SyncResetV1|reset' },
|
||||
]);
|
||||
});
|
||||
@@ -63,8 +63,8 @@ describe(SyncEntityType.SyncResetV1, () => {
|
||||
isPendingSyncReset: true,
|
||||
});
|
||||
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV1], true)).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV1 }),
|
||||
await expect(ctx.syncStream(auth, [SyncRequestType.AssetsV2], true)).resolves.toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
});
|
||||
@@ -74,20 +74,20 @@ describe(SyncEntityType.SyncResetV1, () => {
|
||||
|
||||
await ctx.newAsset({ ownerId: user.id });
|
||||
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV2]);
|
||||
await ctx.syncAckAll(auth, response);
|
||||
|
||||
await ctx.get(SessionRepository).update(auth.session!.id, {
|
||||
isPendingSyncReset: true,
|
||||
});
|
||||
|
||||
const resetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
const resetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV2]);
|
||||
|
||||
await ctx.syncAckAll(auth, resetResponse);
|
||||
|
||||
const postResetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||
const postResetResponse = await ctx.syncStream(auth, [SyncRequestType.AssetsV2]);
|
||||
expect(postResetResponse).toEqual([
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV1 }),
|
||||
expect.objectContaining({ type: SyncEntityType.AssetV2 }),
|
||||
expect.objectContaining({ type: SyncEntityType.SyncCompleteV1 }),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte';
|
||||
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
|
||||
import { getAssetMediaUrl, getAssetPlaybackUrl } from '$lib/utils';
|
||||
import { timeToSeconds } from '$lib/utils/date-time';
|
||||
import { moveFocus } from '$lib/utils/focus-util';
|
||||
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
@@ -274,7 +273,7 @@
|
||||
url={getAssetPlaybackUrl({ id: asset.id, cacheKey: asset.thumbhash })}
|
||||
enablePlayback={mouseOver && $playVideoThumbnailOnHover}
|
||||
curve={selected}
|
||||
durationInSeconds={asset.duration ? timeToSeconds(asset.duration) : 0}
|
||||
durationInSeconds={asset.duration ? asset.duration / 1000 : 0}
|
||||
playbackOnIconHover={!$playVideoThumbnailOnHover}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ export type TimelineAsset = {
|
||||
isVideo: boolean;
|
||||
isImage: boolean;
|
||||
stack: AssetStackResponseDto | null;
|
||||
duration: string | null;
|
||||
duration: number | null;
|
||||
projectionType: string | null;
|
||||
livePhotoVideoId: string | null;
|
||||
city: string | null;
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('utils', () => {
|
||||
originalPath: 'image.gif',
|
||||
originalMimeType: 'image/gif',
|
||||
type: AssetTypeEnum.Image,
|
||||
duration: '2.0',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
const url = getAssetUrl({ asset });
|
||||
@@ -65,7 +65,7 @@ describe('utils', () => {
|
||||
originalPath: 'image.webp',
|
||||
originalMimeType: 'image/webp',
|
||||
type: AssetTypeEnum.Image,
|
||||
duration: '2.0',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
const url = getAssetUrl({ asset });
|
||||
@@ -119,7 +119,7 @@ describe('utils', () => {
|
||||
originalPath: 'image.gif',
|
||||
originalMimeType: 'image/gif',
|
||||
type: AssetTypeEnum.Image,
|
||||
duration: '2.0',
|
||||
duration: 2000,
|
||||
});
|
||||
const sharedLink = sharedLinkFactory.build({ allowDownload: true, showMetadata: true, assets: [asset] });
|
||||
|
||||
@@ -134,7 +134,7 @@ describe('utils', () => {
|
||||
originalPath: 'image.gif',
|
||||
originalMimeType: 'image/gif',
|
||||
type: AssetTypeEnum.Image,
|
||||
duration: '2.0',
|
||||
duration: 2000,
|
||||
});
|
||||
const sharedLink = sharedLinkFactory.build({ allowDownload: false, assets: [asset] });
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('utils', () => {
|
||||
originalPath: 'image.gif',
|
||||
originalMimeType: 'image/gif',
|
||||
type: AssetTypeEnum.Image,
|
||||
duration: '2.0',
|
||||
duration: 2000,
|
||||
});
|
||||
const sharedLink = sharedLinkFactory.build({ showMetadata: false, assets: [asset] });
|
||||
|
||||
|
||||
@@ -1,53 +1,5 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { getAlbumDateRange, getShortDateRange, timeToSeconds } from './date-time';
|
||||
|
||||
describe('converting time to seconds', () => {
|
||||
it('parses hh:mm:ss correctly', () => {
|
||||
expect(timeToSeconds('01:02:03')).toBeCloseTo(3723);
|
||||
});
|
||||
|
||||
it('parses hh:mm:ss.SSS correctly', () => {
|
||||
expect(timeToSeconds('01:02:03.456')).toBeCloseTo(3723.456);
|
||||
});
|
||||
|
||||
it('parses h:m:s.S correctly', () => {
|
||||
expect(timeToSeconds('1:2:3.4')).toBe(0); // Non-standard format, Luxon returns NaN
|
||||
});
|
||||
|
||||
it('parses hhh:mm:ss.SSS correctly', () => {
|
||||
expect(timeToSeconds('100:02:03.456')).toBe(0); // Non-standard format, Luxon returns NaN
|
||||
});
|
||||
|
||||
it('ignores ignores double milliseconds hh:mm:ss.SSS.SSSSSS', () => {
|
||||
expect(timeToSeconds('01:02:03.456.123456')).toBe(0); // Non-standard format, Luxon returns NaN
|
||||
});
|
||||
|
||||
// Test edge cases that can cause crashes
|
||||
it('handles "0" string input', () => {
|
||||
expect(timeToSeconds('0')).toBe(0);
|
||||
});
|
||||
|
||||
it('handles empty string input', () => {
|
||||
expect(timeToSeconds('')).toBe(0);
|
||||
});
|
||||
|
||||
it('parses HH:MM format correctly', () => {
|
||||
expect(timeToSeconds('01:02')).toBe(3720); // 1 hour 2 minutes = 3720 seconds
|
||||
});
|
||||
|
||||
it('handles malformed time strings', () => {
|
||||
expect(timeToSeconds('invalid')).toBe(0);
|
||||
});
|
||||
|
||||
it('parses single hour format correctly', () => {
|
||||
expect(timeToSeconds('01')).toBe(3600); // Luxon interprets "01" as 1 hour
|
||||
});
|
||||
|
||||
it('handles time strings with invalid numbers', () => {
|
||||
expect(timeToSeconds('aa:bb:cc')).toBe(0);
|
||||
expect(timeToSeconds('01:bb:03')).toBe(0);
|
||||
});
|
||||
});
|
||||
import { getAlbumDateRange, getShortDateRange } from './date-time';
|
||||
|
||||
describe('getShortDateRange', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { DateTime } from 'luxon';
|
||||
import { get } from 'svelte/store';
|
||||
import { dateFormats } from '$lib/constants';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
|
||||
/**
|
||||
* Convert time like `01:02:03.456` to seconds.
|
||||
*/
|
||||
export function timeToSeconds(time: string) {
|
||||
if (!time || time === '0') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const seconds = Duration.fromISOTime(time).as('seconds');
|
||||
|
||||
return Number.isNaN(seconds) ? 0 : seconds;
|
||||
}
|
||||
export function parseUtcDate(date: string) {
|
||||
return DateTime.fromISO(date, { zone: 'UTC' }).toUTC();
|
||||
}
|
||||
|
||||
@@ -95,18 +95,10 @@
|
||||
};
|
||||
|
||||
const setProgressDuration = (asset: TimelineAsset) => {
|
||||
if (asset.isVideo) {
|
||||
const timeParts = asset.duration!.split(':').map(Number);
|
||||
const durationInMilliseconds = (timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]) * 1000;
|
||||
progressBarController = new Tween<number>(0, {
|
||||
duration: (from: number, to: number) => (to ? durationInMilliseconds * (to - from) : 0),
|
||||
});
|
||||
} else {
|
||||
progressBarController = new Tween<number>(0, {
|
||||
duration: (from: number, to: number) =>
|
||||
to ? authManager.preferences.memories.duration * 1000 * (to - from) : 0,
|
||||
});
|
||||
}
|
||||
progressBarController = new Tween<number>(0, {
|
||||
duration: (from: number, to: number) =>
|
||||
to ? (asset.isVideo ? asset.duration! : authManager.preferences.memories.duration * 1000) * (to - from) : 0,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNextAsset = () => handleNavigate(current?.next?.asset);
|
||||
|
||||
Reference in New Issue
Block a user