Skip to content

Commit e2f957e

Browse files
committed
Fixed timezone handling for --parse-date
1 parent a4572d2 commit e2f957e

19 files changed

+595
-190
lines changed

osxphotos/cli/export.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1333,7 +1333,7 @@ def export_cli(
13331333
locals_,
13341334
ignore=["ctx", "cli_obj", "dest", "load_config", "save_config", "config_only"],
13351335
)
1336-
1336+
print(f"{locals_=} {cfg=}")
13371337
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
13381338

13391339
if load_config:

osxphotos/cli/param_types.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"TimeISO8601",
4545
"TimeOffset",
4646
"TimeString",
47-
"UTCOffset",
47+
"TimezoneOffset",
4848
]
4949

5050

@@ -299,20 +299,24 @@ def convert(self, value, param, ctx):
299299
)
300300

301301

302-
class UTCOffset(click.ParamType):
303-
"""A UTC offset timezone in format ±[hh]:[mm], ±[h]:[mm], or ±[hh][mm]"""
302+
class TimezoneOffset(click.ParamType):
303+
"""A UTC offset timezone in format ±[hh]:[mm], ±[h]:[mm], or ±[hh][mm] or a named timezone such as "America/Los_Angeles" """
304304

305-
name = "UTC_OFFSET"
305+
name = "TIMEZONE_OFFSET"
306306

307307
def convert(self, value, param, ctx):
308308
try:
309309
offset_seconds = utc_offset_string_to_seconds(value)
310310
return Timezone(offset_seconds)
311-
except Exception:
312-
self.fail(
313-
f"Invalid timezone format: {value}. "
314-
"Valid format for timezone offset: '±HH:MM', '±H:MM', or '±HHMM'"
315-
)
311+
except ValueError as e:
312+
try:
313+
# might be named timezone
314+
return Timezone(value)
315+
except ValueError as e:
316+
self.fail(
317+
f"Invalid timezone format or name: {value}. "
318+
"Valid format for timezone offset: '±HH:MM', '±H:MM', or '±HHMM' or named timezone such as 'America/Los_Angeles'"
319+
)
316320

317321

318322
class StrpDateTimePattern(click.ParamType):

osxphotos/cli/timewarp.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
StrpDateTimePattern,
5050
TimeOffset,
5151
TimeString,
52-
UTCOffset,
52+
TimezoneOffset,
5353
)
5454
from .rich_progress import rich_progress
5555
from .verbose import get_verbose_console, verbose_print
@@ -75,6 +75,10 @@ def get_help(self, ctx):
7575
7676
`osxphotos timewarp --date 2021-09-10 --time-delta "-1 hour" --timezone -0700 --verbose`
7777
78+
A named timezone can also be specified:
79+
80+
`osxphotos timewarp --date 2021-09-10 --time-delta "-1 hour" --timezone "America/Los_Angeles" --verbose`
81+
7882
This example sets the date for all selected photos to `2021-09-10`, subtracts 1 hour from the time of each photo, and sets the timezone of each photo to `GMT -07:00` (Pacific Daylight Time).
7983
8084
osxphotos timewarp has been well tested on macOS Catalina (10.15). It should work on macOS Big Sur (11.0) and macOS Monterey (12.0) but I have not been able to test this. It will not work on macOS Mojave (10.14) or earlier as the Photos database format is different.
@@ -213,15 +217,18 @@ def get_help(self, ctx):
213217
"--timezone",
214218
"-z",
215219
metavar="TIMEZONE",
216-
type=UTCOffset(),
217-
help="Set timezone for selected photos as offset from UTC. "
218-
"Format is one of '±HH:MM', '±H:MM', or '±HHMM'. "
220+
type=TimezoneOffset(),
221+
help="Set timezone for selected photos as offset from UTC or to named IANA timezone. "
222+
"Format is one of '±HH:MM', '±H:MM', '±HHMM', or named timezone such as 'America/Los_Angeles'. "
219223
"The actual time of the photo is not adjusted which means, somewhat counterintuitively, "
220224
"that the time in the new timezone will be different. "
221225
"For example, if photo has time of 12:00 and timezone of GMT+01:00 and new timezone is specified as "
222226
"'--timezone +02:00' (one hour ahead of current GMT+01:00 timezone), the photo's new time will be 13:00 GMT+02:00, "
223227
"which is equivalent to the old time of 12:00+01:00. "
224228
"This is the same behavior exhibited by Photos when manually adjusting timezone in the Get Info window. "
229+
"Note: when a named timezone is provided, daylight savings time will be considered when adjusting the time; "
230+
"it will not be considered when a UTC offset is provided. "
231+
"For list of valid IANA timezone names, see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones "
225232
"See also --match-time. ",
226233
)
227234
@click.option(

osxphotos/datetime_utils.py

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
# source: https://github.com/RhetTbull/datetime-utils
44

5-
__version__ = "2022.04.30"
5+
__version__ = "2024.10.07"
66

77
import datetime
8+
from zoneinfo import ZoneInfo
89

910
# TODO: probably shouldn't use replace here, see this:
1011
# https://stackoverflow.com/questions/13994594/how-to-add-timezone-into-a-naive-datetime-instance-in-python/13994611#13994611
1112

1213
__all__ = [
14+
"datetime_add_tz",
1315
"datetime_has_tz",
1416
"datetime_naive_to_local",
1517
"datetime_naive_to_utc",
@@ -209,39 +211,40 @@ def utc_offset_seconds(dt: datetime.datetime) -> int:
209211
else:
210212
raise ValueError("dt does not have timezone info")
211213

212-
def datetime_add_tz(dt: datetime.datetime,
213-
tzoffset: int | None = None,
214-
tzname: str | None = None,
215-
) -> datetime.datetime:
216-
"""Add a timezone, either as an offset or named timezone, to a naive datetime.
217214

218-
Args:
219-
dt: naive datetime
220-
tzoffset: offset from UTC for timezone in seconds
221-
tzname: name of timezone, for example, "America/Los_Angeles"
215+
def datetime_add_tz(
216+
dt: datetime.datetime,
217+
tzoffset: int | None = None,
218+
tzname: str | None = None,
219+
) -> datetime.datetime:
220+
"""Add a timezone, either as an offset or named timezone, to a naive datetime.
222221
223-
Returns: timezone-aware datetime with new timezone
222+
Args:
223+
dt: naive datetime
224+
tzoffset: offset from UTC for timezone in seconds
225+
tzname: name of timezone, for example, "America/Los_Angeles"
226+
227+
Returns: timezone-aware datetime with new timezone
228+
229+
Raises:
230+
ValueError if both tzoffset and tzname are None
231+
ValueError if dt is not naive
232+
"""
233+
if datetime_has_tz(dt):
234+
raise ValueError(f"dt must be naive datetime: {dt}")
224235

225-
Note: you may pass tzoffset, tzname or both. If both are passed, the offset will be tried
226-
first
227-
"""
228-
if timestamp is None:
229-
return DEFAULT_DATETIME if default else None
236+
if tzoffset is None and tzname is None:
237+
raise ValueError("Both tzoffset and tzname cannot be None")
230238

231-
tzoffset = tzoffset or 0
239+
tzoffset = tzoffset or 0
240+
if tzname:
232241
try:
233-
dt = datetime.datetime.fromtimestamp(timestamp + TIME_DELTA)
234-
# Try to use tzname if provided
235-
if tzname:
236-
try:
237-
tz = ZoneInfo(tzname)
238-
return dt.astimezone(tz)
239-
except Exception:
240-
# If tzname fails, fall back to tzoffset
241-
pass
242-
243-
# Use tzoffset if tzname wasn't provided or failed
244-
tz = datetime.timezone(datetime.timedelta(seconds=tzoffset))
242+
tz = ZoneInfo(tzname)
245243
return dt.astimezone(tz)
246-
except (ValueError, TypeError):
247-
return DEFAULT_DATETIME if default else None
244+
except Exception:
245+
# If tzname fails, fall back to tzoffset
246+
pass
247+
248+
# Use tzoffset if tzname wasn't provided or failed
249+
tz = datetime.timezone(datetime.timedelta(seconds=tzoffset))
250+
return dt.astimezone(tz)

osxphotos/exif_datetime_updater.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@
1717
)
1818
from .exiftool import ExifTool
1919
from .exifutils import ExifDateTime, get_exif_date_time_offset
20+
from .photodates import update_photo_date_time
2021
from .photosdb import PhotosDB
22+
from .platform import assert_macos
23+
from .utils import noop
24+
25+
assert_macos()
26+
2127
from .phototz import PhotoTimeZone, PhotoTimeZoneUpdater
2228
from .timezones import Timezone, format_offset_time
23-
from .utils import noop
2429

2530
__all__ = ["ExifDateTimeUpdater"]
2631

@@ -193,17 +198,28 @@ def update_photos_from_exif(
193198
if dtinfo.datetime:
194199
if datetime_has_tz(dtinfo.datetime):
195200
# convert datetime to naive local time for setting in photos
196-
local_datetime = datetime_remove_tz(
197-
datetime_utc_to_local(datetime_tz_to_utc(dtinfo.datetime))
198-
)
201+
new_datetime = datetime_remove_tz(dtinfo.datetime)
202+
# local_datetime = datetime_remove_tz(
203+
# datetime_utc_to_local(datetime_tz_to_utc(dtinfo.datetime))
204+
# )
199205
else:
200-
local_datetime = dtinfo.datetime
206+
new_datetime = dtinfo.datetime
207+
# local_datetime = dtinfo.datetime
201208
# update date/time
202-
photo.date = local_datetime
203-
self.verbose(
204-
"Updated date/time for photo "
205-
f"[filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid]): [time]{local_datetime}[/time]"
209+
# photo.date = local_datetime
210+
update_photo_date_time(
211+
self.library_path,
212+
photo,
213+
new_datetime.date(),
214+
new_datetime.time(),
215+
None,
216+
None,
217+
self.verbose,
206218
)
219+
# self.verbose(
220+
# "Updated date/time for photo "
221+
# f"[filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid]): [time]{local_datetime}[/time]"
222+
# )
207223

208224
return None
209225

0 commit comments

Comments
 (0)