Live Photo handling: hide MOV companions, LIVE badge, atomic import/delete

PhonePhoto: add companionDevicePath (MOV path stored on the HEIC).

iOS helper: detect Live Photo pairs by IMG_XXXX filename matching.
MOVs with a paired HEIC get isLivePhotoMotion=true, sourceFolder='Live Photo'.
HEICs with a paired MOV get isLivePhotoStill=true, companionDevicePath set.

Grid: Live Photo MOVs appear only in 'Live Photo' folder (Other section).
HEIC stills show a LIVE badge. Camera folder stays clean.

Import: 'Import Live Photo motion clips' toggle (default off).
When on, Copier pulls the companion MOV alongside the HEIC atomically.

Delete: deleteVerifiedFromDevice also deletes the companion MOV when
deleting a Live Photo still.
This commit is contained in:
Kyle Bolen
2026-07-30 04:43:41 +00:00
parent 6876709051
commit 595d583f6e
8 changed files with 91 additions and 12 deletions

View File

@@ -127,12 +127,40 @@ async def cmd_list_photos():
# Build a set of all filenames in this folder for Live Photo pairing
all_filenames_in_folder = set(entries)
# Pre-compute Live Photo pairs: IMG_XXXX.MOV + IMG_XXXX.HEIC/JPG
# Key: base name (IMG_XXXX) -> MOV device path
live_photo_mov_paths = {}
for fname in entries:
if re.match(r'^IMG_\d{4}\.MOV$', fname, re.IGNORECASE):
base = fname.rsplit('.', 1)[0]
for still_ext in ('HEIC', 'HEIF', 'JPG', 'JPEG'):
if f'{base}.{still_ext}' in all_filenames_in_folder:
# Confirm it's small enough to be a Live Photo clip
# (we'll verify size when we hit the MOV entry)
live_photo_mov_paths[base] = f'{folder_path}/{fname}'
break
for filename in entries:
ext = filename.rsplit('.', 1)[-1].lower() if '.' in filename else ''
if ext not in MEDIA_EXTENSIONS:
continue
device_path = f'{folder_path}/{filename}'
base = filename.rsplit('.', 1)[0]
# Determine Live Photo status
is_live_motion = False
companion_device_path = None
if re.match(r'^IMG_\d{4}\.MOV$', filename, re.IGNORECASE):
if base in live_photo_mov_paths:
# This MOV is a companion — mark it but still include it
# (AppState will filter it from display)
is_live_motion = True
elif re.match(r'^IMG_\d{4}\.(HEIC|HEIF|JPG|JPEG)$', filename, re.IGNORECASE):
if base in live_photo_mov_paths:
companion_device_path = live_photo_mov_paths[base]
# Get file metadata via stat
size_bytes = 0
@@ -162,6 +190,8 @@ async def cmd_list_photos():
'sourceFolder': classify_iphone_file(filename, all_filenames_in_folder, size_bytes),
'mediaType': 'VIDEO' if ext in VIDEO_EXTENSIONS else 'PHOTO',
'isHeic': ext in ('heic', 'heif'),
'isLivePhotoMotion': is_live_motion,
'companionDevicePath': companion_device_path,
})
print(json.dumps(photos))