Skip to content

Commit

Permalink
Add support for live photos
Browse files Browse the repository at this point in the history
Look for MediaGroupUUID and ContentIdentifier EXIF tags. These are
present in media files which are part of live photo pairs. If found, any
future files with the same UUID are named the same as the first one and
placed in the same directory.

With this change, live photos and their associated .mov files are moved
together.
  • Loading branch information
qlyoung committed Aug 30, 2023
1 parent f94aa2e commit 49173d8
Show file tree
Hide file tree
Showing 2 changed files with 34 additions and 16 deletions.
4 changes: 2 additions & 2 deletions src/exif.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ def data(self):
def get_exif_command(filename):
# Handle all platform variations
if sys.platform == 'win32':
return f'exiftool -time:all -mimetype -j "{filename}"'
return f'exiftool -time:all -mimetype -j {shlex.quote(filename)}'
return f'exiftool -MediaGroupUUID -ContentIdentifier -time:all -mimetype -j "{filename}"'
return f'exiftool -MediaGroupUUID -ContentIdentifier -time:all -mimetype -j {shlex.quote(filename)}'
46 changes: 32 additions & 14 deletions src/phockup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from src.date import Date
from src.exif import Exif
from collections import defaultdict

logger = logging.getLogger('phockup')
ignored_files = ('.DS_Store', 'Thumbs.db')
Expand All @@ -28,6 +29,7 @@ def __init__(self, input_dir, output_dir, **args):
self.unknown_found = 0
self.files_moved = 0
self.files_copied = 0
self.content_ids = defaultdict(list)

input_dir = os.path.expanduser(input_dir)
output_dir = os.path.expanduser(output_dir)
Expand Down Expand Up @@ -56,7 +58,7 @@ def __init__(self, input_dir, output_dir, **args):
# default to concurrency of one to retain existing behavior
self.max_concurrency = args.get("max_concurrency", 1)
if self.max_concurrency > 1:
logger.info(f"Using {self.max_concurrency} workers to process files.")
logger.debug(f"Using {self.max_concurrency} workers to process files.")

self.stop_depth = self.input_dir.count(os.sep) + self.max_depth \
if self.max_depth > -1 else sys.maxsize
Expand Down Expand Up @@ -85,21 +87,21 @@ def __init__(self, input_dir, output_dir, **args):
self.print_action_report(run_time)

def print_action_report(self, run_time):
logger.info(f"Processed {self.files_processed} files in {run_time:.2f} seconds. Average Throughput: {self.files_processed/run_time:.2f} files/second")
logger.debug(f"Processed {self.files_processed} files in {run_time:.2f} seconds. Average Throughput: {self.files_processed/run_time:.2f} files/second")
if self.unknown_found:
logger.info(f"Found {self.unknown_found} files without EXIF date data.")
logger.debug(f"Found {self.unknown_found} files without EXIF date data.")
if self.duplicates_found:
logger.info(f"Found {self.duplicates_found} duplicate files.")
logger.debug(f"Found {self.duplicates_found} duplicate files.")
if self.files_copied:
if self.dry_run:
logger.info(f"Would have copied {self.files_copied} files.")
logger.debug(f"Would have copied {self.files_copied} files.")
else:
logger.info(f"Copied {self.files_copied} files.")
logger.debug(f"Copied {self.files_copied} files.")
if self.files_moved:
if self.dry_run:
logger.info(f"Would have moved {self.files_moved} files.")
logger.debug(f"Would have moved {self.files_moved} files.")
else:
logger.info(f"Moved {self.files_moved} files.")
logger.debug(f"Moved {self.files_moved} files.")

def check_directories(self):
"""
Expand Down Expand Up @@ -258,16 +260,16 @@ def process_file(self, filename):
and self.file_type != target_file_type:
progress = f"{progress} => skipped, file is '{target_file_type}' \
but looking for '{self.file_type}'"
logger.info(progress)
logger.debug(progress)
break

if self.skip_unknown and output.endswith(self.no_date_dir):
# Skip files that didn't generate a path from EXIF data
progress = f"{progress} => skipped, unknown date EXIF information for '{target_file_name}'"
progress = f"{progress} => skipped, unknown date EXIF debugrmation for '{target_file_name}'"
self.unknown_found += 1
if self.progress:
self.pbar.write(progress)
logger.info(progress)
logger.debug(progress)
break

if os.path.isfile(target_file):
Expand All @@ -276,7 +278,7 @@ def process_file(self, filename):
self.duplicates_found += 1
if self.progress:
self.pbar.write(progress)
logger.info(progress)
logger.debug(progress)
break
else:
if self.move:
Expand Down Expand Up @@ -307,7 +309,7 @@ def process_file(self, filename):
progress = f'{progress} => {target_file}'
if self.progress:
self.pbar.write(progress)
logger.info(progress)
logger.debug(progress)

self.process_xmp(filename, target_file_name, suffix, output)
break
Expand Down Expand Up @@ -342,6 +344,22 @@ def get_file_name_and_path(self, filename):
target_file_name = os.path.basename(filename)

target_file_path = os.path.sep.join([output, target_file_name])

if exif_data and ('ContentIdentifier' in exif_data or 'MediaGroupUUID' in exif_data):
uuid = exif_data['ContentIdentifier'] if 'ContentIdentifier' in exif_data else exif_data['MediaGroupUUID']
logger.debug(f"{filename} has media group ID: {uuid}")
cids = self.content_ids[uuid]

# If a file with this same ContentId UUID exists already, use its
# name and put it in the same directory
if cids:
logger.debug(f"Found existing files with same ID: {cids}")
logger.debug(f"Previous target for current file: {target_file_path}")
target_file_name = os.path.splitext(os.path.basename(cids[0]))[0] + os.path.splitext(filename)[1]
target_file_path = os.path.sep.join([os.path.split(cids[0])[0], target_file_name])
logger.debug(f"New target for current file: {target_file_path}")
cids.append(target_file_path)

return output, target_file_name, target_file_path, target_file_type

def process_xmp(self, original_filename, file_name, suffix, output):
Expand All @@ -364,7 +382,7 @@ def process_xmp(self, original_filename, file_name, suffix, output):

for original, target in xmp_files.items():
xmp_path = os.path.sep.join([output, target])
logger.info(f'{original} => {xmp_path}')
logger.debug(f'{original} => {xmp_path}')

if not self.dry_run:
if self.move:
Expand Down

0 comments on commit 49173d8

Please sign in to comment.