Skip to content

Commit

Permalink
Test updates
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Sep 12, 2024
1 parent 331e1e3 commit c7e149f
Show file tree
Hide file tree
Showing 76 changed files with 812 additions and 161 deletions.
26 changes: 19 additions & 7 deletions osxphotos/cli/timewarp.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def get_help(self, ctx):
**Caution**: This app directly modifies your Photos library database using undocumented features. It may corrupt, damage, or destroy your Photos library. Use at your own caution. I strongly recommend you make a backup of your Photos library before using this script (e.g. use Time Machine).
## Examples
## Examples
**Add 1 day to the date of each photo**
Expand Down Expand Up @@ -130,7 +130,7 @@ def get_help(self, ctx):
`osxphotos timewarp --compare-exif`
**Read the date/time/timezone from the photos' original EXIF metadata to update the photos' date/time/timezone;
**Read the date/time/timezone from the photos' original EXIF metadata to update the photos' date/time/timezone;
if the EXIF data is missing, use the file modification date/time; show verbose output**
`osxphotos timewarp --pull-exif --use-file-time --verbose`
Expand All @@ -151,14 +151,14 @@ def get_help(self, ctx):
- {n}: Match exactly n characters
- {n,}: Match at least n characters
- {n,m}: Match at least n characters and at most m characters
- In addition to `%%` for a literal `%`, the following format codes are supported:
- In addition to `%%` for a literal `%`, the following format codes are supported:
`%^`, `%$`, `%*`, `%|`, `%{`, `%}` for `^`, `$`, `*`, `|`, `{`, `}` respectively
- |: join multiple format codes; each code is tried in order until one matches
- Unlike the standard library, the leading zero is not optional for
- Unlike the standard library, the leading zero is not optional for
%d, %m, %H, %I, %M, %S, %j, %U, %W, and %V
- For optional leading zero, use %-d, %-m, %-H, %-I, %-M, %-S, %-j, %-U, %-W, and %-V
For more information on strptime format codes, see:
For more information on strptime format codes, see:
https://docs.python.org/3/library/datetime.html?highlight=strptime#strftime-and-strptime-format-codes
**Note**: The time zone of the parsed date/time is assumed to be the local time zone.
Expand Down Expand Up @@ -244,6 +244,13 @@ def get_help(self, ctx):
"This changes the date added or imported date in Photos but "
"does not change the date/time/timezone of the photo itself. ",
)
@click.option(
"--reset",
"-R",
is_flag=True,
help="Reset date/time/timezone for selected photos to the original values. "
"This only works on macOS >= 13.0 (Ventura).",
)
@click.option(
"--inspect",
"-i",
Expand Down Expand Up @@ -405,6 +412,7 @@ def timewarp(
plain,
timestamp,
force,
reset,
):
"""Adjust date/time/timezone of photos in Apple Photos.
Expand All @@ -429,6 +437,7 @@ def timewarp(
parse_date,
pull_exif,
push_exif,
reset,
time_delta,
time,
timezone,
Expand All @@ -437,7 +446,7 @@ def timewarp(
raise click.UsageError(
"At least one of --date, --date-delta, --time, --time-delta, "
"--timezone, --inspect, --compare-exif, --push-exif, --pull-exif, "
"--parse-date, --function, --date-added, or --date-added-from-photo "
"--parse-date, --reset, --function, --date-added, or --date-added-from-photo "
"must be specified."
)

Expand All @@ -453,6 +462,8 @@ def timewarp(
if add_to_album and not compare_exif:
raise click.UsageError("--add-to-album must be used with --compare-exif.")

# ZZZ add check for --reset and Photos >= 8 , warn if not supported

verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)

if any([compare_exif, push_exif, pull_exif]):
Expand Down Expand Up @@ -491,6 +502,7 @@ def timewarp(
parse_date,
pull_exif,
push_exif,
reset,
time_delta,
time,
timezone,
Expand Down Expand Up @@ -575,7 +587,7 @@ def timewarp(
tz_seconds, tz_str, tz_name = tzinfo.get_timezone(photo)
photo_date_local = datetime_naive_to_local(photo.date)
photo_date_tz = datetime_to_new_tz(photo_date_local, tz_seconds)
date_added = datetime_naive_to_local(get_photo_date_added_(photo))
date_added = get_photo_date_added_(photo)
echo(
f"[filename]{photo.filename}[/filename], [uuid]{photo.uuid}[/uuid], "
f"[time]{photo_date_local.strftime(DATETIME_FORMAT)}[/time], "
Expand Down
9 changes: 6 additions & 3 deletions osxphotos/exifinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@ def exifinfo_factory(data: dict[str, Any] | None) -> ExifInfo:
codec=data["ZCODEC"],
lens_model=data["ZLENSMODEL"],
# ZDATECREATED, ZTIMEZONEOFFSET, ZTIMEZONENAME added in Ventura / Photos 8 so may not be present
date=photos_datetime(
data.get("ZDATECREATED"), data.get("ZTIMEZONEOFFSET"), default=False
),
tzoffset=data.get("ZTIMEZONEOFFSET"),
tzname=data.get("ZTIMEZONENAME"),
date=photos_datetime(
data.get("ZDATECREATED"),
data.get("ZTIMEZONEOFFSET"),
data.get("ZTIMEZONENAME"),
default=False,
),
)
return exif_info
107 changes: 85 additions & 22 deletions osxphotos/exifwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import contextlib
import dataclasses
import datetime
import json
import logging
import os
Expand All @@ -13,7 +14,7 @@
from typing import TYPE_CHECKING, Any

from ._constants import _MAX_IPTC_KEYWORD_LEN, _OSXPHOTOS_NONE_SENTINEL, _UNKNOWN_PERSON
from .datetime_utils import datetime_tz_to_utc
from .datetime_utils import datetime_has_tz, datetime_tz_to_utc
from .exiftool import ExifTool, ExifToolCaching
from .exportoptions import ExportOptions
from .phototemplate import RenderOptions
Expand Down Expand Up @@ -226,8 +227,11 @@ def exiftool_dict(
EXIF:GPSLatitude, EXIF:GPSLongitude
EXIF:GPSPosition
EXIF:DateTimeOriginal
EXIF:OffsetTimeOriginal
EXIF:SubSecTimeOriginal
EXIF:OffsetTimeOriginal (UTC offset for DateTimeOriginal)
EXIF:ModifyDate
EXIF:SubSecTime
EXIF:OffsetTime (UTC offset for ModifyDate)
IPTC:DateCreated
IPTC:TimeCreated
QuickTime:CreationDate
Expand Down Expand Up @@ -417,18 +421,16 @@ def exiftool_dict(

if options.datetime:
date = self.photo.date
offsettime = date.strftime("%z")
# find timezone offset in format "-04:00"
offset = re.findall(r"([+-]?)([\d]{2})([\d]{2})", offsettime)
offset = offset[0] # findall returns list of tuples
offsettime = f"{offset[0]}{offset[1]}:{offset[2]}"
offsettime = utc_offset_time(date)
subsec = subsec_time(date)

# exiftool expects format to "2015:01:18 12:00:00"
datetimeoriginal = date.strftime("%Y:%m:%d %H:%M:%S")
datetimeoriginal = exiftool_datetime(date)

if self.photo.isphoto:
exif["EXIF:DateTimeOriginal"] = datetimeoriginal
exif["EXIF:CreateDate"] = datetimeoriginal
exif["EXIF:SubSecTimeOriginal"] = subsec
exif["EXIF:OffsetTimeOriginal"] = offsettime

dateoriginal = date.strftime("%Y:%m:%d")
Expand All @@ -441,13 +443,15 @@ def exiftool_dict(
self.photo.date_modified is not None
and not options.ignore_date_modified
):
exif["EXIF:ModifyDate"] = self.photo.date_modified.strftime(
"%Y:%m:%d %H:%M:%S"
exif["EXIF:ModifyDate"] = exiftool_datetime(
self.photo.date_modified
)
exif["EXIF:SubSecTime"] = subsec_time(self.photo.date_modified)
exif["EXIF:OffsetTime"] = utc_offset_time(self.photo.date_modified)
else:
exif["EXIF:ModifyDate"] = self.photo.date.strftime(
"%Y:%m:%d %H:%M:%S"
)
exif["EXIF:ModifyDate"] = exiftool_datetime(self.photo.date)

This comment has been minimized.

Copy link
@oPromessa

oPromessa Sep 15, 2024

Contributor

Hello @RhetTbull, would you consider also updating -FileModifyDate (although it's an OS tag and not EXIF?).

difference between "File Creation Date/Time" and "Create Date"?

exif["EXIF:SubSectime"] = subsec
exif["EXIF:OffsetTime"] = offsettime
elif self.photo.ismovie:
# QuickTime spec specifies times in UTC
# QuickTime:CreateDate and ModifyDate are in UTC w/ no timezone
Expand All @@ -462,14 +466,14 @@ def exiftool_dict(
exif["QuickTime:ContentCreateDate"] = f"{datetimeoriginal}{offsettime}"

date_utc = datetime_tz_to_utc(date)
creationdate = date_utc.strftime("%Y:%m:%d %H:%M:%S")
creationdate = exiftool_datetime(date_utc)
exif["QuickTime:CreateDate"] = creationdate
if self.photo.date_modified is None or options.ignore_date_modified:
exif["QuickTime:ModifyDate"] = creationdate
else:
exif["QuickTime:ModifyDate"] = datetime_tz_to_utc(
self.photo.date_modified
).strftime("%Y:%m:%d %H:%M:%S")
exiftool_datetime(self.photo.date_modified)
)

# if photo in PNG remove any IPTC tags (#1031)
if self.photo.isphoto and self.photo.uti == "public.png":
Expand Down Expand Up @@ -528,28 +532,44 @@ def exiftool_json_sidecar(
Returns: JSON string for dict with exiftool tags / values
Exports the following:
EXIF:ImageDescription
EXIF:ImageDescription (may include template)
XMP:Description (may include template)
IPTC:CaptionAbstract
XMP:Title
IPTC:ObjectName
XMP:TagsList
XMP:TagsList (may include album name, person name, or template)
IPTC:Keywords (may include album name, person name, or template)
XMP:Subject (set to keywords + person)
IPTC:Caption-Abstract
XMP:Subject (set to keywords + persons)
XMP:PersonInImage
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
EXIF:GPSLatitude, EXIF:GPSLongitude
EXIF:GPSPosition
EXIF:DateTimeOriginal
EXIF:OffsetTimeOriginal
EXIF:SubSecTimeOriginal
EXIF:OffsetTimeOriginal (UTC offset for DateTimeOriginal)
EXIF:ModifyDate
IPTC:DigitalCreationDate
EXIF:SubSecTime
EXIF:OffsetTime (UTC offset for ModifyDate)
IPTC:DateCreated
IPTC:TimeCreated
QuickTime:CreationDate
QuickTime:ContentCreateDate
QuickTime:CreateDate (UTC)
QuickTime:ModifyDate (UTC)
QuickTime:GPSCoordinates
UserData:GPSCoordinates
XMP:Rating
XMP:RegionAppliedToDimensionsW
XMP:RegionAppliedToDimensionsH
XMP:RegionAppliedToDimensionsUnit
XMP:RegionName
XMP:RegionType
XMP:RegionAreaX
XMP:RegionAreaY
XMP:RegionAreaW
XMP:RegionAreaH
XMP:RegionAreaUnit
XMP:RegionPersonDisplayName
"""

options = options or ExifOptions()
Expand All @@ -564,3 +584,46 @@ def exiftool_json_sidecar(
exif = exif_new

return json.dumps([exif])


def utc_offset_time(dt: datetime.datetime) -> str:
"""Find the UTC offset for a datetime in format expected by exiftool (+/-HH:MM)
Args:
dt: datetime object to find offset for
Returns: string with offset in format "+/-HH:MM
Raises:
ValueError if datetime is not timezone aware or if timezone cannot be determined
"""
if not datetime_has_tz(dt):
raise ValueError("datetime must be timezone aware")
offsettime = dt.strftime("%z")
offset = re.findall(r"([+-]?)([\d]{2})([\d]{2})", offsettime)
if not offset:
raise ValueError(f"could not parse timezone from datetime {dt}")
offset = offset[0] # findall returns list of tuples
if len(offset) != 3:
raise ValueError(f"could not parse timezone from datetime {dt}")
offsettime = f"{offset[0]}{offset[1]}:{offset[2]}"
return offsettime


def exiftool_datetime(dt: datetime.datetime) -> str:
"""Format a datetime to the format expected by exiftool (YYYY:MM:DD HH:MM:SS)
Args:
dt: datetime to format
Returns: string formatted as date/time value expected by exiftool in YYYY:MM:DD HH:MM:SS format
"""
return dt.strftime("%Y:%m:%d %H:%M:%S")


def subsec_time(dt: datetime.datetime) -> str:
"""Return sub-second time as a string as expected by exiftool for EXIF:SubSecTime"""
# strftime("%f") returns microseconds but only want milliseconds
# if sub-second time is 0, it will return all zeros
# strip off trailing zeroes
return dt.strftime("%f")[:-3].rstrip("0")
Loading

0 comments on commit c7e149f

Please sign in to comment.