Compare commits

...

1 Commits

Author SHA1 Message Date
Jason Rasmussen
82f05e9ca9 feat: editor endpoints 2024-08-08 15:45:42 -04:00
25 changed files with 1566 additions and 5 deletions

View File

@@ -129,6 +129,7 @@ Class | Method | HTTP request | Description
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates |
*EditorApi* | [**createAssetFromEdits**](doc//EditorApi.md#createassetfromedits) | **POST** /editor |
*FacesApi* | [**getFaces**](doc//FacesApi.md#getfaces) | **GET** /faces |
*FacesApi* | [**reassignFacesById**](doc//FacesApi.md#reassignfacesbyid) | **PUT** /faces/{id} |
*FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix |
@@ -314,6 +315,14 @@ Class | Method | HTTP request | Description
- [DownloadUpdate](doc//DownloadUpdate.md)
- [DuplicateDetectionConfig](doc//DuplicateDetectionConfig.md)
- [DuplicateResponseDto](doc//DuplicateResponseDto.md)
- [EditorActionAdjust](doc//EditorActionAdjust.md)
- [EditorActionBlur](doc//EditorActionBlur.md)
- [EditorActionCrop](doc//EditorActionCrop.md)
- [EditorActionRotate](doc//EditorActionRotate.md)
- [EditorActionType](doc//EditorActionType.md)
- [EditorCreateAssetDto](doc//EditorCreateAssetDto.md)
- [EditorCreateAssetDtoEditsInner](doc//EditorCreateAssetDtoEditsInner.md)
- [EditorCropRegion](doc//EditorCropRegion.md)
- [EmailNotificationsResponse](doc//EmailNotificationsResponse.md)
- [EmailNotificationsUpdate](doc//EmailNotificationsUpdate.md)
- [EntityType](doc//EntityType.md)

View File

@@ -38,6 +38,7 @@ part 'api/authentication_api.dart';
part 'api/deprecated_api.dart';
part 'api/download_api.dart';
part 'api/duplicates_api.dart';
part 'api/editor_api.dart';
part 'api/faces_api.dart';
part 'api/file_reports_api.dart';
part 'api/jobs_api.dart';
@@ -125,6 +126,14 @@ part 'model/download_response_dto.dart';
part 'model/download_update.dart';
part 'model/duplicate_detection_config.dart';
part 'model/duplicate_response_dto.dart';
part 'model/editor_action_adjust.dart';
part 'model/editor_action_blur.dart';
part 'model/editor_action_crop.dart';
part 'model/editor_action_rotate.dart';
part 'model/editor_action_type.dart';
part 'model/editor_create_asset_dto.dart';
part 'model/editor_create_asset_dto_edits_inner.dart';
part 'model/editor_crop_region.dart';
part 'model/email_notifications_response.dart';
part 'model/email_notifications_update.dart';
part 'model/entity_type.dart';

65
mobile/openapi/lib/api/editor_api.dart generated Normal file
View File

@@ -0,0 +1,65 @@
//
// 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 EditorApi {
EditorApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
final ApiClient apiClient;
/// Performs an HTTP 'POST /editor' operation and returns the [Response].
/// Parameters:
///
/// * [EditorCreateAssetDto] editorCreateAssetDto (required):
Future<Response> createAssetFromEditsWithHttpInfo(EditorCreateAssetDto editorCreateAssetDto,) async {
// ignore: prefer_const_declarations
final path = r'/editor';
// ignore: prefer_final_locals
Object? postBody = editorCreateAssetDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [EditorCreateAssetDto] editorCreateAssetDto (required):
Future<AssetResponseDto?> createAssetFromEdits(EditorCreateAssetDto editorCreateAssetDto,) async {
final response = await createAssetFromEditsWithHttpInfo(editorCreateAssetDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetResponseDto',) as AssetResponseDto;
}
return null;
}
}

View File

@@ -308,6 +308,22 @@ class ApiClient {
return DuplicateDetectionConfig.fromJson(value);
case 'DuplicateResponseDto':
return DuplicateResponseDto.fromJson(value);
case 'EditorActionAdjust':
return EditorActionAdjust.fromJson(value);
case 'EditorActionBlur':
return EditorActionBlur.fromJson(value);
case 'EditorActionCrop':
return EditorActionCrop.fromJson(value);
case 'EditorActionRotate':
return EditorActionRotate.fromJson(value);
case 'EditorActionType':
return EditorActionTypeTypeTransformer().decode(value);
case 'EditorCreateAssetDto':
return EditorCreateAssetDto.fromJson(value);
case 'EditorCreateAssetDtoEditsInner':
return EditorCreateAssetDtoEditsInner.fromJson(value);
case 'EditorCropRegion':
return EditorCropRegion.fromJson(value);
case 'EmailNotificationsResponse':
return EmailNotificationsResponse.fromJson(value);
case 'EmailNotificationsUpdate':

View File

@@ -82,6 +82,9 @@ String parameterToString(dynamic value) {
if (value is Colorspace) {
return ColorspaceTypeTransformer().encode(value).toString();
}
if (value is EditorActionType) {
return EditorActionTypeTypeTransformer().encode(value).toString();
}
if (value is EntityType) {
return EntityTypeTypeTransformer().encode(value).toString();
}

View File

@@ -0,0 +1,130 @@
//
// 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 EditorActionAdjust {
/// Returns a new [EditorActionAdjust] instance.
EditorActionAdjust({
required this.action,
required this.brightness,
required this.hue,
required this.lightness,
required this.saturation,
});
EditorActionType action;
int brightness;
int hue;
int lightness;
int saturation;
@override
bool operator ==(Object other) => identical(this, other) || other is EditorActionAdjust &&
other.action == action &&
other.brightness == brightness &&
other.hue == hue &&
other.lightness == lightness &&
other.saturation == saturation;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(brightness.hashCode) +
(hue.hashCode) +
(lightness.hashCode) +
(saturation.hashCode);
@override
String toString() => 'EditorActionAdjust[action=$action, brightness=$brightness, hue=$hue, lightness=$lightness, saturation=$saturation]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'brightness'] = this.brightness;
json[r'hue'] = this.hue;
json[r'lightness'] = this.lightness;
json[r'saturation'] = this.saturation;
return json;
}
/// Returns a new [EditorActionAdjust] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditorActionAdjust? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditorActionAdjust(
action: EditorActionType.fromJson(json[r'action'])!,
brightness: mapValueOfType<int>(json, r'brightness')!,
hue: mapValueOfType<int>(json, r'hue')!,
lightness: mapValueOfType<int>(json, r'lightness')!,
saturation: mapValueOfType<int>(json, r'saturation')!,
);
}
return null;
}
static List<EditorActionAdjust> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditorActionAdjust>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditorActionAdjust.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditorActionAdjust> mapFromJson(dynamic json) {
final map = <String, EditorActionAdjust>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditorActionAdjust.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditorActionAdjust-objects as value to a dart map
static Map<String, List<EditorActionAdjust>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditorActionAdjust>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditorActionAdjust.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'brightness',
'hue',
'lightness',
'saturation',
};
}

View File

@@ -0,0 +1,98 @@
//
// 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 EditorActionBlur {
/// Returns a new [EditorActionBlur] instance.
EditorActionBlur({
required this.action,
});
EditorActionType action;
@override
bool operator ==(Object other) => identical(this, other) || other is EditorActionBlur &&
other.action == action;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode);
@override
String toString() => 'EditorActionBlur[action=$action]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
return json;
}
/// Returns a new [EditorActionBlur] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditorActionBlur? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditorActionBlur(
action: EditorActionType.fromJson(json[r'action'])!,
);
}
return null;
}
static List<EditorActionBlur> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditorActionBlur>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditorActionBlur.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditorActionBlur> mapFromJson(dynamic json) {
final map = <String, EditorActionBlur>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditorActionBlur.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditorActionBlur-objects as value to a dart map
static Map<String, List<EditorActionBlur>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditorActionBlur>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditorActionBlur.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
};
}

View File

@@ -0,0 +1,106 @@
//
// 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 EditorActionCrop {
/// Returns a new [EditorActionCrop] instance.
EditorActionCrop({
required this.action,
required this.region,
});
EditorActionType action;
EditorCropRegion region;
@override
bool operator ==(Object other) => identical(this, other) || other is EditorActionCrop &&
other.action == action &&
other.region == region;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(region.hashCode);
@override
String toString() => 'EditorActionCrop[action=$action, region=$region]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'region'] = this.region;
return json;
}
/// Returns a new [EditorActionCrop] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditorActionCrop? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditorActionCrop(
action: EditorActionType.fromJson(json[r'action'])!,
region: EditorCropRegion.fromJson(json[r'region'])!,
);
}
return null;
}
static List<EditorActionCrop> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditorActionCrop>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditorActionCrop.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditorActionCrop> mapFromJson(dynamic json) {
final map = <String, EditorActionCrop>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditorActionCrop.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditorActionCrop-objects as value to a dart map
static Map<String, List<EditorActionCrop>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditorActionCrop>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditorActionCrop.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'region',
};
}

View File

@@ -0,0 +1,106 @@
//
// 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 EditorActionRotate {
/// Returns a new [EditorActionRotate] instance.
EditorActionRotate({
required this.action,
required this.angle,
});
EditorActionType action;
int angle;
@override
bool operator ==(Object other) => identical(this, other) || other is EditorActionRotate &&
other.action == action &&
other.angle == angle;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(angle.hashCode);
@override
String toString() => 'EditorActionRotate[action=$action, angle=$angle]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'angle'] = this.angle;
return json;
}
/// Returns a new [EditorActionRotate] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditorActionRotate? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditorActionRotate(
action: EditorActionType.fromJson(json[r'action'])!,
angle: mapValueOfType<int>(json, r'angle')!,
);
}
return null;
}
static List<EditorActionRotate> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditorActionRotate>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditorActionRotate.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditorActionRotate> mapFromJson(dynamic json) {
final map = <String, EditorActionRotate>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditorActionRotate.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditorActionRotate-objects as value to a dart map
static Map<String, List<EditorActionRotate>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditorActionRotate>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditorActionRotate.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'angle',
};
}

View File

@@ -0,0 +1,91 @@
//
// 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 EditorActionType {
/// Instantiate a new enum with the provided [value].
const EditorActionType._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const crop = EditorActionType._(r'crop');
static const rotate = EditorActionType._(r'rotate');
static const blur = EditorActionType._(r'blur');
static const adjust = EditorActionType._(r'adjust');
/// List of all possible values in this [enum][EditorActionType].
static const values = <EditorActionType>[
crop,
rotate,
blur,
adjust,
];
static EditorActionType? fromJson(dynamic value) => EditorActionTypeTypeTransformer().decode(value);
static List<EditorActionType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditorActionType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditorActionType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [EditorActionType] to String,
/// and [decode] dynamic data back to [EditorActionType].
class EditorActionTypeTypeTransformer {
factory EditorActionTypeTypeTransformer() => _instance ??= const EditorActionTypeTypeTransformer._();
const EditorActionTypeTypeTransformer._();
String encode(EditorActionType data) => data.value;
/// Decodes a [dynamic value][data] to a EditorActionType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
EditorActionType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'crop': return EditorActionType.crop;
case r'rotate': return EditorActionType.rotate;
case r'blur': return EditorActionType.blur;
case r'adjust': return EditorActionType.adjust;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [EditorActionTypeTypeTransformer] instance.
static EditorActionTypeTypeTransformer? _instance;
}

View File

@@ -0,0 +1,126 @@
//
// 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 EditorCreateAssetDto {
/// Returns a new [EditorCreateAssetDto] instance.
EditorCreateAssetDto({
this.edits = const [],
required this.id,
this.stack,
});
/// list of edits
List<EditorCreateAssetDtoEditsInner> edits;
/// Source asset id
String id;
/// Stack the edit and the original
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? stack;
@override
bool operator ==(Object other) => identical(this, other) || other is EditorCreateAssetDto &&
_deepEquality.equals(other.edits, edits) &&
other.id == id &&
other.stack == stack;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(edits.hashCode) +
(id.hashCode) +
(stack == null ? 0 : stack!.hashCode);
@override
String toString() => 'EditorCreateAssetDto[edits=$edits, id=$id, stack=$stack]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'edits'] = this.edits;
json[r'id'] = this.id;
if (this.stack != null) {
json[r'stack'] = this.stack;
} else {
// json[r'stack'] = null;
}
return json;
}
/// Returns a new [EditorCreateAssetDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditorCreateAssetDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditorCreateAssetDto(
edits: EditorCreateAssetDtoEditsInner.listFromJson(json[r'edits']),
id: mapValueOfType<String>(json, r'id')!,
stack: mapValueOfType<bool>(json, r'stack'),
);
}
return null;
}
static List<EditorCreateAssetDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditorCreateAssetDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditorCreateAssetDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditorCreateAssetDto> mapFromJson(dynamic json) {
final map = <String, EditorCreateAssetDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditorCreateAssetDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditorCreateAssetDto-objects as value to a dart map
static Map<String, List<EditorCreateAssetDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditorCreateAssetDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditorCreateAssetDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'edits',
'id',
};
}

View File

@@ -0,0 +1,146 @@
//
// 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 EditorCreateAssetDtoEditsInner {
/// Returns a new [EditorCreateAssetDtoEditsInner] instance.
EditorCreateAssetDtoEditsInner({
required this.action,
required this.region,
required this.angle,
required this.brightness,
required this.hue,
required this.lightness,
required this.saturation,
});
EditorActionType action;
EditorCropRegion region;
int angle;
int brightness;
int hue;
int lightness;
int saturation;
@override
bool operator ==(Object other) => identical(this, other) || other is EditorCreateAssetDtoEditsInner &&
other.action == action &&
other.region == region &&
other.angle == angle &&
other.brightness == brightness &&
other.hue == hue &&
other.lightness == lightness &&
other.saturation == saturation;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(action.hashCode) +
(region.hashCode) +
(angle.hashCode) +
(brightness.hashCode) +
(hue.hashCode) +
(lightness.hashCode) +
(saturation.hashCode);
@override
String toString() => 'EditorCreateAssetDtoEditsInner[action=$action, region=$region, angle=$angle, brightness=$brightness, hue=$hue, lightness=$lightness, saturation=$saturation]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'action'] = this.action;
json[r'region'] = this.region;
json[r'angle'] = this.angle;
json[r'brightness'] = this.brightness;
json[r'hue'] = this.hue;
json[r'lightness'] = this.lightness;
json[r'saturation'] = this.saturation;
return json;
}
/// Returns a new [EditorCreateAssetDtoEditsInner] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditorCreateAssetDtoEditsInner? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditorCreateAssetDtoEditsInner(
action: EditorActionType.fromJson(json[r'action'])!,
region: EditorCropRegion.fromJson(json[r'region'])!,
angle: mapValueOfType<int>(json, r'angle')!,
brightness: mapValueOfType<int>(json, r'brightness')!,
hue: mapValueOfType<int>(json, r'hue')!,
lightness: mapValueOfType<int>(json, r'lightness')!,
saturation: mapValueOfType<int>(json, r'saturation')!,
);
}
return null;
}
static List<EditorCreateAssetDtoEditsInner> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditorCreateAssetDtoEditsInner>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditorCreateAssetDtoEditsInner.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditorCreateAssetDtoEditsInner> mapFromJson(dynamic json) {
final map = <String, EditorCreateAssetDtoEditsInner>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditorCreateAssetDtoEditsInner.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditorCreateAssetDtoEditsInner-objects as value to a dart map
static Map<String, List<EditorCreateAssetDtoEditsInner>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditorCreateAssetDtoEditsInner>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditorCreateAssetDtoEditsInner.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'action',
'region',
'angle',
'brightness',
'hue',
'lightness',
'saturation',
};
}

View File

@@ -0,0 +1,122 @@
//
// 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 EditorCropRegion {
/// Returns a new [EditorCropRegion] instance.
EditorCropRegion({
required this.height,
required this.left,
required this.top,
required this.width,
});
int height;
int left;
int top;
int width;
@override
bool operator ==(Object other) => identical(this, other) || other is EditorCropRegion &&
other.height == height &&
other.left == left &&
other.top == top &&
other.width == width;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(height.hashCode) +
(left.hashCode) +
(top.hashCode) +
(width.hashCode);
@override
String toString() => 'EditorCropRegion[height=$height, left=$left, top=$top, width=$width]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'height'] = this.height;
json[r'left'] = this.left;
json[r'top'] = this.top;
json[r'width'] = this.width;
return json;
}
/// Returns a new [EditorCropRegion] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EditorCropRegion? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return EditorCropRegion(
height: mapValueOfType<int>(json, r'height')!,
left: mapValueOfType<int>(json, r'left')!,
top: mapValueOfType<int>(json, r'top')!,
width: mapValueOfType<int>(json, r'width')!,
);
}
return null;
}
static List<EditorCropRegion> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EditorCropRegion>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EditorCropRegion.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EditorCropRegion> mapFromJson(dynamic json) {
final map = <String, EditorCropRegion>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EditorCropRegion.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EditorCropRegion-objects as value to a dart map
static Map<String, List<EditorCropRegion>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EditorCropRegion>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EditorCropRegion.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'height',
'left',
'top',
'width',
};
}

View File

@@ -2469,6 +2469,48 @@
]
}
},
"/editor": {
"post": {
"operationId": "createAssetFromEdits",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/EditorCreateAssetDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Editor"
]
}
},
"/faces": {
"get": {
"operationId": "getFaces",
@@ -8562,6 +8604,144 @@
],
"type": "object"
},
"EditorActionAdjust": {
"properties": {
"action": {
"$ref": "#/components/schemas/EditorActionType"
},
"brightness": {
"type": "integer"
},
"hue": {
"type": "integer"
},
"lightness": {
"type": "integer"
},
"saturation": {
"type": "integer"
}
},
"required": [
"action",
"brightness",
"hue",
"lightness",
"saturation"
],
"type": "object"
},
"EditorActionBlur": {
"properties": {
"action": {
"$ref": "#/components/schemas/EditorActionType"
}
},
"required": [
"action"
],
"type": "object"
},
"EditorActionCrop": {
"properties": {
"action": {
"$ref": "#/components/schemas/EditorActionType"
},
"region": {
"$ref": "#/components/schemas/EditorCropRegion"
}
},
"required": [
"action",
"region"
],
"type": "object"
},
"EditorActionRotate": {
"properties": {
"action": {
"$ref": "#/components/schemas/EditorActionType"
},
"angle": {
"type": "integer"
}
},
"required": [
"action",
"angle"
],
"type": "object"
},
"EditorActionType": {
"enum": [
"crop",
"rotate",
"blur",
"adjust"
],
"type": "string"
},
"EditorCreateAssetDto": {
"properties": {
"edits": {
"description": "list of edits",
"items": {
"anyOf": [
{
"$ref": "#/components/schemas/EditorActionCrop"
},
{
"$ref": "#/components/schemas/EditorActionRotate"
},
{
"$ref": "#/components/schemas/EditorActionBlur"
},
{
"$ref": "#/components/schemas/EditorActionAdjust"
}
]
},
"type": "array"
},
"id": {
"description": "Source asset id",
"format": "uuid",
"type": "string"
},
"stack": {
"description": "Stack the edit and the original",
"type": "boolean"
}
},
"required": [
"edits",
"id"
],
"type": "object"
},
"EditorCropRegion": {
"properties": {
"height": {
"type": "integer"
},
"left": {
"type": "integer"
},
"top": {
"type": "integer"
},
"width": {
"type": "integer"
}
},
"required": [
"height",
"left",
"top",
"width"
],
"type": "object"
},
"EmailNotificationsResponse": {
"properties": {
"albumInvite": {

View File

@@ -444,6 +444,38 @@ export type DuplicateResponseDto = {
assets: AssetResponseDto[];
duplicateId: string;
};
export type EditorCropRegion = {
height: number;
left: number;
top: number;
width: number;
};
export type EditorActionCrop = {
action: EditorActionType;
region: EditorCropRegion;
};
export type EditorActionRotate = {
action: EditorActionType;
angle: number;
};
export type EditorActionBlur = {
action: EditorActionType;
};
export type EditorActionAdjust = {
action: EditorActionType;
brightness: number;
hue: number;
lightness: number;
saturation: number;
};
export type EditorCreateAssetDto = {
/** list of edits */
edits: (EditorActionCrop | EditorActionRotate | EditorActionBlur | EditorActionAdjust)[];
/** Source asset id */
id: string;
/** Stack the edit and the original */
stack?: boolean;
};
export type PersonResponseDto = {
birthDate: string | null;
id: string;
@@ -1836,6 +1868,18 @@ export function getAssetDuplicates(opts?: Oazapfts.RequestOpts) {
...opts
}));
}
export function createAssetFromEdits({ editorCreateAssetDto }: {
editorCreateAssetDto: EditorCreateAssetDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 201;
data: AssetResponseDto;
}>("/editor", oazapfts.json({
...opts,
method: "POST",
body: editorCreateAssetDto
})));
}
export function getFaces({ id }: {
id: string;
}, opts?: Oazapfts.RequestOpts) {
@@ -3138,6 +3182,12 @@ export enum EntityType {
Asset = "ASSET",
Album = "ALBUM"
}
export enum EditorActionType {
Crop = "crop",
Rotate = "rotate",
Blur = "blur",
Adjust = "adjust"
}
export enum JobName {
ThumbnailGeneration = "thumbnailGeneration",
MetadataExtraction = "metadataExtraction",

View File

@@ -0,0 +1,19 @@
import { Body, Controller, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { EditorCreateAssetDto } from 'src/dtos/editor.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { EditorService } from 'src/services/editor.service';
@ApiTags('Editor')
@Controller('editor')
export class EditorController {
constructor(private service: EditorService) {}
@Post()
@Authenticated()
createAssetFromEdits(@Auth() auth: AuthDto, @Body() dto: EditorCreateAssetDto): Promise<AssetResponseDto> {
return this.service.createAssetFromEdits(auth, dto);
}
}

View File

@@ -8,6 +8,7 @@ import { AuditController } from 'src/controllers/audit.controller';
import { AuthController } from 'src/controllers/auth.controller';
import { DownloadController } from 'src/controllers/download.controller';
import { DuplicateController } from 'src/controllers/duplicate.controller';
import { EditorController } from 'src/controllers/editor.controller';
import { FaceController } from 'src/controllers/face.controller';
import { ReportController } from 'src/controllers/file-report.controller';
import { JobController } from 'src/controllers/job.controller';
@@ -43,6 +44,7 @@ export const controllers = [
AuthController,
DownloadController,
DuplicateController,
EditorController,
FaceController,
JobController,
LibraryController,

View File

@@ -0,0 +1,98 @@
import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import { ClassConstructor, plainToInstance, Transform, Type } from 'class-transformer';
import { IsEnum, IsInt, ValidateNested } from 'class-validator';
import { ValidateBoolean, ValidateUUID } from 'src/validation';
export enum EditorActionType {
Crop = 'crop',
Rotate = 'rotate',
Blur = 'blur',
Adjust = 'adjust',
}
export class EditorActionItem {
@IsEnum(EditorActionType)
@ApiProperty({ enum: EditorActionType, enumName: 'EditorActionType' })
action!: EditorActionType;
}
export class EditorActionAdjust extends EditorActionItem {
@IsInt()
@ApiProperty({ type: 'integer' })
brightness!: number;
@IsInt()
@ApiProperty({ type: 'integer' })
saturation!: number;
@IsInt()
@ApiProperty({ type: 'integer' })
hue!: number;
@IsInt()
@ApiProperty({ type: 'integer' })
lightness!: number;
}
export class EditorActionBlur extends EditorActionItem {}
class EditorCropRegion {
@IsInt()
@ApiProperty({ type: 'integer' })
left!: number;
@IsInt()
@ApiProperty({ type: 'integer' })
top!: number;
@IsInt()
@ApiProperty({ type: 'integer' })
width!: number;
@IsInt()
@ApiProperty({ type: 'integer' })
height!: number;
}
export class EditorActionCrop extends EditorActionItem {
@Type(() => EditorCropRegion)
@ValidateNested()
region!: EditorCropRegion;
}
export class EditorActionRotate extends EditorActionItem {
@IsInt()
@ApiProperty({ type: 'integer' })
angle!: number;
}
export type EditorAction = EditorActionRotate | EditorActionBlur | EditorActionCrop | EditorActionAdjust;
const actionToClass: Record<EditorActionType, ClassConstructor<EditorAction>> = {
[EditorActionType.Crop]: EditorActionCrop,
[EditorActionType.Rotate]: EditorActionRotate,
[EditorActionType.Blur]: EditorActionBlur,
[EditorActionType.Adjust]: EditorActionAdjust,
};
const getActionClass = (item: EditorActionItem): ClassConstructor<EditorAction> =>
actionToClass[item.action] || EditorActionItem;
@ApiExtraModels(EditorActionRotate, EditorActionBlur, EditorActionCrop, EditorActionAdjust)
export class EditorCreateAssetDto {
/** Source asset id */
@ValidateUUID()
id!: string;
/** Stack the edit and the original */
@ValidateBoolean({ optional: true })
stack?: boolean;
/** list of edits */
@ValidateNested({ each: true })
@Transform(({ value: edits }) =>
Array.isArray(edits) ? edits.map((item) => plainToInstance(getActionClass(item), item)) : edits,
)
@ApiProperty({ anyOf: Object.values(actionToClass).map((target) => ({ $ref: getSchemaPath(target) })) })
edits!: EditorAction[];
}

View File

@@ -117,7 +117,7 @@ export interface IBaseJob {
export interface IEntityJob extends IBaseJob {
id: string;
source?: 'upload' | 'sidecar-write' | 'copy';
source?: 'upload' | 'sidecar-write' | 'copy' | 'editor';
}
export interface IAssetDeleteJob extends IEntityJob {

View File

@@ -1,4 +1,5 @@
import { Writable } from 'node:stream';
import { Region } from 'sharp';
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/config';
export const IMediaRepository = 'IMediaRepository';
@@ -71,6 +72,20 @@ export interface BitrateDistribution {
unit: string;
}
export type MediaEditItem =
| { action: 'crop'; region: Region }
| { action: 'rotate'; angle: number }
| { action: 'blur' }
| {
action: 'modulate';
brightness?: number;
saturation?: number;
hue?: number;
lightness?: number;
};
export type MediaEdits = MediaEditItem[];
export interface VideoCodecSWConfig {
getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream: AudioStreamInfo): TranscodeCommand;
}
@@ -89,4 +104,7 @@ export interface IMediaRepository {
// video
probe(input: string): Promise<VideoInfo>;
transcode(input: string, output: string | Writable, command: TranscodeCommand): Promise<void>;
// editor
applyEdits(input: string, output: string, edits: MediaEditItem[]): Promise<void>;
}

View File

@@ -8,8 +8,9 @@ import sharp from 'sharp';
import { Colorspace } from 'src/config';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import {
IMediaRepository,
ImageDimensions,
IMediaRepository,
MediaEdits,
ThumbnailOptions,
TranscodeCommand,
VideoInfo,
@@ -44,6 +45,41 @@ export class MediaRepository implements IMediaRepository {
return true;
}
async applyEdits(input: string, output: string, edits: MediaEdits) {
const pipeline = sharp(input, { failOn: 'error', limitInputPixels: false }).keepMetadata();
for (const edit of edits) {
switch (edit.action) {
case 'crop': {
pipeline.extract(edit.region);
break;
}
case 'rotate': {
pipeline.rotate(edit.angle);
break;
}
case 'blur': {
pipeline.blur(true);
break;
}
case 'modulate': {
pipeline.modulate({
brightness: edit.brightness,
saturation: edit.saturation,
hue: edit.hue,
lightness: edit.lightness,
});
break;
}
}
}
await pipeline.toFile(output);
}
async generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise<void> {
// some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes
const pipeline = sharp(input, { failOn: options.processInvalidImages ? 'none' : 'error', limitInputPixels: false })

View File

@@ -0,0 +1,128 @@
import { BadRequestException, Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
import { dirname } from 'node:path';
import { AccessCore, Permission } from 'src/cores/access.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import {
EditorAction,
EditorActionAdjust,
EditorActionBlur,
EditorActionCrop,
EditorActionRotate,
EditorActionType,
EditorCreateAssetDto,
} from 'src/dtos/editor.dto';
import { AssetType } from 'src/entities/asset.entity';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMediaRepository, MediaEditItem } from 'src/interfaces/media.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
@Injectable()
export class EditorService {
private access: AccessCore;
constructor(
@Inject(IAccessRepository) accessRepository: IAccessRepository,
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {
this.access = AccessCore.create(accessRepository);
}
async createAssetFromEdits(auth: AuthDto, dto: EditorCreateAssetDto): Promise<AssetResponseDto> {
await this.access.requirePermission(auth, Permission.ASSET_VIEW, dto.id);
const asset = await this.assetRepository.getById(dto.id);
if (!asset) {
throw new BadRequestException('Asset not found');
}
if (asset.type !== AssetType.IMAGE) {
throw new BadRequestException('Only images can be edited');
}
const uuid = this.cryptoRepository.randomUUID();
const outputFile = StorageCore.getNestedPath(StorageFolder.UPLOAD, auth.user.id, uuid);
this.storageRepository.mkdirSync(dirname(outputFile));
await this.mediaRepository.applyEdits(asset.originalPath, outputFile, this.asMediaEdits(dto.edits));
try {
const checksum = await this.cryptoRepository.hashFile(outputFile);
const { size } = await this.storageRepository.stat(outputFile);
const newAsset = await this.assetRepository.create({
id: uuid,
ownerId: auth.user.id,
deviceId: 'immich-editor',
deviceAssetId: asset.deviceAssetId + `-edit-${Date.now()}`,
libraryId: null,
type: asset.type,
originalPath: outputFile,
localDateTime: asset.localDateTime,
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
isFavorite: false,
isArchived: false,
isExternal: false,
isOffline: false,
checksum,
isVisible: true,
originalFileName: asset.originalFileName,
sidecarPath: null,
tags: asset.tags,
duplicateId: null,
});
await this.assetRepository.upsertExif({ assetId: newAsset.id, fileSizeInByte: size });
await this.jobRepository.queue({
name: JobName.METADATA_EXTRACTION,
data: { id: newAsset.id, source: 'editor' },
});
return mapAsset(newAsset, { auth });
} catch (error: Error | any) {
this.logger.error(`Failed to create asset from edits: ${error}`, error?.stack);
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [outputFile] } });
throw new InternalServerErrorException('Failed to create asset from edits');
}
}
private asMediaEdits(edits: EditorAction[]) {
const mediaEdits: MediaEditItem[] = [];
for (const { action, ...options } of edits) {
switch (action) {
case EditorActionType.Crop: {
mediaEdits.push({ ...(options as EditorActionCrop), action: 'crop' });
break;
}
case EditorActionType.Rotate: {
mediaEdits.push({ ...(options as EditorActionRotate), action: 'rotate' });
break;
}
case EditorActionType.Blur: {
mediaEdits.push({ ...(options as EditorActionBlur), action: 'blur' });
break;
}
case EditorActionType.Adjust: {
mediaEdits.push({ ...(options as EditorActionAdjust), action: 'modulate' });
break;
}
}
}
return mediaEdits;
}
}

View File

@@ -10,6 +10,7 @@ import { CliService } from 'src/services/cli.service';
import { DatabaseService } from 'src/services/database.service';
import { DownloadService } from 'src/services/download.service';
import { DuplicateService } from 'src/services/duplicate.service';
import { EditorService } from 'src/services/editor.service';
import { JobService } from 'src/services/job.service';
import { LibraryService } from 'src/services/library.service';
import { MapService } from 'src/services/map.service';
@@ -50,6 +51,7 @@ export const services = [
DatabaseService,
DownloadService,
DuplicateService,
EditorService,
JobService,
LibraryService,
MapService,

View File

@@ -250,7 +250,7 @@ export class JobService {
}
case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: {
if (item.data.source === 'upload' || item.data.source === 'copy') {
if (item.data.source === 'upload' || item.data.source === 'copy' || item.data.source === 'editor') {
await this.jobRepository.queue({ name: JobName.GENERATE_PREVIEW, data: item.data });
}
break;
@@ -271,7 +271,7 @@ export class JobService {
{ name: JobName.GENERATE_THUMBHASH, data: item.data },
];
if (item.data.source === 'upload') {
if (item.data.source === 'upload' || item.data.source === 'editor') {
jobs.push({ name: JobName.SMART_SEARCH, data: item.data }, { name: JobName.FACE_DETECTION, data: item.data });
const [asset] = await this.assetRepository.getByIds([item.data.id]);
@@ -289,7 +289,7 @@ export class JobService {
}
case JobName.GENERATE_THUMBNAIL: {
if (item.data.source !== 'upload') {
if (item.data.source !== 'upload' && item.data.source !== 'editor') {
break;
}

View File

@@ -9,5 +9,6 @@ export const newMediaRepositoryMock = (): Mocked<IMediaRepository> => {
probe: vitest.fn(),
transcode: vitest.fn(),
getImageDimensions: vitest.fn(),
applyEdits: vitest.fn(),
};
};