mirror of
https://github.com/immich-app/immich.git
synced 2025-12-16 09:13:13 +03:00
Compare commits
1 Commits
chore/log-
...
feat/asset
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
853943aba1 |
@@ -1,10 +1,8 @@
|
|||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
final RemoteAssetRepository _remoteAssetRepository;
|
final RemoteAssetRepository _remoteAssetRepository;
|
||||||
@@ -58,22 +56,11 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<double> getAspectRatio(BaseAsset asset) async {
|
Future<double> getAspectRatio(BaseAsset asset) async {
|
||||||
bool isFlipped;
|
|
||||||
double? width;
|
double? width;
|
||||||
double? height;
|
double? height;
|
||||||
|
|
||||||
if (asset.hasRemote) {
|
|
||||||
final exif = await getExif(asset);
|
|
||||||
isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation);
|
|
||||||
width = asset.width?.toDouble();
|
width = asset.width?.toDouble();
|
||||||
height = asset.height?.toDouble();
|
height = asset.height?.toDouble();
|
||||||
} else if (asset is LocalAsset) {
|
|
||||||
isFlipped = CurrentPlatform.isAndroid && (asset.orientation == 90 || asset.orientation == 270);
|
|
||||||
width = asset.width?.toDouble();
|
|
||||||
height = asset.height?.toDouble();
|
|
||||||
} else {
|
|
||||||
isFlipped = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (width == null || height == null) {
|
if (width == null || height == null) {
|
||||||
if (asset.hasRemote) {
|
if (asset.hasRemote) {
|
||||||
@@ -89,10 +76,8 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final orientedWidth = isFlipped ? height : width;
|
if (width != null && height != null && height > 0) {
|
||||||
final orientedHeight = isFlipped ? width : height;
|
return width / height;
|
||||||
if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) {
|
|
||||||
return orientedWidth / orientedHeight;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1.0;
|
return 1.0;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey;
|
import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole, UserMetadataKey;
|
||||||
import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey;
|
import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole, UserMetadataKey;
|
||||||
@@ -194,6 +195,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
livePhotoVideoId: Value(asset.livePhotoVideoId),
|
||||||
stackId: Value(asset.stackId),
|
stackId: Value(asset.stackId),
|
||||||
libraryId: Value(asset.libraryId),
|
libraryId: Value(asset.libraryId),
|
||||||
|
width: Value(asset.width),
|
||||||
|
height: Value(asset.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
batch.insert(
|
batch.insert(
|
||||||
@@ -245,10 +248,21 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
await _db.batch((batch) {
|
await _db.batch((batch) {
|
||||||
for (final exif in data) {
|
for (final exif in data) {
|
||||||
|
int? width;
|
||||||
|
int? height;
|
||||||
|
|
||||||
|
if (ExifDtoConverter.isOrientationFlipped(exif.orientation)) {
|
||||||
|
width = exif.exifImageHeight;
|
||||||
|
height = exif.exifImageWidth;
|
||||||
|
} else {
|
||||||
|
width = exif.exifImageWidth;
|
||||||
|
height = exif.exifImageHeight;
|
||||||
|
}
|
||||||
|
|
||||||
batch.update(
|
batch.update(
|
||||||
_db.remoteAssetEntity,
|
_db.remoteAssetEntity,
|
||||||
RemoteAssetEntityCompanion(width: Value(exif.exifImageWidth), height: Value(exif.exifImageHeight)),
|
RemoteAssetEntityCompanion(width: Value(width), height: Value(height)),
|
||||||
where: (row) => row.id.equals(exif.assetId),
|
where: (row) => row.id.equals(exif.assetId) & row.width.isNull() & row.height.isNull(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
34
mobile/openapi/lib/model/asset_response_dto.dart
generated
34
mobile/openapi/lib/model/asset_response_dto.dart
generated
@@ -23,6 +23,7 @@ class AssetResponseDto {
|
|||||||
required this.fileCreatedAt,
|
required this.fileCreatedAt,
|
||||||
required this.fileModifiedAt,
|
required this.fileModifiedAt,
|
||||||
required this.hasMetadata,
|
required this.hasMetadata,
|
||||||
|
required this.height,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.isArchived,
|
required this.isArchived,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
@@ -45,6 +46,7 @@ class AssetResponseDto {
|
|||||||
this.unassignedFaces = const [],
|
this.unassignedFaces = const [],
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
required this.visibility,
|
required this.visibility,
|
||||||
|
required this.width,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// base64 encoded sha1 hash
|
/// base64 encoded sha1 hash
|
||||||
@@ -77,6 +79,8 @@ class AssetResponseDto {
|
|||||||
|
|
||||||
bool hasMetadata;
|
bool hasMetadata;
|
||||||
|
|
||||||
|
num? height;
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
bool isArchived;
|
bool isArchived;
|
||||||
@@ -141,6 +145,8 @@ class AssetResponseDto {
|
|||||||
|
|
||||||
AssetVisibility visibility;
|
AssetVisibility visibility;
|
||||||
|
|
||||||
|
num? width;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
|
||||||
other.checksum == checksum &&
|
other.checksum == checksum &&
|
||||||
@@ -153,6 +159,7 @@ class AssetResponseDto {
|
|||||||
other.fileCreatedAt == fileCreatedAt &&
|
other.fileCreatedAt == fileCreatedAt &&
|
||||||
other.fileModifiedAt == fileModifiedAt &&
|
other.fileModifiedAt == fileModifiedAt &&
|
||||||
other.hasMetadata == hasMetadata &&
|
other.hasMetadata == hasMetadata &&
|
||||||
|
other.height == height &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isArchived == isArchived &&
|
other.isArchived == isArchived &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
@@ -174,7 +181,8 @@ class AssetResponseDto {
|
|||||||
other.type == type &&
|
other.type == type &&
|
||||||
_deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
|
_deepEquality.equals(other.unassignedFaces, unassignedFaces) &&
|
||||||
other.updatedAt == updatedAt &&
|
other.updatedAt == updatedAt &&
|
||||||
other.visibility == visibility;
|
other.visibility == visibility &&
|
||||||
|
other.width == width;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@@ -189,6 +197,7 @@ class AssetResponseDto {
|
|||||||
(fileCreatedAt.hashCode) +
|
(fileCreatedAt.hashCode) +
|
||||||
(fileModifiedAt.hashCode) +
|
(fileModifiedAt.hashCode) +
|
||||||
(hasMetadata.hashCode) +
|
(hasMetadata.hashCode) +
|
||||||
|
(height == null ? 0 : height!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isArchived.hashCode) +
|
(isArchived.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
(isFavorite.hashCode) +
|
||||||
@@ -210,10 +219,11 @@ class AssetResponseDto {
|
|||||||
(type.hashCode) +
|
(type.hashCode) +
|
||||||
(unassignedFaces.hashCode) +
|
(unassignedFaces.hashCode) +
|
||||||
(updatedAt.hashCode) +
|
(updatedAt.hashCode) +
|
||||||
(visibility.hashCode);
|
(visibility.hashCode) +
|
||||||
|
(width == null ? 0 : width!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility]';
|
String toString() => 'AssetResponseDto[checksum=$checksum, createdAt=$createdAt, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, height=$height, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt, visibility=$visibility, width=$width]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -235,6 +245,11 @@ class AssetResponseDto {
|
|||||||
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String();
|
json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String();
|
||||||
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String();
|
json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String();
|
||||||
json[r'hasMetadata'] = this.hasMetadata;
|
json[r'hasMetadata'] = this.hasMetadata;
|
||||||
|
if (this.height != null) {
|
||||||
|
json[r'height'] = this.height;
|
||||||
|
} else {
|
||||||
|
// json[r'height'] = null;
|
||||||
|
}
|
||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
json[r'isArchived'] = this.isArchived;
|
json[r'isArchived'] = this.isArchived;
|
||||||
json[r'isFavorite'] = this.isFavorite;
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
@@ -285,6 +300,11 @@ class AssetResponseDto {
|
|||||||
json[r'unassignedFaces'] = this.unassignedFaces;
|
json[r'unassignedFaces'] = this.unassignedFaces;
|
||||||
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
|
||||||
json[r'visibility'] = this.visibility;
|
json[r'visibility'] = this.visibility;
|
||||||
|
if (this.width != null) {
|
||||||
|
json[r'width'] = this.width;
|
||||||
|
} else {
|
||||||
|
// json[r'width'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +327,9 @@ class AssetResponseDto {
|
|||||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
|
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!,
|
||||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
|
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!,
|
||||||
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
|
hasMetadata: mapValueOfType<bool>(json, r'hasMetadata')!,
|
||||||
|
height: json[r'height'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'height']}'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isArchived: mapValueOfType<bool>(json, r'isArchived')!,
|
isArchived: mapValueOfType<bool>(json, r'isArchived')!,
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
@@ -329,6 +352,9 @@ class AssetResponseDto {
|
|||||||
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
|
unassignedFaces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'unassignedFaces']),
|
||||||
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
updatedAt: mapDateTime(json, r'updatedAt', r'')!,
|
||||||
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
||||||
|
width: json[r'width'] == null
|
||||||
|
? null
|
||||||
|
: num.parse('${json[r'width']}'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -384,6 +410,7 @@ class AssetResponseDto {
|
|||||||
'fileCreatedAt',
|
'fileCreatedAt',
|
||||||
'fileModifiedAt',
|
'fileModifiedAt',
|
||||||
'hasMetadata',
|
'hasMetadata',
|
||||||
|
'height',
|
||||||
'id',
|
'id',
|
||||||
'isArchived',
|
'isArchived',
|
||||||
'isFavorite',
|
'isFavorite',
|
||||||
@@ -397,6 +424,7 @@ class AssetResponseDto {
|
|||||||
'type',
|
'type',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'visibility',
|
'visibility',
|
||||||
|
'width',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
mobile/openapi/lib/model/sync_asset_v1.dart
generated
30
mobile/openapi/lib/model/sync_asset_v1.dart
generated
@@ -18,6 +18,7 @@ class SyncAssetV1 {
|
|||||||
required this.duration,
|
required this.duration,
|
||||||
required this.fileCreatedAt,
|
required this.fileCreatedAt,
|
||||||
required this.fileModifiedAt,
|
required this.fileModifiedAt,
|
||||||
|
required this.height,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.isFavorite,
|
required this.isFavorite,
|
||||||
required this.libraryId,
|
required this.libraryId,
|
||||||
@@ -29,6 +30,7 @@ class SyncAssetV1 {
|
|||||||
required this.thumbhash,
|
required this.thumbhash,
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.visibility,
|
required this.visibility,
|
||||||
|
required this.width,
|
||||||
});
|
});
|
||||||
|
|
||||||
String checksum;
|
String checksum;
|
||||||
@@ -41,6 +43,8 @@ class SyncAssetV1 {
|
|||||||
|
|
||||||
DateTime? fileModifiedAt;
|
DateTime? fileModifiedAt;
|
||||||
|
|
||||||
|
int? height;
|
||||||
|
|
||||||
String id;
|
String id;
|
||||||
|
|
||||||
bool isFavorite;
|
bool isFavorite;
|
||||||
@@ -63,6 +67,8 @@ class SyncAssetV1 {
|
|||||||
|
|
||||||
AssetVisibility visibility;
|
AssetVisibility visibility;
|
||||||
|
|
||||||
|
int? width;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 &&
|
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 &&
|
||||||
other.checksum == checksum &&
|
other.checksum == checksum &&
|
||||||
@@ -70,6 +76,7 @@ class SyncAssetV1 {
|
|||||||
other.duration == duration &&
|
other.duration == duration &&
|
||||||
other.fileCreatedAt == fileCreatedAt &&
|
other.fileCreatedAt == fileCreatedAt &&
|
||||||
other.fileModifiedAt == fileModifiedAt &&
|
other.fileModifiedAt == fileModifiedAt &&
|
||||||
|
other.height == height &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.libraryId == libraryId &&
|
other.libraryId == libraryId &&
|
||||||
@@ -80,7 +87,8 @@ class SyncAssetV1 {
|
|||||||
other.stackId == stackId &&
|
other.stackId == stackId &&
|
||||||
other.thumbhash == thumbhash &&
|
other.thumbhash == thumbhash &&
|
||||||
other.type == type &&
|
other.type == type &&
|
||||||
other.visibility == visibility;
|
other.visibility == visibility &&
|
||||||
|
other.width == width;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@@ -90,6 +98,7 @@ class SyncAssetV1 {
|
|||||||
(duration == null ? 0 : duration!.hashCode) +
|
(duration == null ? 0 : duration!.hashCode) +
|
||||||
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
|
(fileCreatedAt == null ? 0 : fileCreatedAt!.hashCode) +
|
||||||
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
|
||||||
|
(height == null ? 0 : height!.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(isFavorite.hashCode) +
|
(isFavorite.hashCode) +
|
||||||
(libraryId == null ? 0 : libraryId!.hashCode) +
|
(libraryId == null ? 0 : libraryId!.hashCode) +
|
||||||
@@ -100,10 +109,11 @@ class SyncAssetV1 {
|
|||||||
(stackId == null ? 0 : stackId!.hashCode) +
|
(stackId == null ? 0 : stackId!.hashCode) +
|
||||||
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
(thumbhash == null ? 0 : thumbhash!.hashCode) +
|
||||||
(type.hashCode) +
|
(type.hashCode) +
|
||||||
(visibility.hashCode);
|
(visibility.hashCode) +
|
||||||
|
(width == null ? 0 : width!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, ownerId=$ownerId, stackId=$stackId, thumbhash=$thumbhash, type=$type, visibility=$visibility]';
|
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, duration=$duration, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, height=$height, id=$id, 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() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -127,6 +137,11 @@ class SyncAssetV1 {
|
|||||||
json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String();
|
json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String();
|
||||||
} else {
|
} else {
|
||||||
// json[r'fileModifiedAt'] = null;
|
// 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'id'] = this.id;
|
||||||
json[r'isFavorite'] = this.isFavorite;
|
json[r'isFavorite'] = this.isFavorite;
|
||||||
@@ -159,6 +174,11 @@ class SyncAssetV1 {
|
|||||||
}
|
}
|
||||||
json[r'type'] = this.type;
|
json[r'type'] = this.type;
|
||||||
json[r'visibility'] = this.visibility;
|
json[r'visibility'] = this.visibility;
|
||||||
|
if (this.width != null) {
|
||||||
|
json[r'width'] = this.width;
|
||||||
|
} else {
|
||||||
|
// json[r'width'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +196,7 @@ class SyncAssetV1 {
|
|||||||
duration: mapValueOfType<String>(json, r'duration'),
|
duration: mapValueOfType<String>(json, r'duration'),
|
||||||
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''),
|
fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''),
|
||||||
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
|
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
|
||||||
|
height: mapValueOfType<int>(json, r'height'),
|
||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
|
||||||
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
libraryId: mapValueOfType<String>(json, r'libraryId'),
|
||||||
@@ -187,6 +208,7 @@ class SyncAssetV1 {
|
|||||||
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
|
||||||
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
type: AssetTypeEnum.fromJson(json[r'type'])!,
|
||||||
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
visibility: AssetVisibility.fromJson(json[r'visibility'])!,
|
||||||
|
width: mapValueOfType<int>(json, r'width'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -239,6 +261,7 @@ class SyncAssetV1 {
|
|||||||
'duration',
|
'duration',
|
||||||
'fileCreatedAt',
|
'fileCreatedAt',
|
||||||
'fileModifiedAt',
|
'fileModifiedAt',
|
||||||
|
'height',
|
||||||
'id',
|
'id',
|
||||||
'isFavorite',
|
'isFavorite',
|
||||||
'libraryId',
|
'libraryId',
|
||||||
@@ -250,6 +273,7 @@ class SyncAssetV1 {
|
|||||||
'thumbhash',
|
'thumbhash',
|
||||||
'type',
|
'type',
|
||||||
'visibility',
|
'visibility',
|
||||||
|
'width',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
185
mobile/test/domain/repositories/sync_stream_repository_test.dart
Normal file
185
mobile/test/domain/repositories/sync_stream_repository_test.dart
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import 'package:drift/drift.dart' as drift;
|
||||||
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
SyncUserV1 _createUser({String id = 'user-1'}) {
|
||||||
|
return SyncUserV1(
|
||||||
|
id: id,
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@test.com',
|
||||||
|
deletedAt: null,
|
||||||
|
avatarColor: null,
|
||||||
|
hasProfileImage: false,
|
||||||
|
profileChangedAt: DateTime(2024, 1, 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncAssetV1 _createAsset({
|
||||||
|
required String id,
|
||||||
|
required String checksum,
|
||||||
|
required String fileName,
|
||||||
|
String ownerId = 'user-1',
|
||||||
|
int? width,
|
||||||
|
int? height,
|
||||||
|
}) {
|
||||||
|
return SyncAssetV1(
|
||||||
|
id: id,
|
||||||
|
checksum: checksum,
|
||||||
|
originalFileName: fileName,
|
||||||
|
type: AssetTypeEnum.IMAGE,
|
||||||
|
ownerId: ownerId,
|
||||||
|
isFavorite: false,
|
||||||
|
fileCreatedAt: DateTime(2024, 1, 1),
|
||||||
|
fileModifiedAt: DateTime(2024, 1, 1),
|
||||||
|
localDateTime: DateTime(2024, 1, 1),
|
||||||
|
visibility: AssetVisibility.timeline,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
deletedAt: null,
|
||||||
|
duration: null,
|
||||||
|
libraryId: null,
|
||||||
|
livePhotoVideoId: null,
|
||||||
|
stackId: null,
|
||||||
|
thumbhash: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncAssetExifV1 _createExif({
|
||||||
|
required String assetId,
|
||||||
|
required int width,
|
||||||
|
required int height,
|
||||||
|
required String orientation,
|
||||||
|
}) {
|
||||||
|
return SyncAssetExifV1(
|
||||||
|
assetId: assetId,
|
||||||
|
exifImageWidth: width,
|
||||||
|
exifImageHeight: height,
|
||||||
|
orientation: orientation,
|
||||||
|
city: null,
|
||||||
|
country: null,
|
||||||
|
dateTimeOriginal: null,
|
||||||
|
description: null,
|
||||||
|
exposureTime: null,
|
||||||
|
fNumber: null,
|
||||||
|
fileSizeInByte: null,
|
||||||
|
focalLength: null,
|
||||||
|
fps: null,
|
||||||
|
iso: null,
|
||||||
|
latitude: null,
|
||||||
|
lensModel: null,
|
||||||
|
longitude: null,
|
||||||
|
make: null,
|
||||||
|
model: null,
|
||||||
|
modifyDate: null,
|
||||||
|
profileDescription: null,
|
||||||
|
projectionType: null,
|
||||||
|
rating: null,
|
||||||
|
state: null,
|
||||||
|
timeZone: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late Drift db;
|
||||||
|
late SyncStreamRepository sut;
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||||
|
sut = SyncStreamRepository(db);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() async {
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('SyncStreamRepository - Dimension swapping based on orientation', () {
|
||||||
|
test('swaps dimensions for asset with rotated orientation', () async {
|
||||||
|
final flippedOrientations = ['5', '6', '7', '8', '90', '-90'];
|
||||||
|
|
||||||
|
for (final orientation in flippedOrientations) {
|
||||||
|
final assetId = 'asset-$orientation-degrees';
|
||||||
|
|
||||||
|
await sut.updateUsersV1([_createUser()]);
|
||||||
|
|
||||||
|
final asset = _createAsset(
|
||||||
|
id: assetId,
|
||||||
|
checksum: 'checksum-$orientation',
|
||||||
|
fileName: 'rotated_$orientation.jpg',
|
||||||
|
);
|
||||||
|
await sut.updateAssetsV1([asset]);
|
||||||
|
|
||||||
|
final exif = _createExif(
|
||||||
|
assetId: assetId,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
orientation: orientation, // EXIF orientation value for 90 degrees CW
|
||||||
|
);
|
||||||
|
await sut.updateAssetsExifV1([exif]);
|
||||||
|
|
||||||
|
final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId));
|
||||||
|
final result = await query.getSingle();
|
||||||
|
|
||||||
|
expect(result.width, equals(1080));
|
||||||
|
expect(result.height, equals(1920));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not swap dimensions for asset with normal orientation', () async {
|
||||||
|
final nonFlippedOrientations = ['1', '2', '3', '4'];
|
||||||
|
for (final orientation in nonFlippedOrientations) {
|
||||||
|
final assetId = 'asset-$orientation-degrees';
|
||||||
|
|
||||||
|
await sut.updateUsersV1([_createUser()]);
|
||||||
|
|
||||||
|
final asset = _createAsset(id: assetId, checksum: 'checksum-$orientation', fileName: 'normal_$orientation.jpg');
|
||||||
|
await sut.updateAssetsV1([asset]);
|
||||||
|
|
||||||
|
final exif = _createExif(
|
||||||
|
assetId: assetId,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
orientation: orientation, // EXIF orientation value for normal
|
||||||
|
);
|
||||||
|
await sut.updateAssetsExifV1([exif]);
|
||||||
|
|
||||||
|
final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId));
|
||||||
|
final result = await query.getSingle();
|
||||||
|
|
||||||
|
expect(result.width, equals(1920));
|
||||||
|
expect(result.height, equals(1080));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not update dimensions if asset already has width and height', () async {
|
||||||
|
const assetId = 'asset-with-dimensions';
|
||||||
|
const existingWidth = 1920;
|
||||||
|
const existingHeight = 1080;
|
||||||
|
const exifWidth = 3840;
|
||||||
|
const exifHeight = 2160;
|
||||||
|
|
||||||
|
await sut.updateUsersV1([_createUser()]);
|
||||||
|
|
||||||
|
final asset = _createAsset(
|
||||||
|
id: assetId,
|
||||||
|
checksum: 'checksum-with-dims',
|
||||||
|
fileName: 'with_dimensions.jpg',
|
||||||
|
width: existingWidth,
|
||||||
|
height: existingHeight,
|
||||||
|
);
|
||||||
|
await sut.updateAssetsV1([asset]);
|
||||||
|
|
||||||
|
final exif = _createExif(assetId: assetId, width: exifWidth, height: exifHeight, orientation: '6');
|
||||||
|
await sut.updateAssetsExifV1([exif]);
|
||||||
|
|
||||||
|
// Verify the asset still has original dimensions (not updated from EXIF)
|
||||||
|
final query = db.remoteAssetEntity.select()..where((tbl) => tbl.id.equals(assetId));
|
||||||
|
final result = await query.getSingle();
|
||||||
|
|
||||||
|
expect(result.width, equals(existingWidth), reason: 'Width should remain as originally set');
|
||||||
|
expect(result.height, equals(existingHeight), reason: 'Height should remain as originally set');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/asset.service.dart';
|
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||||
@@ -22,42 +21,6 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('getAspectRatio', () {
|
group('getAspectRatio', () {
|
||||||
test('flips dimensions on Android for 90° and 270° orientations', () async {
|
|
||||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
|
||||||
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
||||||
|
|
||||||
for (final orientation in [90, 270]) {
|
|
||||||
final localAsset = TestUtils.createLocalAsset(
|
|
||||||
id: 'local-$orientation',
|
|
||||||
width: 1920,
|
|
||||||
height: 1080,
|
|
||||||
orientation: orientation,
|
|
||||||
);
|
|
||||||
|
|
||||||
final result = await sut.getAspectRatio(localAsset);
|
|
||||||
|
|
||||||
expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip on Android');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('does not flip dimensions on iOS regardless of orientation', () async {
|
|
||||||
debugDefaultTargetPlatformOverride = TargetPlatform.iOS;
|
|
||||||
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
|
||||||
|
|
||||||
for (final orientation in [0, 90, 270]) {
|
|
||||||
final localAsset = TestUtils.createLocalAsset(
|
|
||||||
id: 'local-$orientation',
|
|
||||||
width: 1920,
|
|
||||||
height: 1080,
|
|
||||||
orientation: orientation,
|
|
||||||
);
|
|
||||||
|
|
||||||
final result = await sut.getAspectRatio(localAsset);
|
|
||||||
|
|
||||||
expect(result, 1920 / 1080, reason: 'iOS should never flip dimensions');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fetches dimensions from remote repository when missing from asset', () async {
|
test('fetches dimensions from remote repository when missing from asset', () async {
|
||||||
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
|
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
|
||||||
|
|
||||||
@@ -112,54 +75,23 @@ void main() {
|
|||||||
expect(result, 1.0);
|
expect(result, 1.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles local asset with remoteId and uses exif from remote', () async {
|
test('handles local asset with remoteId and uses remote dimensions', () async {
|
||||||
final localAsset = TestUtils.createLocalAsset(
|
final localAsset = TestUtils.createLocalAsset(
|
||||||
id: 'local-1',
|
id: 'local-1',
|
||||||
remoteId: 'remote-1',
|
remoteId: 'remote-1',
|
||||||
width: 1920,
|
width: null,
|
||||||
height: 1080,
|
height: null,
|
||||||
orientation: 0,
|
orientation: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
final exif = const ExifInfo(orientation: '6');
|
when(
|
||||||
|
() => mockRemoteAssetRepository.get('remote-1'),
|
||||||
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
|
).thenAnswer((_) async => TestUtils.createRemoteAsset(id: 'remote-1', width: 1920, height: 1080));
|
||||||
|
|
||||||
final result = await sut.getAspectRatio(localAsset);
|
final result = await sut.getAspectRatio(localAsset);
|
||||||
|
verify(() => mockRemoteAssetRepository.get('remote-1')).called(1);
|
||||||
|
|
||||||
expect(result, 1080 / 1920);
|
expect(result, 1920 / 1080);
|
||||||
});
|
|
||||||
|
|
||||||
test('handles various flipped EXIF orientations correctly', () async {
|
|
||||||
final flippedOrientations = ['5', '6', '7', '8', '90', '-90'];
|
|
||||||
|
|
||||||
for (final orientation in flippedOrientations) {
|
|
||||||
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080);
|
|
||||||
|
|
||||||
final exif = ExifInfo(orientation: orientation);
|
|
||||||
|
|
||||||
when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif);
|
|
||||||
|
|
||||||
final result = await sut.getAspectRatio(remoteAsset);
|
|
||||||
|
|
||||||
expect(result, 1080 / 1920, reason: 'Orientation $orientation should flip dimensions');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('handles various non-flipped EXIF orientations correctly', () async {
|
|
||||||
final nonFlippedOrientations = ['1', '2', '3', '4'];
|
|
||||||
|
|
||||||
for (final orientation in nonFlippedOrientations) {
|
|
||||||
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-$orientation', width: 1920, height: 1080);
|
|
||||||
|
|
||||||
final exif = ExifInfo(orientation: orientation);
|
|
||||||
|
|
||||||
when(() => mockRemoteAssetRepository.getExif('remote-$orientation')).thenAnswer((_) async => exif);
|
|
||||||
|
|
||||||
final result = await sut.getAspectRatio(remoteAsset);
|
|
||||||
|
|
||||||
expect(result, 1920 / 1080, reason: 'Orientation $orientation should NOT flip dimensions');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
22
mobile/test/fixtures/sync_stream.stub.dart
vendored
22
mobile/test/fixtures/sync_stream.stub.dart
vendored
@@ -94,25 +94,11 @@ abstract final class SyncStreamStub {
|
|||||||
required String ack,
|
required String ack,
|
||||||
DateTime? trashedAt,
|
DateTime? trashedAt,
|
||||||
}) {
|
}) {
|
||||||
return _assetV1(
|
return _assetV1(id: id, checksum: checksum, deletedAt: trashedAt ?? DateTime(2025, 1, 1), ack: ack);
|
||||||
id: id,
|
|
||||||
checksum: checksum,
|
|
||||||
deletedAt: trashedAt ?? DateTime(2025, 1, 1),
|
|
||||||
ack: ack,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static SyncEvent assetModified({
|
static SyncEvent assetModified({required String id, required String checksum, required String ack}) {
|
||||||
required String id,
|
return _assetV1(id: id, checksum: checksum, deletedAt: null, ack: ack);
|
||||||
required String checksum,
|
|
||||||
required String ack,
|
|
||||||
}) {
|
|
||||||
return _assetV1(
|
|
||||||
id: id,
|
|
||||||
checksum: checksum,
|
|
||||||
deletedAt: null,
|
|
||||||
ack: ack,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static SyncEvent _assetV1({
|
static SyncEvent _assetV1({
|
||||||
@@ -140,6 +126,8 @@ abstract final class SyncStreamStub {
|
|||||||
thumbhash: null,
|
thumbhash: null,
|
||||||
type: AssetTypeEnum.IMAGE,
|
type: AssetTypeEnum.IMAGE,
|
||||||
visibility: AssetVisibility.timeline,
|
visibility: AssetVisibility.timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
),
|
),
|
||||||
ack: ack,
|
ack: ack,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,5 +45,17 @@ void main() {
|
|||||||
addDefault(value, keys, defaultValue);
|
addDefault(value, keys, defaultValue);
|
||||||
expect(value['alpha']['beta'], 'gamma');
|
expect(value['alpha']['beta'], 'gamma');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('addDefault with null', () {
|
||||||
|
dynamic value = jsonDecode("""
|
||||||
|
{
|
||||||
|
"download": {
|
||||||
|
"archiveSize": 4294967296,
|
||||||
|
"includeEmbeddedVideos": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
expect(value['download']['unknownKey'], isNull);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15706,6 +15706,10 @@
|
|||||||
"hasMetadata": {
|
"hasMetadata": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"height": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -15826,6 +15830,10 @@
|
|||||||
"$ref": "#/components/schemas/AssetVisibility"
|
"$ref": "#/components/schemas/AssetVisibility"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -15837,6 +15845,7 @@
|
|||||||
"fileCreatedAt",
|
"fileCreatedAt",
|
||||||
"fileModifiedAt",
|
"fileModifiedAt",
|
||||||
"hasMetadata",
|
"hasMetadata",
|
||||||
|
"height",
|
||||||
"id",
|
"id",
|
||||||
"isArchived",
|
"isArchived",
|
||||||
"isFavorite",
|
"isFavorite",
|
||||||
@@ -15849,7 +15858,8 @@
|
|||||||
"thumbhash",
|
"thumbhash",
|
||||||
"type",
|
"type",
|
||||||
"updatedAt",
|
"updatedAt",
|
||||||
"visibility"
|
"visibility",
|
||||||
|
"width"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@@ -20624,6 +20634,10 @@
|
|||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"height": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -20670,6 +20684,10 @@
|
|||||||
"$ref": "#/components/schemas/AssetVisibility"
|
"$ref": "#/components/schemas/AssetVisibility"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "integer"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -20678,6 +20696,7 @@
|
|||||||
"duration",
|
"duration",
|
||||||
"fileCreatedAt",
|
"fileCreatedAt",
|
||||||
"fileModifiedAt",
|
"fileModifiedAt",
|
||||||
|
"height",
|
||||||
"id",
|
"id",
|
||||||
"isFavorite",
|
"isFavorite",
|
||||||
"libraryId",
|
"libraryId",
|
||||||
@@ -20688,7 +20707,8 @@
|
|||||||
"stackId",
|
"stackId",
|
||||||
"thumbhash",
|
"thumbhash",
|
||||||
"type",
|
"type",
|
||||||
"visibility"
|
"visibility",
|
||||||
|
"width"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ export type AssetResponseDto = {
|
|||||||
/** The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. */
|
/** The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken. */
|
||||||
fileModifiedAt: string;
|
fileModifiedAt: string;
|
||||||
hasMetadata: boolean;
|
hasMetadata: boolean;
|
||||||
|
height: number | null;
|
||||||
id: string;
|
id: string;
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
@@ -373,6 +374,7 @@ export type AssetResponseDto = {
|
|||||||
/** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */
|
/** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
visibility: AssetVisibility;
|
visibility: AssetVisibility;
|
||||||
|
width: number | null;
|
||||||
};
|
};
|
||||||
export type ContributorCountResponseDto = {
|
export type ContributorCountResponseDto = {
|
||||||
assetCount: number;
|
assetCount: number;
|
||||||
|
|||||||
@@ -340,6 +340,8 @@ export const columns = {
|
|||||||
'asset.originalPath',
|
'asset.originalPath',
|
||||||
'asset.ownerId',
|
'asset.ownerId',
|
||||||
'asset.type',
|
'asset.type',
|
||||||
|
'asset.width',
|
||||||
|
'asset.height',
|
||||||
],
|
],
|
||||||
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
|
assetFiles: ['asset_file.id', 'asset_file.path', 'asset_file.type'],
|
||||||
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
|
authUser: ['user.id', 'user.name', 'user.email', 'user.isAdmin', 'user.quotaUsageInBytes', 'user.quotaSizeInBytes'],
|
||||||
@@ -390,6 +392,8 @@ export const columns = {
|
|||||||
'asset.livePhotoVideoId',
|
'asset.livePhotoVideoId',
|
||||||
'asset.stackId',
|
'asset.stackId',
|
||||||
'asset.libraryId',
|
'asset.libraryId',
|
||||||
|
'asset.width',
|
||||||
|
'asset.height',
|
||||||
],
|
],
|
||||||
syncAlbumUser: ['album_user.albumId as albumId', 'album_user.userId as userId', 'album_user.role'],
|
syncAlbumUser: ['album_user.albumId as albumId', 'album_user.userId as userId', 'album_user.role'],
|
||||||
syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'],
|
syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'],
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export class SanitizedAssetResponseDto {
|
|||||||
duration!: string;
|
duration!: string;
|
||||||
livePhotoVideoId?: string | null;
|
livePhotoVideoId?: string | null;
|
||||||
hasMetadata!: boolean;
|
hasMetadata!: boolean;
|
||||||
|
width!: number | null;
|
||||||
|
height!: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AssetResponseDto extends SanitizedAssetResponseDto {
|
export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||||
@@ -129,6 +131,8 @@ export type MapAsset = {
|
|||||||
tags?: Tag[];
|
tags?: Tag[];
|
||||||
thumbhash: Buffer<ArrayBufferLike> | null;
|
thumbhash: Buffer<ArrayBufferLike> | null;
|
||||||
type: AssetType;
|
type: AssetType;
|
||||||
|
width: number | null;
|
||||||
|
height: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AssetStackResponseDto {
|
export class AssetStackResponseDto {
|
||||||
@@ -190,6 +194,8 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||||||
duration: entity.duration ?? '0:00:00.00000',
|
duration: entity.duration ?? '0:00:00.00000',
|
||||||
livePhotoVideoId: entity.livePhotoVideoId,
|
livePhotoVideoId: entity.livePhotoVideoId,
|
||||||
hasMetadata: false,
|
hasMetadata: false,
|
||||||
|
width: entity.width,
|
||||||
|
height: entity.height,
|
||||||
};
|
};
|
||||||
return sanitizedAssetResponse as AssetResponseDto;
|
return sanitizedAssetResponse as AssetResponseDto;
|
||||||
}
|
}
|
||||||
@@ -227,5 +233,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
|||||||
hasMetadata: true,
|
hasMetadata: true,
|
||||||
duplicateId: entity.duplicateId,
|
duplicateId: entity.duplicateId,
|
||||||
resized: true,
|
resized: true,
|
||||||
|
width: entity.width,
|
||||||
|
height: entity.height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ export class SyncAssetV1 {
|
|||||||
livePhotoVideoId!: string | null;
|
livePhotoVideoId!: string | null;
|
||||||
stackId!: string | null;
|
stackId!: string | null;
|
||||||
libraryId!: string | null;
|
libraryId!: string | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
width!: number | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
height!: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExtraModel()
|
@ExtraModel()
|
||||||
|
|||||||
@@ -345,14 +345,10 @@ with
|
|||||||
"asset_exif"."projectionType",
|
"asset_exif"."projectionType",
|
||||||
coalesce(
|
coalesce(
|
||||||
case
|
case
|
||||||
when asset_exif."exifImageHeight" = 0
|
when asset."height" = 0
|
||||||
or asset_exif."exifImageWidth" = 0 then 1
|
or asset."width" = 0 then 1
|
||||||
when "asset_exif"."orientation" in ('5', '6', '7', '8', '-90', '90') then round(
|
|
||||||
asset_exif."exifImageHeight"::numeric / asset_exif."exifImageWidth"::numeric,
|
|
||||||
3
|
|
||||||
)
|
|
||||||
else round(
|
else round(
|
||||||
asset_exif."exifImageWidth"::numeric / asset_exif."exifImageHeight"::numeric,
|
asset."width"::numeric / asset."height"::numeric,
|
||||||
3
|
3
|
||||||
)
|
)
|
||||||
end,
|
end,
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ select
|
|||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
"asset"."stackId",
|
"asset"."stackId",
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
|
"asset"."width",
|
||||||
|
"asset"."height",
|
||||||
"album_asset"."updateId"
|
"album_asset"."updateId"
|
||||||
from
|
from
|
||||||
"album_asset" as "album_asset"
|
"album_asset" as "album_asset"
|
||||||
@@ -99,6 +101,8 @@ select
|
|||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
"asset"."stackId",
|
"asset"."stackId",
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
|
"asset"."width",
|
||||||
|
"asset"."height",
|
||||||
"asset"."updateId"
|
"asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset" as "asset"
|
"asset" as "asset"
|
||||||
@@ -134,7 +138,9 @@ select
|
|||||||
"asset"."duration",
|
"asset"."duration",
|
||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
"asset"."stackId",
|
"asset"."stackId",
|
||||||
"asset"."libraryId"
|
"asset"."libraryId",
|
||||||
|
"asset"."width",
|
||||||
|
"asset"."height"
|
||||||
from
|
from
|
||||||
"album_asset" as "album_asset"
|
"album_asset" as "album_asset"
|
||||||
inner join "asset" on "asset"."id" = "album_asset"."assetId"
|
inner join "asset" on "asset"."id" = "album_asset"."assetId"
|
||||||
@@ -448,6 +454,8 @@ select
|
|||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
"asset"."stackId",
|
"asset"."stackId",
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
|
"asset"."width",
|
||||||
|
"asset"."height",
|
||||||
"asset"."updateId"
|
"asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset" as "asset"
|
"asset" as "asset"
|
||||||
@@ -740,6 +748,8 @@ select
|
|||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
"asset"."stackId",
|
"asset"."stackId",
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
|
"asset"."width",
|
||||||
|
"asset"."height",
|
||||||
"asset"."updateId"
|
"asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset" as "asset"
|
"asset" as "asset"
|
||||||
@@ -789,6 +799,8 @@ select
|
|||||||
"asset"."livePhotoVideoId",
|
"asset"."livePhotoVideoId",
|
||||||
"asset"."stackId",
|
"asset"."stackId",
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
|
"asset"."width",
|
||||||
|
"asset"."height",
|
||||||
"asset"."updateId"
|
"asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset" as "asset"
|
"asset" as "asset"
|
||||||
|
|||||||
@@ -632,11 +632,9 @@ export class AssetRepository {
|
|||||||
.coalesce(
|
.coalesce(
|
||||||
eb
|
eb
|
||||||
.case()
|
.case()
|
||||||
.when(sql`asset_exif."exifImageHeight" = 0 or asset_exif."exifImageWidth" = 0`)
|
.when(sql`asset."height" = 0 or asset."width" = 0`)
|
||||||
.then(eb.lit(1))
|
.then(eb.lit(1))
|
||||||
.when('asset_exif.orientation', 'in', sql<string>`('5', '6', '7', '8', '-90', '90')`)
|
.else(sql`round(asset."width"::numeric / asset."height"::numeric, 3)`)
|
||||||
.then(sql`round(asset_exif."exifImageHeight"::numeric / asset_exif."exifImageWidth"::numeric, 3)`)
|
|
||||||
.else(sql`round(asset_exif."exifImageWidth"::numeric / asset_exif."exifImageHeight"::numeric, 3)`)
|
|
||||||
.end(),
|
.end(),
|
||||||
eb.lit(1),
|
eb.lit(1),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "asset" ADD COLUMN "width" integer;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset" ADD COLUMN "height" integer;`.execute(db);
|
||||||
|
|
||||||
|
// Populate width and height from exif data with orientation-aware swapping
|
||||||
|
await sql`
|
||||||
|
UPDATE "asset"
|
||||||
|
SET
|
||||||
|
"width" = CASE
|
||||||
|
WHEN "asset_exif"."orientation" IN ('5', '6', '7', '8', '-90', '90') THEN "asset_exif"."exifImageHeight"
|
||||||
|
ELSE "asset_exif"."exifImageWidth"
|
||||||
|
END,
|
||||||
|
"height" = CASE
|
||||||
|
WHEN "asset_exif"."orientation" IN ('5', '6', '7', '8', '-90', '90') THEN "asset_exif"."exifImageWidth"
|
||||||
|
ELSE "asset_exif"."exifImageHeight"
|
||||||
|
END
|
||||||
|
FROM "asset_exif"
|
||||||
|
WHERE "asset"."id" = "asset_exif"."assetId"
|
||||||
|
AND ("asset_exif"."exifImageWidth" IS NOT NULL OR "asset_exif"."exifImageHeight" IS NOT NULL)
|
||||||
|
`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`ALTER TABLE "asset" DROP COLUMN "width";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset" DROP COLUMN "height";`.execute(db);
|
||||||
|
}
|
||||||
@@ -137,4 +137,10 @@ export class AssetTable {
|
|||||||
|
|
||||||
@Column({ enum: asset_visibility_enum, default: AssetVisibility.Timeline })
|
@Column({ enum: asset_visibility_enum, default: AssetVisibility.Timeline })
|
||||||
visibility!: Generated<AssetVisibility>;
|
visibility!: Generated<AssetVisibility>;
|
||||||
|
|
||||||
|
@Column({ type: 'integer', nullable: true })
|
||||||
|
width!: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'integer', nullable: true })
|
||||||
|
height!: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ export class JobService extends BaseService {
|
|||||||
livePhotoVideoId: asset.livePhotoVideoId,
|
livePhotoVideoId: asset.livePhotoVideoId,
|
||||||
stackId: asset.stackId,
|
stackId: asset.stackId,
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
|
width: asset.width,
|
||||||
|
height: asset.height,
|
||||||
},
|
},
|
||||||
exif: {
|
exif: {
|
||||||
assetId: exif.assetId,
|
assetId: exif.assetId,
|
||||||
|
|||||||
@@ -221,6 +221,8 @@ describe(MetadataService.name, () => {
|
|||||||
fileCreatedAt: fileModifiedAt,
|
fileCreatedAt: fileModifiedAt,
|
||||||
fileModifiedAt,
|
fileModifiedAt,
|
||||||
localDateTime: fileModifiedAt,
|
localDateTime: fileModifiedAt,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -245,6 +247,8 @@ describe(MetadataService.name, () => {
|
|||||||
fileCreatedAt,
|
fileCreatedAt,
|
||||||
fileModifiedAt,
|
fileModifiedAt,
|
||||||
localDateTime: fileCreatedAt,
|
localDateTime: fileCreatedAt,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -288,6 +292,8 @@ describe(MetadataService.name, () => {
|
|||||||
fileCreatedAt: assetStub.image.fileCreatedAt,
|
fileCreatedAt: assetStub.image.fileCreatedAt,
|
||||||
fileModifiedAt: assetStub.image.fileCreatedAt,
|
fileModifiedAt: assetStub.image.fileCreatedAt,
|
||||||
localDateTime: assetStub.image.fileCreatedAt,
|
localDateTime: assetStub.image.fileCreatedAt,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -317,6 +323,8 @@ describe(MetadataService.name, () => {
|
|||||||
fileCreatedAt: assetStub.withLocation.fileCreatedAt,
|
fileCreatedAt: assetStub.withLocation.fileCreatedAt,
|
||||||
fileModifiedAt: assetStub.withLocation.fileModifiedAt,
|
fileModifiedAt: assetStub.withLocation.fileModifiedAt,
|
||||||
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -346,6 +354,8 @@ describe(MetadataService.name, () => {
|
|||||||
fileCreatedAt: assetStub.withLocation.fileCreatedAt,
|
fileCreatedAt: assetStub.withLocation.fileCreatedAt,
|
||||||
fileModifiedAt: assetStub.withLocation.fileModifiedAt,
|
fileModifiedAt: assetStub.withLocation.fileModifiedAt,
|
||||||
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
localDateTime: new Date('2023-02-22T05:06:29.716Z'),
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1517,6 +1527,49 @@ describe(MetadataService.name, () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should properly set width/height for normal images', async () => {
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||||
|
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000 });
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
width: 1000,
|
||||||
|
height: 2000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly swap asset width/height for rotated images', async () => {
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue(assetStub.image);
|
||||||
|
mockReadTags({ ImageWidth: 1000, ImageHeight: 2000, Orientation: 6 });
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
width: 2000,
|
||||||
|
height: 1000,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not overwrite existing width/height if they already exist', async () => {
|
||||||
|
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
|
||||||
|
...assetStub.image,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
});
|
||||||
|
mockReadTags({ ImageWidth: 1280, ImageHeight: 720 });
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
expect(mocks.asset.update).not.toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleQueueSidecar', () => {
|
describe('handleQueueSidecar', () => {
|
||||||
|
|||||||
@@ -195,6 +195,15 @@ export class MetadataService extends BaseService {
|
|||||||
await this.eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId: motionAsset.ownerId });
|
await this.eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId: motionAsset.ownerId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isOrientationSidewards(orientation: ExifOrientation | number): boolean {
|
||||||
|
return [
|
||||||
|
ExifOrientation.MirrorHorizontalRotate270CW,
|
||||||
|
ExifOrientation.Rotate90CW,
|
||||||
|
ExifOrientation.MirrorHorizontalRotate90CW,
|
||||||
|
ExifOrientation.Rotate270CW,
|
||||||
|
].includes(orientation);
|
||||||
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.AssetExtractMetadataQueueAll, queue: QueueName.MetadataExtraction })
|
@OnJob({ name: JobName.AssetExtractMetadataQueueAll, queue: QueueName.MetadataExtraction })
|
||||||
async handleQueueMetadataExtraction(job: JobOf<JobName.AssetExtractMetadataQueueAll>): Promise<JobStatus> {
|
async handleQueueMetadataExtraction(job: JobOf<JobName.AssetExtractMetadataQueueAll>): Promise<JobStatus> {
|
||||||
const { force } = job;
|
const { force } = job;
|
||||||
@@ -288,6 +297,10 @@ export class MetadataService extends BaseService {
|
|||||||
autoStackId: this.getAutoStackId(exifTags),
|
autoStackId: this.getAutoStackId(exifTags),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isSidewards = exifTags.Orientation && this.isOrientationSidewards(exifTags.Orientation);
|
||||||
|
const assetWidth = isSidewards ? validate(height) : validate(width);
|
||||||
|
const assetHeight = isSidewards ? validate(width) : validate(height);
|
||||||
|
|
||||||
const promises: Promise<unknown>[] = [
|
const promises: Promise<unknown>[] = [
|
||||||
this.assetRepository.upsertExif(exifData),
|
this.assetRepository.upsertExif(exifData),
|
||||||
this.assetRepository.update({
|
this.assetRepository.update({
|
||||||
@@ -296,6 +309,11 @@ export class MetadataService extends BaseService {
|
|||||||
localDateTime: dates.localDateTime,
|
localDateTime: dates.localDateTime,
|
||||||
fileCreatedAt: dates.dateTimeOriginal ?? undefined,
|
fileCreatedAt: dates.dateTimeOriginal ?? undefined,
|
||||||
fileModifiedAt: stats.mtime,
|
fileModifiedAt: stats.mtime,
|
||||||
|
|
||||||
|
// only update the dimensions if they don't already exist
|
||||||
|
// we don't want to overwrite width/height that are modified by edits
|
||||||
|
width: asset.width == null ? assetWidth : undefined,
|
||||||
|
height: asset.height == null ? assetHeight : undefined,
|
||||||
}),
|
}),
|
||||||
this.applyTagList(asset, exifTags),
|
this.applyTagList(asset, exifTags),
|
||||||
];
|
];
|
||||||
@@ -698,12 +716,7 @@ export class MetadataService extends BaseService {
|
|||||||
return regionInfo;
|
return regionInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSidewards = [
|
const isSidewards = this.isOrientationSidewards(orientation);
|
||||||
ExifOrientation.MirrorHorizontalRotate270CW,
|
|
||||||
ExifOrientation.Rotate90CW,
|
|
||||||
ExifOrientation.MirrorHorizontalRotate90CW,
|
|
||||||
ExifOrientation.Rotate270CW,
|
|
||||||
].includes(orientation);
|
|
||||||
|
|
||||||
// swap image dimensions in AppliedToDimensions if orientation is sidewards
|
// swap image dimensions in AppliedToDimensions if orientation is sidewards
|
||||||
const adjustedAppliedToDimensions = isSidewards
|
const adjustedAppliedToDimensions = isSidewards
|
||||||
@@ -949,9 +962,17 @@ export class MetadataService extends BaseService {
|
|||||||
private async getVideoTags(originalPath: string) {
|
private async getVideoTags(originalPath: string) {
|
||||||
const { videoStreams, format } = await this.mediaRepository.probe(originalPath);
|
const { videoStreams, format } = await this.mediaRepository.probe(originalPath);
|
||||||
|
|
||||||
const tags: Pick<ImmichTags, 'Duration' | 'Orientation'> = {};
|
const tags: Pick<ImmichTags, 'Duration' | 'Orientation' | 'ImageWidth' | 'ImageHeight'> = {};
|
||||||
|
|
||||||
if (videoStreams[0]) {
|
if (videoStreams[0]) {
|
||||||
|
// Set video dimensions
|
||||||
|
if (videoStreams[0].width) {
|
||||||
|
tags.ImageWidth = videoStreams[0].width;
|
||||||
|
}
|
||||||
|
if (videoStreams[0].height) {
|
||||||
|
tags.ImageHeight = videoStreams[0].height;
|
||||||
|
}
|
||||||
|
|
||||||
switch (videoStreams[0].rotation) {
|
switch (videoStreams[0].rotation) {
|
||||||
case -90: {
|
case -90: {
|
||||||
tags.Orientation = ExifOrientation.Rotate90CW;
|
tags.Orientation = ExifOrientation.Rotate90CW;
|
||||||
|
|||||||
48
server/test/fixtures/asset.stub.ts
vendored
48
server/test/fixtures/asset.stub.ts
vendored
@@ -101,6 +101,8 @@ export const assetStub = {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
updateId: '42',
|
updateId: '42',
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
noWebpPath: Object.freeze({
|
noWebpPath: Object.freeze({
|
||||||
@@ -139,6 +141,8 @@ export const assetStub = {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
updateId: '42',
|
updateId: '42',
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
noThumbhash: Object.freeze({
|
noThumbhash: Object.freeze({
|
||||||
@@ -174,6 +178,8 @@ export const assetStub = {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
updateId: '42',
|
updateId: '42',
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
primaryImage: Object.freeze({
|
primaryImage: Object.freeze({
|
||||||
@@ -219,6 +225,8 @@ export const assetStub = {
|
|||||||
updateId: '42',
|
updateId: '42',
|
||||||
libraryId: null,
|
libraryId: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
image: Object.freeze({
|
image: Object.freeze({
|
||||||
@@ -261,8 +269,8 @@ export const assetStub = {
|
|||||||
stack: null,
|
stack: null,
|
||||||
orientation: '',
|
orientation: '',
|
||||||
projectionType: null,
|
projectionType: null,
|
||||||
height: 3840,
|
height: null,
|
||||||
width: 2160,
|
width: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -304,6 +312,8 @@ export const assetStub = {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
updateId: '42',
|
updateId: '42',
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
trashedOffline: Object.freeze({
|
trashedOffline: Object.freeze({
|
||||||
@@ -344,6 +354,8 @@ export const assetStub = {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
updateId: '42',
|
updateId: '42',
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
archived: Object.freeze({
|
archived: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
@@ -383,6 +395,8 @@ export const assetStub = {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
updateId: '42',
|
updateId: '42',
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
external: Object.freeze({
|
external: Object.freeze({
|
||||||
@@ -422,6 +436,8 @@ export const assetStub = {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
stack: null,
|
stack: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
image1: Object.freeze({
|
image1: Object.freeze({
|
||||||
@@ -461,6 +477,8 @@ export const assetStub = {
|
|||||||
libraryId: null,
|
libraryId: null,
|
||||||
stack: null,
|
stack: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageFrom2015: Object.freeze({
|
imageFrom2015: Object.freeze({
|
||||||
@@ -499,6 +517,8 @@ export const assetStub = {
|
|||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
video: Object.freeze({
|
video: Object.freeze({
|
||||||
@@ -539,6 +559,8 @@ export const assetStub = {
|
|||||||
libraryId: null,
|
libraryId: null,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
livePhotoMotionAsset: Object.freeze({
|
livePhotoMotionAsset: Object.freeze({
|
||||||
@@ -556,6 +578,8 @@ export const assetStub = {
|
|||||||
files: [] as AssetFile[],
|
files: [] as AssetFile[],
|
||||||
libraryId: null,
|
libraryId: null,
|
||||||
visibility: AssetVisibility.Hidden,
|
visibility: AssetVisibility.Hidden,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }),
|
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }),
|
||||||
|
|
||||||
livePhotoStillAsset: Object.freeze({
|
livePhotoStillAsset: Object.freeze({
|
||||||
@@ -574,6 +598,8 @@ export const assetStub = {
|
|||||||
files,
|
files,
|
||||||
faces: [] as AssetFace[],
|
faces: [] as AssetFace[],
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[] }),
|
} as MapAsset & { faces: AssetFace[]; files: AssetFile[] }),
|
||||||
|
|
||||||
livePhotoWithOriginalFileName: Object.freeze({
|
livePhotoWithOriginalFileName: Object.freeze({
|
||||||
@@ -594,6 +620,8 @@ export const assetStub = {
|
|||||||
libraryId: null,
|
libraryId: null,
|
||||||
faces: [] as AssetFace[],
|
faces: [] as AssetFace[],
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[] }),
|
} as MapAsset & { faces: AssetFace[]; files: AssetFile[] }),
|
||||||
|
|
||||||
withLocation: Object.freeze({
|
withLocation: Object.freeze({
|
||||||
@@ -638,6 +666,8 @@ export const assetStub = {
|
|||||||
isOffline: false,
|
isOffline: false,
|
||||||
tags: [],
|
tags: [],
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sidecar: Object.freeze({
|
sidecar: Object.freeze({
|
||||||
@@ -673,6 +703,8 @@ export const assetStub = {
|
|||||||
libraryId: null,
|
libraryId: null,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sidecarWithoutExt: Object.freeze({
|
sidecarWithoutExt: Object.freeze({
|
||||||
@@ -705,6 +737,8 @@ export const assetStub = {
|
|||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasEncodedVideo: Object.freeze({
|
hasEncodedVideo: Object.freeze({
|
||||||
@@ -744,6 +778,8 @@ export const assetStub = {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
stack: null,
|
stack: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasFileExtension: Object.freeze({
|
hasFileExtension: Object.freeze({
|
||||||
@@ -780,6 +816,8 @@ export const assetStub = {
|
|||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageDng: Object.freeze({
|
imageDng: Object.freeze({
|
||||||
@@ -820,6 +858,8 @@ export const assetStub = {
|
|||||||
libraryId: null,
|
libraryId: null,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageHif: Object.freeze({
|
imageHif: Object.freeze({
|
||||||
@@ -860,6 +900,8 @@ export const assetStub = {
|
|||||||
libraryId: null,
|
libraryId: null,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
panoramaTif: Object.freeze({
|
panoramaTif: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
@@ -899,5 +941,7 @@ export const assetStub = {
|
|||||||
libraryId: null,
|
libraryId: null,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
6
server/test/fixtures/shared-link.stub.ts
vendored
6
server/test/fixtures/shared-link.stub.ts
vendored
@@ -72,6 +72,8 @@ const assetResponse: AssetResponseDto = {
|
|||||||
libraryId: 'library-id',
|
libraryId: 'library-id',
|
||||||
hasMetadata: true,
|
hasMetadata: true,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const assetResponseWithoutMetadata = {
|
const assetResponseWithoutMetadata = {
|
||||||
@@ -83,6 +85,8 @@ const assetResponseWithoutMetadata = {
|
|||||||
duration: '0:00:00.00000',
|
duration: '0:00:00.00000',
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
hasMetadata: false,
|
hasMetadata: false,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
} as AssetResponseDto;
|
} as AssetResponseDto;
|
||||||
|
|
||||||
const albumResponse: AlbumResponseDto = {
|
const albumResponse: AlbumResponseDto = {
|
||||||
@@ -257,6 +261,8 @@ export const sharedLinkStub = {
|
|||||||
libraryId: null,
|
libraryId: null,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: 500,
|
||||||
|
height: 500,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
|||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
libraryId: null,
|
libraryId: null,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
});
|
});
|
||||||
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
const { album } = await ctx.newAlbum({ ownerId: user2.id });
|
||||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||||
@@ -79,6 +81,8 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
|||||||
livePhotoVideoId: asset.livePhotoVideoId,
|
livePhotoVideoId: asset.livePhotoVideoId,
|
||||||
stackId: asset.stackId,
|
stackId: asset.stackId,
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
|
width: asset.width,
|
||||||
|
height: asset.height,
|
||||||
},
|
},
|
||||||
type: SyncEntityType.AlbumAssetCreateV1,
|
type: SyncEntityType.AlbumAssetCreateV1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ describe(SyncEntityType.AssetV1, () => {
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
duration: '0:10:00.00000',
|
duration: '0:10:00.00000',
|
||||||
libraryId: null,
|
libraryId: null,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
const response = await ctx.syncStream(auth, [SyncRequestType.AssetsV1]);
|
||||||
@@ -60,6 +62,8 @@ describe(SyncEntityType.AssetV1, () => {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
|
width: asset.width,
|
||||||
|
height: asset.height,
|
||||||
},
|
},
|
||||||
type: 'AssetV1',
|
type: 'AssetV1',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
|||||||
stackId: null,
|
stackId: null,
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
},
|
},
|
||||||
type: SyncEntityType.PartnerAssetV1,
|
type: SyncEntityType.PartnerAssetV1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ const assetFactory = (asset: Partial<MapAsset> = {}) => ({
|
|||||||
thumbhash: null,
|
thumbhash: null,
|
||||||
type: AssetType.Image,
|
type: AssetType.Image,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: null,
|
||||||
|
height: null,
|
||||||
...asset,
|
...asset,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
|
|||||||
isOffline: Sync.each(() => faker.datatype.boolean()),
|
isOffline: Sync.each(() => faker.datatype.boolean()),
|
||||||
hasMetadata: Sync.each(() => faker.datatype.boolean()),
|
hasMetadata: Sync.each(() => faker.datatype.boolean()),
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
width: faker.number.int({ min: 100, max: 1000 }),
|
||||||
|
height: faker.number.int({ min: 100, max: 1000 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({
|
export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({
|
||||||
|
|||||||
Reference in New Issue
Block a user