Detect Live Photo companions and show video file size
iOS helper: classify_iphone_file now detects Live Photo MOV companions by checking if IMG_XXXX.MOV has a matching IMG_XXXX.HEIC/JPG in the same DCIM folder. Falls back to size heuristic (<5MB = Live Photo). Live Photo companions appear in 'Live Photo' group (Other section) — excluded from camera-roll default selection. DenseThumb: show file size on video badge (e.g. '▶ 2MB' or '▶ 42MB') so Live Photo clips (small) are visually distinct from real videos (large).
This commit is contained in:
@@ -247,9 +247,14 @@ private fun DenseThumb(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (photo.mediaType == MediaType.VIDEO) {
|
if (photo.mediaType == MediaType.VIDEO) {
|
||||||
|
val sizeLabel = if (photo.sizeBytes > 1024 * 1024)
|
||||||
|
"▶ ${photo.sizeBytes / (1024 * 1024)}MB"
|
||||||
|
else if (photo.sizeBytes > 0)
|
||||||
|
"▶ ${photo.sizeBytes / 1024}KB"
|
||||||
|
else "▶"
|
||||||
Badge(containerColor = MaterialTheme.colorScheme.tertiary,
|
Badge(containerColor = MaterialTheme.colorScheme.tertiary,
|
||||||
modifier = Modifier.align(Alignment.BottomStart).padding(2.dp)) {
|
modifier = Modifier.align(Alignment.BottomStart).padding(2.dp)) {
|
||||||
Text("▶", style = MaterialTheme.typography.labelSmall)
|
Text(sizeLabel, style = MaterialTheme.typography.labelSmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ object ExifFilter {
|
|||||||
fun isCameraRollFolder(folderName: String): Boolean {
|
fun isCameraRollFolder(folderName: String): Boolean {
|
||||||
// iPhone classified groups (from iOS helper classify_iphone_file)
|
// iPhone classified groups (from iOS helper classify_iphone_file)
|
||||||
if (folderName == "Camera" || folderName == "Edited") return true
|
if (folderName == "Camera" || folderName == "Edited") return true
|
||||||
if (folderName in setOf("Screenshots", "Screen Recordings", "Videos", "Other")) return false
|
if (folderName in setOf("Screenshots", "Screen Recordings", "Videos", "Live Photo", "Other")) return false
|
||||||
|
|
||||||
// Android: path-based exclusion
|
// Android: path-based exclusion
|
||||||
val lower = folderName.lowercase()
|
val lower = folderName.lowercase()
|
||||||
|
|||||||
@@ -42,10 +42,13 @@ SCREENSHOT_RE = re.compile(r'.*\.PNG$', re.IGNORECASE)
|
|||||||
# Screen recordings
|
# Screen recordings
|
||||||
SCREEN_RECORDING_RE = re.compile(r'^RPReplay_Final.*\.mp4$', re.IGNORECASE)
|
SCREEN_RECORDING_RE = re.compile(r'^RPReplay_Final.*\.mp4$', re.IGNORECASE)
|
||||||
|
|
||||||
def classify_iphone_file(filename: str) -> str:
|
def classify_iphone_file(filename: str, all_filenames: set = None, size_bytes: int = 0) -> str:
|
||||||
"""
|
"""
|
||||||
Returns a group name for display in the folder panel.
|
Returns a group name for display in the folder panel.
|
||||||
Groups: 'Camera', 'Edited', 'Screenshots', 'Videos', 'Other'
|
Groups: 'Camera', 'Edited', 'Live Photo', 'Screenshots', 'Videos', 'Other'
|
||||||
|
|
||||||
|
all_filenames: set of all filenames in the same DCIM folder (for pairing detection).
|
||||||
|
size_bytes: used to distinguish Live Photo clips (<5MB) from real videos.
|
||||||
"""
|
"""
|
||||||
ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
|
ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
|
||||||
if SCREEN_RECORDING_RE.match(filename):
|
if SCREEN_RECORDING_RE.match(filename):
|
||||||
@@ -54,7 +57,19 @@ def classify_iphone_file(filename: str) -> str:
|
|||||||
return 'Screenshots'
|
return 'Screenshots'
|
||||||
if CAMERA_ORIGINAL_RE.match(filename):
|
if CAMERA_ORIGINAL_RE.match(filename):
|
||||||
if ext in ('mov',):
|
if ext in ('mov',):
|
||||||
return 'Videos'
|
# Check if this is a Live Photo companion
|
||||||
|
base = filename.rsplit('.', 1)[0] # IMG_XXXX
|
||||||
|
is_live_companion = False
|
||||||
|
if all_filenames is not None:
|
||||||
|
# Paired HEIC/JPEG exists with same base name → Live Photo companion
|
||||||
|
for still_ext in ('HEIC', 'HEIF', 'JPG', 'JPEG'):
|
||||||
|
if f'{base}.{still_ext}' in all_filenames:
|
||||||
|
is_live_companion = True
|
||||||
|
break
|
||||||
|
elif size_bytes > 0 and size_bytes < 5 * 1024 * 1024:
|
||||||
|
# No pairing info but very small MOV → likely Live Photo
|
||||||
|
is_live_companion = True
|
||||||
|
return 'Live Photo' if is_live_companion else 'Videos'
|
||||||
return 'Camera'
|
return 'Camera'
|
||||||
if CAMERA_EDITED_RE.match(filename):
|
if CAMERA_EDITED_RE.match(filename):
|
||||||
return 'Edited'
|
return 'Edited'
|
||||||
@@ -109,6 +124,9 @@ async def cmd_list_photos():
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Build a set of all filenames in this folder for Live Photo pairing
|
||||||
|
all_filenames_in_folder = set(entries)
|
||||||
|
|
||||||
for filename in entries:
|
for filename in entries:
|
||||||
ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
|
ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
|
||||||
if ext not in MEDIA_EXTENSIONS:
|
if ext not in MEDIA_EXTENSIONS:
|
||||||
@@ -141,7 +159,7 @@ async def cmd_list_photos():
|
|||||||
'filename': filename,
|
'filename': filename,
|
||||||
'sizeBytes': size_bytes,
|
'sizeBytes': size_bytes,
|
||||||
'dateIso': date_iso,
|
'dateIso': date_iso,
|
||||||
'sourceFolder': classify_iphone_file(filename),
|
'sourceFolder': classify_iphone_file(filename, all_filenames_in_folder, size_bytes),
|
||||||
'mediaType': 'VIDEO' if ext in VIDEO_EXTENSIONS else 'PHOTO',
|
'mediaType': 'VIDEO' if ext in VIDEO_EXTENSIONS else 'PHOTO',
|
||||||
'isHeic': ext in ('heic', 'heif'),
|
'isHeic': ext in ('heic', 'heif'),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user