From e3276e2d0d149c699d00279833576a2808f15878 Mon Sep 17 00:00:00 2001 From: MSP Date: Fri, 13 Sep 2024 17:09:10 +0100 Subject: [PATCH 1/9] exportdb: Implemented --dry-run on some options which change data: create, check, repair, vacuum, save-config, delete-uuid, delete-file, upgrade, sql Used "[Dryrun]" prefix in some output messages when using --dry-run. Don't know if it is aligned with the message output philosophy. ;) migrate_photos_library added to sub_commands (to run only one of sub_commands) --- osxphotos/cli/exportdb.py | 157 +++++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 60 deletions(-) diff --git a/osxphotos/cli/exportdb.py b/osxphotos/cli/exportdb.py index d14939e22..10fc5ae79 100644 --- a/osxphotos/cli/exportdb.py +++ b/osxphotos/cli/exportdb.py @@ -59,12 +59,14 @@ @click.option( "--check-signatures", is_flag=True, - help="Check signatures for all exported photos in the database to find signatures that don't match.", + help="Check signatures for all exported photos in the database to find signatures that don't match. " + "See also option --export-dir.", ) @click.option( "--update-signatures", is_flag=True, - help="Update signatures for all exported photos in the database to match on-disk signatures.", + help="Update signatures for all exported photos in the database to match on-disk signatures. " + "See also option --export-dir.", ) @click.option( "--touch-file", @@ -268,6 +270,7 @@ def exportdb( info, last_run, last_export_dir, + migrate_photos_library, upgrade, repair, report, @@ -323,47 +326,63 @@ def exportdb( ) sys.exit(1) - try: - ExportDB(export_db, export_dir, create) - except Exception as e: - rich_echo_error(f"[error]Error: {e}[/error]") - sys.exit(1) + if not dry_run: + try: + ExportDB(export_db, export_dir, create) + except Exception as e: + rich_echo_error(f"[error]Error: {e}[/error]") + sys.exit(1) + else: + rich_echo(f"Created export database [filepath]{export_db}[/]") + sys.exit(0) else: - rich_echo(f"Created export database [filepath]{export_db}[/]") + rich_echo(f"[Dryrun] Created export database [filepath]{export_db}[/]") sys.exit(0) if check: - errors = sqlite_check_integrity(export_db) - if not errors: - rich_echo(f"Ok: [filepath]{export_db}[/]") - sys.exit(0) + if not dry_run: + errors = sqlite_check_integrity(export_db) + if not errors: + rich_echo(f"Ok: [filepath]{export_db}[/]") + sys.exit(0) + else: + rich_echo_error(f"[error]Errors: [filepath]{export_db}[/][/error]") + for error in errors: + rich_echo_error(error) + sys.exit(1) else: - rich_echo_error(f"[error]Errors: [filepath]{export_db}[/][/error]") - for error in errors: - rich_echo_error(error) - sys.exit(1) + rich_echo(f"[Dryrun] Check [filepath]{export_db}[/]") + sys.exit(0) if repair: - try: - sqlite_repair_db(export_db) - except Exception as e: - rich_echo_error(f"[error]Error: {e}[/error]") - sys.exit(1) + if not dry_run: + try: + sqlite_repair_db(export_db) + except Exception as e: + rich_echo_error(f"[error]Error: {e}[/error]") + sys.exit(1) + else: + rich_echo(f"Ok: [filepath]{export_db}[/]") + sys.exit(0) else: - rich_echo(f"Ok: [filepath]{export_db}[/]") + rich_echo(f"[Dryrun] Repair [filepath]{export_db}[/]") sys.exit(0) if vacuum: - try: - start_size = pathlib.Path(export_db).stat().st_size - export_db_vacuum(export_db) - except Exception as e: - rich_echo_error(f"[error]Error: {e}[/error]") - sys.exit(1) + if not dry_run: + try: + start_size = pathlib.Path(export_db).stat().st_size + export_db_vacuum(export_db) + except Exception as e: + rich_echo_error(f"[error]Error: {e}[/error]") + sys.exit(1) + else: + rich_echo( + f"Vacuumed {export_db}! [num]{start_size}[/] bytes -> [num]{pathlib.Path(export_db).stat().st_size}[/] bytes" + ) + sys.exit(0) else: - rich_echo( - f"Vacuumed {export_db}! [num]{start_size}[/] bytes -> [num]{pathlib.Path(export_db).stat().st_size}[/] bytes" - ) + rich_echo(f"[Dryrun] Vacuum [filepath]{export_db}[/]") sys.exit(0) if update_signatures: @@ -417,13 +436,17 @@ def exportdb( sys.exit(1) if save_config: - try: - export_db_save_config_to_file(export_db, save_config) - except Exception as e: - rich_echo_error(f"[error]Error: {e}[/error]") - sys.exit(1) + if not dry_run: + try: + export_db_save_config_to_file(export_db, save_config) + except Exception as e: + rich_echo_error(f"[error]Error: {e}[/error]") + sys.exit(1) + else: + rich_echo(f"Saved configuration to [filepath]{save_config}") + sys.exit(0) else: - rich_echo(f"Saved configuration to [filepath]{save_config}") + rich_echo(f"[Dryrun] Saved configuration to [filepath]{save_config}") sys.exit(0) if check_signatures: @@ -561,10 +584,11 @@ def exportdb( exportdb = ExportDB(export_db, export_dir) for uuid in delete_uuid: rich_echo(f"Deleting uuid [uuid]{uuid}[/] from database.") - count = exportdb.delete_data_for_uuid(uuid) - rich_echo( - f"Deleted [num]{count}[/] {pluralize(count, 'record', 'records')}." - ) + if not dry_run: + count = exportdb.delete_data_for_uuid(uuid) + rich_echo( + f"Deleted [num]{count}[/] {pluralize(count, 'record', 'records')}." + ) sys.exit(0) if delete_file: @@ -572,10 +596,11 @@ def exportdb( exportdb = ExportDB(export_db, export_dir) for filepath in delete_file: rich_echo(f"Deleting file [filepath]{filepath}[/] from database.") - count = exportdb.delete_data_for_filepath(filepath) - rich_echo( - f"Deleted [num]{count}[/] {pluralize(count, 'record', 'records')}." - ) + if not dry_run: + count = exportdb.delete_data_for_filepath(filepath) + rich_echo( + f"Deleted [num]{count}[/] {pluralize(count, 'record', 'records')}." + ) sys.exit(0) if report: @@ -599,28 +624,40 @@ def exportdb( sys.exit(0) if upgrade: - exportdb = ExportDB(export_db, export_dir) - if upgraded := exportdb.was_upgraded: - rich_echo( - f"Upgraded export database [filepath]{export_db}[/] from version [num]{upgraded[0]}[/] to [num]{upgraded[1]}[/]" - ) + if not dry_run: + exportdb = ExportDB(export_db, export_dir) + if upgraded := exportdb.was_upgraded: + rich_echo( + f"Upgraded export database [filepath]{export_db}[/] from version [num]{upgraded[0]}[/] to [num]{upgraded[1]}[/]" + ) + else: + rich_echo( + f"Export database [filepath]{export_db}[/] is already at latest version [num]{OSXPHOTOS_EXPORTDB_VERSION}[/]" + ) else: + # Does OSXPHOTOS_EXPORTDB_VERSION reflect the actual exportdb file version? rich_echo( - f"Export database [filepath]{export_db}[/] is already at latest version [num]{OSXPHOTOS_EXPORTDB_VERSION}[/]" + f"[Dryrun] Upgrading database [filepath]{export_db}[/]" ) sys.exit(0) if sql: - exportdb = ExportDB(export_db, export_dir) - try: - c = exportdb._conn.cursor() - results = c.execute(sql) - except Exception as e: - rich_echo_error(f"[error]Error: {e}[/error]") - sys.exit(1) + if not dry_run: + exportdb = ExportDB(export_db, export_dir) + try: + c = exportdb._conn.cursor() + results = c.execute(sql) + except Exception as e: + rich_echo_error(f"[error]Error: {e}[/error]") + sys.exit(1) + else: + for row in results: + print(row) + sys.exit(0) else: - for row in results: - print(row) + rich_echo( + f"[Dryrun] SQL_STATEMENT: [filepath]{sql}[/]" + ) sys.exit(0) if migrate_photos_library: From f0cf49f5ac5a67f29941fb3cdb5826ef877915b8 Mon Sep 17 00:00:00 2001 From: MSP Date: Mon, 16 Sep 2024 19:37:10 +0100 Subject: [PATCH 2/9] push-exif --compare location consider QuickTime:GPSCoordaintes for Movies. Aligned with push-exif location (when writing) --- osxphotos/cli/push_exif.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osxphotos/cli/push_exif.py b/osxphotos/cli/push_exif.py index a9c6c3ccf..870cf4edd 100644 --- a/osxphotos/cli/push_exif.py +++ b/osxphotos/cli/push_exif.py @@ -635,10 +635,16 @@ def compare_location(photo: PhotoInfo, file_data: dict[str, Any]) -> str: """Compare location between Photos and original file for a single photo""" photo_latitude = photo.latitude photo_longitude = photo.longitude - exif_latitude = file_data.get("EXIF:GPSLatitude") - exif_longitude = file_data.get("EXIF:GPSLongitude") - exif_latitude_ref = file_data.get("EXIF:GPSLatitudeRef") - exif_longitude_ref = file_data.get("EXIF:GPSLongitudeRef") + if photo.isphoto: + exif_latitude = file_data.get("EXIF:GPSLatitude") + exif_longitude = file_data.get("EXIF:GPSLongitude") + exif_latitude_ref = file_data.get("EXIF:GPSLatitudeRef") + exif_longitude_ref = file_data.get("EXIF:GPSLongitudeRef") + elif photo.ismovie: + exif_coordinates = file_data.get('QuickTime:GPSCoordinates') + exif_latitude, exif_longitude = [float(x) for x in exif_coordinates.split()] if exif_coordinates else [ None, None] + exif_latitude_ref = None + exif_longitude_ref = None if exif_longitude and exif_longitude_ref == "W": exif_longitude = -exif_longitude From 396b7b905573d13e067468b580b1b3a2dada1c4b Mon Sep 17 00:00:00 2001 From: MSP Date: Thu, 19 Sep 2024 10:21:21 +0100 Subject: [PATCH 3/9] QuickTime:GPSCoordinates may return lat, long and altitude. Discard altitude in this case. --- osxphotos/cli/push_exif.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osxphotos/cli/push_exif.py b/osxphotos/cli/push_exif.py index 870cf4edd..d37003073 100644 --- a/osxphotos/cli/push_exif.py +++ b/osxphotos/cli/push_exif.py @@ -642,7 +642,7 @@ def compare_location(photo: PhotoInfo, file_data: dict[str, Any]) -> str: exif_longitude_ref = file_data.get("EXIF:GPSLongitudeRef") elif photo.ismovie: exif_coordinates = file_data.get('QuickTime:GPSCoordinates') - exif_latitude, exif_longitude = [float(x) for x in exif_coordinates.split()] if exif_coordinates else [ None, None] + exif_latitude, exif_longitude = [float(x) for x in exif_coordinates.split()[:2]] if exif_coordinates else [ None, None] exif_latitude_ref = None exif_longitude_ref = None From 5b62238da3bd4b638668051451a6fb9aabd92c21 Mon Sep 17 00:00:00 2001 From: MSP Date: Fri, 20 Sep 2024 19:30:04 +0100 Subject: [PATCH 4/9] Protect exif.get(date fiels) as some old mp4 may return ContentCreationDate as YYYY (eg. 2014) which is converted to int, causing re.match(pattern, dt) to fail. --- osxphotos/exifutils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osxphotos/exifutils.py b/osxphotos/exifutils.py index c1bd0e7f6..af11e6c54 100644 --- a/osxphotos/exifutils.py +++ b/osxphotos/exifutils.py @@ -80,6 +80,9 @@ def get_exif_date_time_offset( for dt_str in time_fields: dt = exif.get(dt_str) + # Some old mp4 may return ContentCreationDate as YYYY (eg. 2014) which + # is converted to int causing re.match(pattern, dt) to fail. + dt = str(dt) if isinstance(dt, int) else dt if dt and dt_str in {"IPTC:DateCreated", "DateCreated"}: # also need time time_ = exif.get("IPTC:TimeCreated") or exif.get("TimeCreated") From ca7bb32b3ccf065c7304c7e0dbbf1a07d10c7c8f Mon Sep 17 00:00:00 2001 From: MSP Date: Sun, 22 Sep 2024 15:09:53 +0100 Subject: [PATCH 5/9] timewarp --inspect and --compare-exif are mutually exclusive. Side note: Would it be best not to generate osxphotos_crash.log file for such situation (a bit over my level to make that change ;) ) Refactor compare_exif_no_markup and compare_exif_with_markup into timewarp_compare_exif --- osxphotos/cli/timewarp.py | 9 ++-- osxphotos/compare_exif.py | 89 ++++++++++++--------------------------- 2 files changed, 32 insertions(+), 66 deletions(-) diff --git a/osxphotos/cli/timewarp.py b/osxphotos/cli/timewarp.py index ac02cfb64..8f63a1360 100644 --- a/osxphotos/cli/timewarp.py +++ b/osxphotos/cli/timewarp.py @@ -441,6 +441,9 @@ def timewarp( "must be specified." ) + if inspect and compare_exif: + raise click.UsageError("--inspect and --compare-exif are mutually exclusive.") + if date and date_delta: raise click.UsageError("--date and --date-delta are mutually exclusive.") @@ -600,11 +603,7 @@ def timewarp( ) for photo in photos: set_crash_data("photo", f"{photo.uuid} {photo.filename}") - diff_results = ( - photocomp.compare_exif_no_markup(photo) - if plain - else photocomp.compare_exif_with_markup(photo) - ) + diff_results = photocomp.timewarp_compare_exif(photo, plain) if not plain: filename = ( diff --git a/osxphotos/compare_exif.py b/osxphotos/compare_exif.py index 76d55b679..20d388566 100644 --- a/osxphotos/compare_exif.py +++ b/osxphotos/compare_exif.py @@ -87,79 +87,45 @@ def compare_exif(self, photo: Photo) -> List[str]: return [photos_date_str, photos_tz_str, exif_date, exif_offset] - def compare_exif_with_markup(self, photo: Photo) -> ExifDiff: + def timewarp_compare_exif(self, photo: Photo, plain: bool = False) -> ExifDiff: """Compare date/time/timezone in Photos to the exif data and return an ExifDiff named tuple; - adds rich markup to strings to show differences + optionally adds rich markup to strings to show differences. Args: photo (Photo): Photo object to compare - """ + plain (bool): Flag to determine if plain (True) or markup (False) should be applied + """ + def compare_values(photo_value: str, exif_value: str) -> tuple: + """Compare two values and return them with or without markup. + + Affects nonlocal variable diff (from timewarp_compare_exif) with result. + """ + + nonlocal diff + if photo_value != exif_value: + diff = True + if not plain: + return change(photo_value), change(exif_value) + else: + if not plain: + return no_change(photo_value), no_change(exif_value) + return photo_value, exif_value + + # Get values from comparison function photos_date, photos_tz, exif_date, exif_tz = self.compare_exif(photo) diff = False - photos_date, photos_time = photos_date.split(" ", 1) - try: - exif_date, exif_time = exif_date.split(" ", 1) - except ValueError: - exif_date = exif_date - exif_time = "" - - if photos_date != exif_date: - photos_date = change(photos_date) - exif_date = change(exif_date) - diff = True - else: - photos_date = no_change(photos_date) - exif_date = no_change(exif_date) - - if photos_time != exif_time: - photos_time = change(photos_time) - exif_time = change(exif_time) - diff = True - else: - photos_time = no_change(photos_time) - exif_time = no_change(exif_time) - - if photos_tz != exif_tz: - photos_tz = change(photos_tz) - exif_tz = change(exif_tz) - diff = True - else: - photos_tz = no_change(photos_tz) - exif_tz = no_change(exif_tz) - - return ExifDiff( - diff, - photos_date, - photos_time, - photos_tz, - exif_date, - exif_time, - exif_tz, - ) - - def compare_exif_no_markup(self, photo: Photo) -> ExifDiff: - """Compare date/time/timezone in Photos to the exif data and return an ExifDiff named tuple; - Args: - photo (Photo): Photo object to compare - """ - photos_date, photos_tz, exif_date, exif_tz = self.compare_exif(photo) - diff = False + # Split date and time photos_date, photos_time = photos_date.split(" ", 1) try: exif_date, exif_time = exif_date.split(" ", 1) except ValueError: - exif_date = exif_date - exif_time = "" - - if photos_date != exif_date: - diff = True - - if photos_time != exif_time: - diff = True + exif_time = "" # Handle missing time in exif_date - if photos_tz != exif_tz: - diff = True + # Compare dates, times, and timezones + photos_date, exif_date = compare_values(photos_date, exif_date) + photos_time, exif_time = compare_values(photos_time, exif_time) + photos_tz, exif_tz = compare_values(photos_tz, exif_tz) return ExifDiff( diff, @@ -170,3 +136,4 @@ def compare_exif_no_markup(self, photo: Photo) -> ExifDiff: exif_time, exif_tz, ) + \ No newline at end of file From 2a06ac2650a9af1ee83eccf35c5cb8832a444f9d Mon Sep 17 00:00:00 2001 From: MSP Date: Sun, 22 Sep 2024 23:34:53 +0100 Subject: [PATCH 6/9] get_exif_date_time_offset: Prioritize QuickTime:ContentCreateDate over EXIF:DateTimeOriginal for videos. --- osxphotos/exifutils.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osxphotos/exifutils.py b/osxphotos/exifutils.py index af11e6c54..33101cab1 100644 --- a/osxphotos/exifutils.py +++ b/osxphotos/exifutils.py @@ -51,16 +51,28 @@ def get_exif_date_time_offset( # set to True if no date/time in EXIF and the FileModifyDate is used used_file_modify_date = False + # determine file type + mime_type = exif.get('File:MIMEType', 'image') + isphoto = mime_type.startswith("image") + ismovie = mime_type.startswith("video") + # search these fields in this order for date/time/timezone - time_fields = [ + # Prioritize QuickTime:ContentCreateDate over EXIF:DateTimeOriginal for videos + start_time_fields = [ "Composite:DateTimeCreated", "Composite:SubSecDateTimeOriginal", "Composite:SubSecCreateDate", + ] + photo_time_fields = [ "EXIF:DateTimeOriginal", "EXIF:CreateDate", + ] + movie_time_fields = [ "QuickTime:ContentCreateDate", "QuickTime:CreationDate", "QuickTime:CreateDate", + ] + end_time_fields = [ "IPTC:DateCreated", "XMP-exif:DateTimeOriginal", "XMP-xmp:DateCreated", @@ -75,6 +87,13 @@ def get_exif_date_time_offset( "ContentCreateDate", "CreationDate", ] + if isphoto: + time_fields = start_time_fields + photo_time_fields + movie_time_fields + end_time_fields + elif ismovie: + time_fields = start_time_fields + movie_time_fields + photo_time_fields + end_time_fields + else: + time_fields = start_time_fields + photo_time_fields + movie_time_fields + end_time_fields + if use_file_modify_date: time_fields.extend(["File:FileModifyDate", "FileModifyDate"]) From b5c618a3ae9594768db6db6de8f30ab3c7db773c Mon Sep 17 00:00:00 2001 From: MSP Date: Wed, 25 Sep 2024 00:02:48 +0100 Subject: [PATCH 7/9] For non-compliant exiftool -OffsetTimeOriginal values, protect get_exif_date_time_offset prior to callingexif_offset_to_seconds. Check if offset matches '+/-hh:mm' format. --- osxphotos/exifutils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osxphotos/exifutils.py b/osxphotos/exifutils.py index c1bd0e7f6..f207f2d24 100644 --- a/osxphotos/exifutils.py +++ b/osxphotos/exifutils.py @@ -119,6 +119,11 @@ def get_exif_date_time_offset( dt = f"{matched.group(1)} 00:00:00" default_time = True + if offset: + # make sure we have offset + if not re.match(r"([+-]\d{2}:\d{2})", offset): + offset = None + offset_seconds = exif_offset_to_seconds(offset) if offset else None if dt: From 72885eb635e2c0bd5b33959e8077e365412d7ba4 Mon Sep 17 00:00:00 2001 From: MSP Date: Wed, 2 Oct 2024 02:27:16 +0100 Subject: [PATCH 8/9] exifutils.py commented debug prints. --- osxphotos/exifutils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osxphotos/exifutils.py b/osxphotos/exifutils.py index 536a5edf9..55565a858 100644 --- a/osxphotos/exifutils.py +++ b/osxphotos/exifutils.py @@ -97,8 +97,11 @@ def get_exif_date_time_offset( if use_file_modify_date: time_fields.extend(["File:FileModifyDate", "FileModifyDate"]) + # print(f"{isphoto=} {ismovie=}") + for dt_str in time_fields: dt = exif.get(dt_str) + # print(f"{dt_str=} {dt=}") # Some old mp4 may return ContentCreationDate as YYYY (eg. 2014) which # is converted to int causing re.match(pattern, dt) to fail. dt = str(dt) if isinstance(dt, int) else dt @@ -117,6 +120,8 @@ def get_exif_date_time_offset( # no date/time found dt = None + # print(f"{dt=} for {dt_str=}") + # try to get offset from EXIF:OffsetTimeOriginal offset = exif.get("EXIF:OffsetTimeOriginal") or exif.get("OffsetTimeOriginal") if dt and not offset: From c54c6860168379f1f17be94b803fbd6fa1b24137 Mon Sep 17 00:00:00 2001 From: MSP Date: Mon, 7 Oct 2024 16:33:37 +0100 Subject: [PATCH 9/9] timewarp --pull-exif but whtn TZ offset == 0 --- osxphotos/exif_datetime_updater.py | 4 +++- osxphotos/exifutils.py | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/osxphotos/exif_datetime_updater.py b/osxphotos/exif_datetime_updater.py index 0671ff248..885b12707 100644 --- a/osxphotos/exif_datetime_updater.py +++ b/osxphotos/exif_datetime_updater.py @@ -178,7 +178,9 @@ def update_photos_from_exif( ) return None - if dtinfo.offset_seconds: + self.verbose(f"[bold]{dtinfo=} {type(dtinfo)=} {dtinfo.offset_seconds=}[/bold]") + + if dtinfo.offset_seconds or dtinfo.offset_seconds == 0: # update timezone then update date/time timezone = Timezone(dtinfo.offset_seconds) tzupdater = PhotoTimeZoneUpdater( diff --git a/osxphotos/exifutils.py b/osxphotos/exifutils.py index 55565a858..5d7710224 100644 --- a/osxphotos/exifutils.py +++ b/osxphotos/exifutils.py @@ -97,11 +97,11 @@ def get_exif_date_time_offset( if use_file_modify_date: time_fields.extend(["File:FileModifyDate", "FileModifyDate"]) - # print(f"{isphoto=} {ismovie=}") + print(f"{isphoto=} {ismovie=}") for dt_str in time_fields: dt = exif.get(dt_str) - # print(f"{dt_str=} {dt=}") + print(f"{dt_str=} {dt=}") # Some old mp4 may return ContentCreationDate as YYYY (eg. 2014) which # is converted to int causing re.match(pattern, dt) to fail. dt = str(dt) if isinstance(dt, int) else dt @@ -120,10 +120,11 @@ def get_exif_date_time_offset( # no date/time found dt = None - # print(f"{dt=} for {dt_str=}") - # try to get offset from EXIF:OffsetTimeOriginal offset = exif.get("EXIF:OffsetTimeOriginal") or exif.get("OffsetTimeOriginal") + + print(f"{dt=} for {dt_str=} and {offset=}") + if dt and not offset: # see if offset set in the dt string for pattern in ( @@ -153,6 +154,8 @@ def get_exif_date_time_offset( offset_seconds = exif_offset_to_seconds(offset) if offset else None + print(f"{offset_seconds=}") + if dt: if offset: # drop offset from dt string and add it back on in datetime %z format @@ -169,11 +172,16 @@ def get_exif_date_time_offset( # some files can have bad date/time data, (e.g. #24, Date/Time Original = 0000:00:00 00:00:00) try: dt = datetime.datetime.strptime(dt, dt_format) + + print(f"Converted dt: {dt=} for {type(dt)=}") + except ValueError: dt = None # format offset in form +/-hhmm offset_str = offset.replace(":", "") if offset else "" + + print(f"FINAL {dt=}, {offset_seconds=}, {offset_str=}, {default_time=}, {used_file_modify_date=}") return ExifDateTime( dt, offset_seconds, offset_str, default_time, used_file_modify_date )