#990 renaming: processor for hash (md5/sha1/sha256)

This commit is contained in:
Thibault Deckers 2024-04-22 22:43:21 +02:00
parent 949f9a514f
commit 65e224ef57
12 changed files with 204 additions and 41 deletions

View file

@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
### Added
- Cataloguing: identify Apple variant of HDR images
- Collection: allow using hash (md5/sha1/sha256) when bulk renaming
- option to force using western arabic numerals for dates
### Changed

View file

@ -85,6 +85,7 @@ import deckers.thibault.aves.metadata.xmp.XMP.isMotionPhoto
import deckers.thibault.aves.metadata.xmp.XMP.isPanorama
import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.ContextUtils.queryContentPropValue
import deckers.thibault.aves.utils.HashUtils
import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.TIFF_EXTENSION_PATTERN
@ -104,6 +105,7 @@ import org.json.JSONObject
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import java.text.ParseException
import java.util.Locale
import kotlin.math.roundToInt
import kotlin.math.roundToLong
@ -1307,10 +1309,36 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
return
}
val metadataMap = HashMap<String, Any>()
val metadataMap = HashMap<String, Any?>()
val hashFields = fields.filter { it.startsWith(HASH_FIELD_PREFIX) }.toSet()
metadataMap.putAll(getHashFields(uri, mimeType, sizeBytes, hashFields))
val exifFields = fields.filterNot { hashFields.contains(it) }.toSet()
metadataMap.putAll(getExifFields(uri, mimeType, sizeBytes, exifFields))
result.success(metadataMap)
}
private fun getHashFields(uri: Uri, mimeType: String, sizeBytes: Long?, fields: Set<String>): FieldMap {
val metadataMap = HashMap<String, Any?>()
fields.forEach { field ->
val function = field.substringAfter(HASH_FIELD_PREFIX).lowercase(Locale.ROOT)
try {
Metadata.openSafeInputStream(context, uri, mimeType, sizeBytes)?.use { input ->
metadataMap[field] = HashUtils.getHash(input, function)
}
} catch (e: Exception) {
Log.w(LOG_TAG, "failed to get hash for mimeType=$mimeType uri=$uri function=$function", e)
}
}
return metadataMap
}
private fun getExifFields(uri: Uri, mimeType: String, sizeBytes: Long?, fields: Set<String>): FieldMap {
val metadataMap = HashMap<String, Any?>()
if (fields.isEmpty() || isVideo(mimeType)) {
result.success(metadataMap)
return
return metadataMap
}
var foundExif = false
@ -1359,7 +1387,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
}
}
result.success(metadataMap)
return metadataMap
}
companion object {
@ -1434,6 +1462,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
// additional media key
private const val KEY_HAS_EMBEDDED_PICTURE = "Has Embedded Picture"
private const val HASH_FIELD_PREFIX = "hash"
private const val VALUE_SKIPPED_DATA = "[skipped]"
}
}
}

View file

@ -0,0 +1,35 @@
package deckers.thibault.aves.utils
import java.io.InputStream
import java.math.BigInteger
import java.security.MessageDigest
object HashUtils {
fun getHash(input: InputStream, algorithmKey: String): String {
val algorithm = toMessageDigestAlgorithm(algorithmKey)
val digest = MessageDigest.getInstance(algorithm)
val buffer = ByteArray(1 shl 14)
var read: Int
while ((input.read(buffer).also { read = it }) > 0) {
digest.update(buffer, 0, read)
}
val md5sum = digest.digest()
val output = BigInteger(1, md5sum).toString(16)
return when (algorithm) {
"MD5" -> output.padStart(32, '0') // 128 bits = 32 hex digits
"SHA-1" -> output.padStart(40, '0') // 160 bits = 40 hex digits
"SHA-256" -> output.padStart(64, '0') // 256 bits = 64 hex digits
else -> throw IllegalArgumentException("unsupported hash algorithm: $algorithmKey")
}
}
private fun toMessageDigestAlgorithm(algorithmKey: String): String {
return when (algorithmKey) {
"md5" -> "MD5"
"sha1" -> "SHA-1"
"sha256" -> "SHA-256"
else -> throw IllegalArgumentException("unsupported hash algorithm: $algorithmKey")
}
}
}

View file

@ -53,23 +53,30 @@ extension ExtraMetadataFieldConvert on MetadataField {
return MetadataType.mp4;
case MetadataField.xmpXmpCreateDate:
return MetadataType.xmp;
case MetadataField.hashMd5:
case MetadataField.hashSha1:
case MetadataField.hashSha256:
return MetadataType.file;
}
}
String? get toPlatform {
if (type == MetadataType.exif) {
return _toExifInterfaceTag();
} else {
switch (this) {
case MetadataField.mp4GpsCoordinates:
return 'gpsCoordinates';
case MetadataField.mp4RotationDegrees:
return 'rotationDegrees';
case MetadataField.mp4Xmp:
return 'xmp';
default:
return null;
}
switch (type) {
case MetadataType.exif:
return _toExifInterfaceTag();
case MetadataType.file:
return name;
default:
switch (this) {
case MetadataField.mp4GpsCoordinates:
return 'gpsCoordinates';
case MetadataField.mp4RotationDegrees:
return 'rotationDegrees';
case MetadataField.mp4Xmp:
return 'xmp';
default:
return null;
}
}
}

View file

@ -23,6 +23,8 @@ extension ExtraMetadataTypeConvert on MetadataType {
return 'photoshop_irb';
case MetadataType.xmp:
return 'xmp';
case MetadataType.file:
return 'file';
}
}
}

View file

@ -440,6 +440,7 @@
"renameEntrySetPagePreviewSectionTitle": "Preview",
"renameProcessorCounter": "Counter",
"renameProcessorHash": "Hash",
"renameProcessorName": "Name",
"deleteSingleAlbumConfirmationDialogMessage": "{count, plural, =1{Delete this album and the item in it?} other{Delete this album and the {count} items in it?}}",

View file

@ -39,18 +39,6 @@ class NamingPattern {
final processorKey = match.group(1);
final processorOptions = match.group(3);
switch (processorKey) {
case DateNamingProcessor.key:
if (processorOptions != null) {
processors.add(DateNamingProcessor(processorOptions.trim(), locale));
}
case TagsNamingProcessor.key:
processors.add(TagsNamingProcessor(processorOptions?.trim() ?? ''));
case MetadataFieldNamingProcessor.key:
if (processorOptions != null) {
processors.add(MetadataFieldNamingProcessor(processorOptions.trim()));
}
case NameNamingProcessor.key:
processors.add(const NameNamingProcessor());
case CounterNamingProcessor.key:
int? start, padding;
_applyProcessorOptions(processorOptions, (key, value) {
@ -65,6 +53,22 @@ class NamingPattern {
}
});
processors.add(CounterNamingProcessor(start: start ?? defaultCounterStart, padding: padding ?? defaultCounterPadding));
case DateNamingProcessor.key:
if (processorOptions != null) {
processors.add(DateNamingProcessor(processorOptions.trim(), locale));
}
case HashNamingProcessor.key:
if (processorOptions != null) {
processors.add(HashNamingProcessor(processorOptions.trim()));
}
case MetadataFieldNamingProcessor.key:
if (processorOptions != null) {
processors.add(MetadataFieldNamingProcessor(processorOptions.trim()));
}
case NameNamingProcessor.key:
processors.add(const NameNamingProcessor());
case TagsNamingProcessor.key:
processors.add(TagsNamingProcessor(processorOptions?.trim() ?? ''));
default:
debugPrint('unsupported naming processor: ${match.group(0)}');
}
@ -106,6 +110,8 @@ class NamingPattern {
switch (processorKey) {
case DateNamingProcessor.key:
return '<$processorKey, yyyyMMdd-HHmmss>';
case HashNamingProcessor.key:
return '<$processorKey, md5>';
case TagsNamingProcessor.key:
return '<$processorKey, ->';
case CounterNamingProcessor.key:
@ -204,9 +210,7 @@ class MetadataFieldNamingProcessor extends NamingProcessor {
}
@override
Set<MetadataField> getRequiredFields() {
return {field}.whereNotNull().toSet();
}
Set<MetadataField> getRequiredFields() => {field}.whereNotNull().toSet();
@override
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
@ -247,3 +251,27 @@ class CounterNamingProcessor extends NamingProcessor {
@override
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) => '${index + start}'.padLeft(padding, '0');
}
@immutable
class HashNamingProcessor extends NamingProcessor {
static const key = 'hash';
static const optionFunction = 'function';
late final MetadataField? function;
@override
List<Object?> get props => [function];
HashNamingProcessor(String function) {
final lowerField = 'hash${function.toLowerCase()}';
this.function = MetadataField.values.firstWhereOrNull((v) => v.name.toLowerCase() == lowerField);
}
@override
Set<MetadataField> getRequiredFields() => {function}.whereNotNull().toSet();
@override
String? process(AvesEntry entry, int index, Map<String, dynamic> fieldValues) {
return fieldValues[function?.toPlatform]?.toString();
}
}

View file

@ -14,6 +14,7 @@ extension ExtraMetadataTypeView on MetadataType {
MetadataType.mp4 => 'MP4',
MetadataType.photoshopIrb => 'Photoshop',
MetadataType.xmp => 'XMP',
MetadataType.file => 'File',
};
}
}

View file

@ -119,14 +119,19 @@ class _RenameEntrySetPageState extends State<RenameEntrySetPage> {
icon: AIcons.more,
title: MaterialLocalizations.of(context).moreButtonTooltip,
items: [
MetadataField.exifMake,
MetadataField.exifModel,
]
.map((field) => PopupMenuItem(
value: MetadataFieldNamingProcessor.keyWithField(field),
child: MenuRow(text: field.title),
))
.toList(),
...[
MetadataField.exifMake,
MetadataField.exifModel,
]
.map((field) => PopupMenuItem(
value: MetadataFieldNamingProcessor.keyWithField(field),
child: MenuRow(text: field.title),
)),
PopupMenuItem(
value: HashNamingProcessor.key,
child: MenuRow(text: l10n.renameProcessorHash),
)
],
),
];
},

View file

@ -45,6 +45,8 @@ enum MetadataType {
photoshopIrb,
// XMP: https://en.wikipedia.org/wiki/Extensible_Metadata_Platform
xmp,
// others (hash, etc.)
file,
}
class MetadataTypes {

View file

@ -7,6 +7,7 @@ enum MetadataSyntheticField {
}
enum MetadataField {
// exif
exifDate,
exifDateOriginal,
exifDateDigitized,
@ -46,10 +47,16 @@ enum MetadataField {
exifMake,
exifModel,
exifUserComment,
// mp4
mp4GpsCoordinates,
mp4RotationDegrees,
mp4Xmp,
// xmp
xmpXmpCreateDate,
// file
hashMd5,
hashSha1,
hashSha256,
}
class MetadataFields {

View file

@ -1,9 +1,11 @@
{
"ar": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
"be": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -241,6 +243,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -677,6 +680,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -793,6 +797,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -1229,6 +1234,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"collectionActionSetHome",
"setHomeCustomCollection",
"settingsThumbnailShowHdrIcon",
@ -1494,6 +1500,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -1930,6 +1937,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -1939,6 +1947,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"castDialogTitle",
"aboutDataUsageSectionTitle",
"aboutDataUsageData",
@ -1956,6 +1965,7 @@
],
"es": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -1964,6 +1974,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -2011,6 +2022,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -2527,6 +2539,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -2959,6 +2972,7 @@
],
"fr": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -3089,6 +3103,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -3775,6 +3790,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -4427,6 +4443,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -4859,10 +4876,12 @@
],
"hu": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
"id": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -4871,6 +4890,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -4879,6 +4899,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"collectionActionSetHome",
"setHomeCustomCollection",
"settingsThumbnailShowHdrIcon",
@ -4911,6 +4932,7 @@
"videoResumptionModeAlways",
"widgetTapUpdateWidget",
"vaultBinUsageDialogMessage",
"renameProcessorHash",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"castDialogTitle",
@ -5172,6 +5194,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -5604,6 +5627,7 @@
],
"ko": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -5661,6 +5685,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"renameProcessorHash",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"tooManyItemsErrorDialogMessage",
@ -5964,6 +5989,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -6407,6 +6433,7 @@
"widgetOpenPageHome",
"widgetOpenPageCollection",
"widgetOpenPageViewer",
"renameProcessorHash",
"menuActionConfigureView",
"castDialogTitle",
"aboutDataUsageClearCache",
@ -6532,6 +6559,7 @@
"settingsVideoEnablePip",
"widgetTapUpdateWidget",
"patternDialogEnter",
"renameProcessorHash",
"castDialogTitle",
"aboutDataUsageInternal",
"aboutDataUsageExternal",
@ -6593,6 +6621,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"renameProcessorHash",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"tooManyItemsErrorDialogMessage",
@ -6655,6 +6684,7 @@
"widgetTapUpdateWidget",
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"renameProcessorHash",
"viewDialogSortSectionTitle",
"viewDialogReverseSortOrder",
"castDialogTitle",
@ -6922,6 +6952,7 @@
"renameEntrySetPagePatternFieldLabel",
"renameEntrySetPageInsertTooltip",
"renameProcessorCounter",
"renameProcessorHash",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
"exportEntryDialogFormat",
@ -7306,10 +7337,12 @@
],
"pl": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
"pt": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -7318,10 +7351,12 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
"ru": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -7582,6 +7617,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -8018,6 +8054,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -8276,6 +8313,7 @@
"renameEntrySetPageInsertTooltip",
"renameEntrySetPagePreviewSectionTitle",
"renameProcessorCounter",
"renameProcessorHash",
"renameProcessorName",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
@ -8727,6 +8765,7 @@
"renameAlbumDialogLabel",
"renameAlbumDialogLabelAlreadyExistsHelper",
"renameEntrySetPageTitle",
"renameProcessorHash",
"deleteSingleAlbumConfirmationDialogMessage",
"deleteMultiAlbumConfirmationDialogMessage",
"editEntryDialogTargetFieldsHeader",
@ -9076,6 +9115,7 @@
"authenticateToConfigureVault",
"authenticateToUnlockVault",
"vaultBinUsageDialogMessage",
"renameProcessorHash",
"exportEntryDialogQuality",
"exportEntryDialogWriteMetadata",
"editEntryDateDialogExtractFromTitle",
@ -9430,10 +9470,12 @@
],
"tr": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
"uk": [
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -9442,6 +9484,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -9450,6 +9493,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
],
@ -9458,6 +9502,7 @@
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"renameProcessorHash",
"settingsForceWesternArabicNumeralsTile"
]
}