info: improved listing for directories with same name

This commit is contained in:
Thibault Deckers 2021-10-09 20:33:10 +09:00
parent b84fde14af
commit 34f8b9cef9
2 changed files with 146 additions and 99 deletions

View file

@ -52,10 +52,10 @@ import deckers.thibault.aves.metadata.XMP.isPanorama
import deckers.thibault.aves.model.FieldMap import deckers.thibault.aves.model.FieldMap
import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.LogUtils
import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes
import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface import deckers.thibault.aves.utils.MimeTypes.canReadWithExifInterface
import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor import deckers.thibault.aves.utils.MimeTypes.canReadWithMetadataExtractor
import deckers.thibault.aves.utils.MimeTypes.isHeic
import deckers.thibault.aves.utils.MimeTypes.isImage
import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.isVideo
import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern import deckers.thibault.aves.utils.MimeTypes.tiffExtensionPattern
import deckers.thibault.aves.utils.StorageUtils import deckers.thibault.aves.utils.StorageUtils
@ -104,34 +104,45 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java) foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java) foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java)
val uuidDirCount = HashMap<String, Int>() val uuidDirCount = HashMap<String, Int>()
for (dir in metadata.directories.filter { val dirByName = metadata.directories.filter {
it.tagCount > 0 it.tagCount > 0
&& it !is FileTypeDirectory && it !is FileTypeDirectory
&& it !is AviDirectory && it !is AviDirectory
}) { }.groupBy { dir -> dir.name }
// directory name for (dirEntry in dirByName) {
var dirName = dir.name val baseDirName = dirEntry.key
if (dir is Mp4UuidBoxDirectory) {
val uuid = dir.getString(Mp4UuidBoxDirectory.TAG_UUID).substringBefore('-')
dirName += " $uuid"
val count = uuidDirCount[uuid] ?: 0
uuidDirCount[uuid] = count + 1
if (count > 0) {
dirName += " ($count)"
}
}
// exclude directories known to be redundant with info derived on the Dart side // exclude directories known to be redundant with info derived on the Dart side
// they are excluded by name instead of runtime type because excluding `Mp4Directory` // they are excluded by name instead of runtime type because excluding `Mp4Directory`
// would also exclude derived directories, such as `Mp4UuidBoxDirectory` // would also exclude derived directories, such as `Mp4UuidBoxDirectory`
if (allMetadataRedundantDirNames.contains(dirName)) continue if (allMetadataRedundantDirNames.contains(baseDirName)) continue
val sameNameDirs = dirEntry.value
val sameNameDirCount = sameNameDirs.size
for (dirIndex in 0 until sameNameDirCount) {
val dir = sameNameDirs[dirIndex]
// directory name
var thisDirName = baseDirName
if (dir is Mp4UuidBoxDirectory) {
val uuid = dir.getString(Mp4UuidBoxDirectory.TAG_UUID).substringBefore('-')
thisDirName += " $uuid"
val count = uuidDirCount[uuid] ?: 0
uuidDirCount[uuid] = count + 1
if (count > 0) {
thisDirName += " ($count)"
}
} else if (sameNameDirCount > 1 && !allMetadataMergeableDirNames.contains(baseDirName)) {
// optional count for multiple directories of the same type
thisDirName = "$thisDirName[${dirIndex + 1}]"
}
// optional parent to distinguish child directories of the same type // optional parent to distinguish child directories of the same type
dir.parent?.name?.let { dirName = "$it/$dirName" } dir.parent?.name?.let { thisDirName = "$it/$thisDirName" }
val dirMap = metadataMap[dirName] ?: HashMap() val dirMap = metadataMap[thisDirName] ?: HashMap()
metadataMap[dirName] = dirMap metadataMap[thisDirName] = dirMap
// tags // tags
val tags = dir.tags val tags = dir.tags
@ -183,20 +194,20 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
GSpherical.SPHERICAL_VIDEO_V1_UUID -> { GSpherical.SPHERICAL_VIDEO_V1_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA) val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe()) metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
metadataMap.remove(dirName) metadataMap.remove(thisDirName)
} }
QuickTimeMetadata.PROF_UUID -> { QuickTimeMetadata.PROF_UUID -> {
// redundant with info derived on the Dart side // redundant with info derived on the Dart side
metadataMap.remove(dirName) metadataMap.remove(thisDirName)
} }
QuickTimeMetadata.USMT_UUID -> { QuickTimeMetadata.USMT_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA) val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
val blocks = QuickTimeMetadata.parseUuidUsmt(bytes) val blocks = QuickTimeMetadata.parseUuidUsmt(bytes)
if (blocks.isNotEmpty()) { if (blocks.isNotEmpty()) {
metadataMap.remove(dirName) metadataMap.remove(thisDirName)
dirName = "QuickTime User Media" thisDirName = "QuickTime User Media"
val usmt = metadataMap[dirName] ?: HashMap() val usmt = metadataMap[thisDirName] ?: HashMap()
metadataMap[dirName] = usmt metadataMap[thisDirName] = usmt
blocks.forEach { blocks.forEach {
var key = it.type var key = it.type
@ -218,6 +229,7 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
} }
} }
} }
}
} catch (e: Exception) { } catch (e: Exception) {
Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e) Log.w(LOG_TAG, "failed to get metadata by metadata-extractor for uri=$uri", e)
} catch (e: NoClassDefFoundError) { } catch (e: NoClassDefFoundError) {
@ -767,6 +779,16 @@ class MetadataFetchHandler(private val context: Context) : MethodCallHandler {
"QuickTime Sound", "QuickTime Sound",
"QuickTime Video", "QuickTime Video",
) )
private val allMetadataMergeableDirNames = setOf(
"Exif SubIFD",
"GIF Control",
"GIF Image",
"HEIF",
"ICC Profile",
"IPTC",
"WebP",
"XMP",
)
// catalog metadata // catalog metadata
private const val KEY_MIME_TYPE = "mimeType" private const val KEY_MIME_TYPE = "mimeType"

View file

@ -40,9 +40,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
ValueNotifier<Map<String, MetadataDirectory>> get metadataNotifier => widget.metadataNotifier; ValueNotifier<Map<String, MetadataDirectory>> get metadataNotifier => widget.metadataNotifier;
// directory names may contain the name of their parent directory // directory names may contain the name of their parent directory (as prefix + '/')
// if so, they are separated by this character // directory names may contain an index (as suffix in '[]')
static const parentChildSeparator = '/'; static final directoryNamePattern = RegExp(r'^((?<parent>.*?)/)?(?<name>.*?)(\[(?<index>\d+)\])?$');
@override @override
void initState() { void initState() {
@ -138,14 +138,27 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
var directoryName = dirKV.key as String; var directoryName = dirKV.key as String;
String? parent; String? parent;
final parts = directoryName.split(parentChildSeparator); int? index;
if (parts.length > 1) { final match = directoryNamePattern.firstMatch(directoryName);
parent = parts[0]; if (match != null) {
directoryName = parts[1]; parent = match.namedGroup('parent');
final nameMatch = match.namedGroup('name');
if (nameMatch != null) {
directoryName = nameMatch;
}
final indexMatch = match.namedGroup('index');
if (indexMatch != null) {
index = int.tryParse(indexMatch);
}
} }
final rawTags = dirKV.value as Map; final rawTags = dirKV.value as Map;
return MetadataDirectory(directoryName, parent, _toSortedTags(rawTags)); return MetadataDirectory(
directoryName,
_toSortedTags(rawTags),
parent: parent,
index: index,
);
}).toList(); }).toList();
if (entry.isVideo || (entry.mimeType == MimeTypes.heif && entry.isMultiPage)) { if (entry.isVideo || (entry.mimeType == MimeTypes.heif && entry.isMultiPage)) {
@ -157,6 +170,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) { if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) {
title = '${dir.parent}/$title'; title = '${dir.parent}/$title';
} }
if (dir.index != null) {
title += ' ${dir.index}';
}
return MapEntry(title, dir); return MapEntry(title, dir);
}).toList() }).toList()
..sort((a, b) => compareAsciiUpperCase(a.key, b.key)); ..sort((a, b) => compareAsciiUpperCase(a.key, b.key));
@ -171,7 +187,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
final formattedMediaTags = VideoMetadataFormatter.formatInfo(mediaInfo); final formattedMediaTags = VideoMetadataFormatter.formatInfo(mediaInfo);
if (formattedMediaTags.isNotEmpty) { if (formattedMediaTags.isNotEmpty) {
// overwrite generic directory found from the platform side // overwrite generic directory found from the platform side
directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, null, _toSortedTags(formattedMediaTags))); directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, _toSortedTags(formattedMediaTags)));
} }
if (mediaInfo.containsKey(Keys.streams)) { if (mediaInfo.containsKey(Keys.streams)) {
@ -210,7 +226,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream); final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream);
if (formattedStreamTags.isNotEmpty) { if (formattedStreamTags.isNotEmpty) {
final color = stringToColor(typeText); final color = stringToColor(typeText);
directories.add(MetadataDirectory(dirName, null, _toSortedTags(formattedStreamTags), color: color)); directories.add(MetadataDirectory(dirName, _toSortedTags(formattedStreamTags), color: color));
} }
} }
} }
@ -232,7 +248,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
final names = value.whereNotNull().toSet().toList()..sort(compareAsciiUpperCase); final names = value.whereNotNull().toSet().toList()..sort(compareAsciiUpperCase);
return MapEntry(key, '$count items: ${names.join(', ')}'); return MapEntry(key, '$count items: ${names.join(', ')}');
}); });
directories.add(MetadataDirectory('Attachments', null, _toSortedTags(rawTags))); directories.add(MetadataDirectory('Attachments', _toSortedTags(rawTags)));
} }
} }
} }
@ -254,6 +270,7 @@ class MetadataDirectory {
final String name; final String name;
final Color? color; final Color? color;
final String? parent; final String? parent;
final int? index;
final SplayTreeMap<String, String> allTags; final SplayTreeMap<String, String> allTags;
final SplayTreeMap<String, String> tags; final SplayTreeMap<String, String> tags;
@ -265,14 +282,22 @@ class MetadataDirectory {
const MetadataDirectory( const MetadataDirectory(
this.name, this.name,
this.parent,
this.allTags, { this.allTags, {
SplayTreeMap<String, String>? tags, SplayTreeMap<String, String>? tags,
this.color, this.color,
this.parent,
this.index,
}) : tags = tags ?? allTags; }) : tags = tags ?? allTags;
MetadataDirectory filterKeys(bool Function(String key) testKey) { MetadataDirectory filterKeys(bool Function(String key) testKey) {
final filteredTags = SplayTreeMap.of(Map.fromEntries(allTags.entries.where((kv) => testKey(kv.key)))); final filteredTags = SplayTreeMap.of(Map.fromEntries(allTags.entries.where((kv) => testKey(kv.key))));
return MetadataDirectory(name, parent, tags, tags: filteredTags, color: color); return MetadataDirectory(
name,
tags,
tags: filteredTags,
color: color,
parent: parent,
index: index,
);
} }
} }