Skip to content

Commit 7196041

Browse files
authored
Timezone 849 1661 (#1797)
* Working on #1661 * Fix for #1661 * Renamed --timezone --set-timezone * Fix for Timezone test * Added --timezone
1 parent 96f1758 commit 7196041

16 files changed

+3641
-2856
lines changed

Diff for: osxphotos/cli/import_cli.py

+195-45
Large diffs are not rendered by default.

Diff for: osxphotos/export_db_utils.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -610,10 +610,10 @@ def export_db_get_photoinfo_for_filepath(
610610
"""Return photoinfo object for a given filepath
611611
612612
Args:
613-
exportdb: path to the export database
614-
exportdir: path to the export directory or None
613+
exportdb_path: path to the export database
615614
filepath: absolute path to the file to retrieve info for from the database
616615
exiftool: optional path to exiftool to be passed to the PhotoInfoFromDict object
616+
exportdir_path: path to the export directory or None
617617
618618
Returns: PhotoInfoFromDict | None
619619
"""

Diff for: osxphotos/iphoto.py

+13
Original file line numberDiff line numberDiff line change
@@ -1072,6 +1072,11 @@ def date(self) -> datetime.datetime:
10721072
default=True,
10731073
)
10741074

1075+
@property
1076+
def date_original(self) -> datetime.datetime:
1077+
"""Date photo was taken"""
1078+
return self.date
1079+
10751080
@property
10761081
def date_modified(self) -> datetime.datetime:
10771082
"""Date modified in library"""
@@ -1099,6 +1104,11 @@ def tzoffset(self) -> int:
10991104
tz = ZoneInfo(tzname)
11001105
return int(tz.utcoffset(self.date).total_seconds())
11011106

1107+
@property
1108+
def tzname(self) -> str | None:
1109+
"""Timezone name for the asset creation date"""
1110+
return self._db._db_photos[self._uuid]["timezone"] or None
1111+
11021112
@property
11031113
def path(self) -> str | None:
11041114
"""Path to original photo asset in library"""
@@ -1678,6 +1688,9 @@ def asdict(self, shallow: bool = True) -> dict[str, Any]:
16781688
dict_data["shared_moment"] = self.shared_moment
16791689
dict_data["shared_library"] = self.shared_library
16801690
dict_data["rating"] = self.rating
1691+
dict_data["screen_recording"] = self.screen_recording
1692+
dict_data["date_original"] = self.date_original
1693+
dict_data["tzname"] = self.tzname
16811694

16821695
return dict_data
16831696

Diff for: osxphotos/metadata_reader.py

+4
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class MetaData:
4343
tz_offset_sec: int or None if not set, offset from UTC in seconds
4444
height: int or None if not set, height of photo in pixels
4545
width: int or None if not set, width of photo in pixels
46+
tzname: str or None if not set, timezone name for timezone
4647
"""
4748

4849
title: str = ""
@@ -57,6 +58,7 @@ class MetaData:
5758
tz_offset_sec: float | None = None
5859
height: int | None = None
5960
width: int | None = None
61+
tzname: str | None = None
6062

6163
def __ior__(self, other):
6264
if isinstance(other, MetaData):
@@ -399,6 +401,7 @@ def metadata_from_metadata_dict(metadata: dict) -> MetaData:
399401
tz_offset_sec=tz_offset,
400402
height=height,
401403
width=width,
404+
tzname=None, # the sidecar doesn't store the timezone name
402405
)
403406

404407

@@ -537,4 +540,5 @@ def metadata_from_photoinfo(photoinfo: PhotoInfoProtocol) -> MetaData:
537540
tz_offset_sec=tz_offset,
538541
height=photoinfo.height,
539542
width=photoinfo.width,
543+
tzname=photoinfo.tzname,
540544
)

Diff for: osxphotos/photodates.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,8 @@ def update_photo_time_for_new_timezone(
209209
old_timezone_offset = PhotoTimeZone(library_path=library_path).get_timezone(photo)[
210210
0
211211
]
212-
delta = old_timezone_offset - new_timezone.offset
213212
photo_date = photo.date
213+
delta = old_timezone_offset - new_timezone.offset_for_date(photo_date)
214214
new_photo_date = update_datetime(
215215
dt=photo_date, time_delta=datetime.timedelta(seconds=delta)
216216
)

Diff for: osxphotos/photoexporter.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .export_db import ExportDBTemp
2121
from .exportoptions import ExportOptions, ExportResults
2222
from .fileutil import FileUtil
23+
from .photoinfo_common import photoinfo_minify_dict
2324
from .phototemplate import RenderOptions
2425
from .platform import is_macos
2526
from .rich_utils import add_rich_markup_tag
@@ -1045,7 +1046,7 @@ def _export_photo(
10451046
# set data in the database
10461047
with export_db.create_or_get_file_record(dest_str, self.photo.uuid) as rec:
10471048
if rec.photoinfo:
1048-
last_data = json.loads(rec.photoinfo)
1049+
last_data = photoinfo_minify_dict(json.loads(rec.photoinfo))
10491050
# to avoid issues with datetime comparisons, list order
10501051
# need to deserialize from photo.json() instead of using photo.asdict()
10511052
current_data = json.loads(self.photo.json(shallow=True))
@@ -1067,7 +1068,7 @@ def _json_default(o):
10671068
diff = json.dumps(diff, default=_json_default) if diff else None
10681069
else:
10691070
diff = None
1070-
rec.photoinfo = self.photo.json(shallow=True)
1071+
rec.photoinfo = self.photo.json(shallow=False)
10711072
rec.export_options = options.bit_flags
10721073
# don't set src_sig as that is set above before any modifications by convert_to_jpeg or exiftool
10731074
if not options.ignore_signature:

Diff for: osxphotos/photoinfo.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def tzoffset(self) -> int:
173173

174174
@property
175175
def tzname(self) -> str | None:
176-
"""Timezone name for the Photos creation date; on Photos version < 5, returns None"""
176+
"""Timezone name for the asset creation date; on Photos version < 5, returns None"""
177177
return self._info["imageTimeZoneName"]
178178

179179
@property
@@ -2131,6 +2131,7 @@ def asdict(self, shallow: bool = True) -> dict[str, Any]:
21312131
dict_data["rating"] = self.rating
21322132
dict_data["screen_recording"] = self.screen_recording
21332133
dict_data["date_original"] = self.date_original
2134+
dict_data["tzname"] = self.tzname
21342135

21352136
return dict_data
21362137

Diff for: osxphotos/photoinfo_common.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
""" Common utilities for PhotoInfo variations """
2+
3+
from typing import Any
4+
5+
# These are PhotoInfo.asdict() keys that that are removed from the output
6+
# by the PhotoExporter for comparing PhotoInfo objects.
7+
8+
FULL_KEYS = [
9+
"album_info",
10+
"path_derivatives",
11+
"adjustments",
12+
"burst_album_info",
13+
"burst_albums",
14+
"burst_default_pick",
15+
"burst_key",
16+
"burst_photos",
17+
"burst_selected",
18+
"cloud_metadata",
19+
"import_info",
20+
"labels_normalized",
21+
"person_info",
22+
"project_info",
23+
"search_info",
24+
"search_info_normalized",
25+
"syndicated",
26+
"saved_to_library",
27+
"shared_moment",
28+
"shared_library",
29+
"rating",
30+
"screen_recording",
31+
"date_original",
32+
"tzname",
33+
]
34+
35+
36+
def photoinfo_minify_dict(info: dict[str, Any]) -> dict[str, Any]:
37+
"""Convert a full PhotoInfo dict to a minimum PhotoInfo dict"""
38+
return {k: v for k, v in info.items() if k not in FULL_KEYS}

Diff for: osxphotos/photoinfo_dict.py

+15-6
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,23 @@ class PhotoInfoFromDict(PhotoInfoMixin):
5656
@property
5757
def album_info(self) -> AlbumInfoFromDict:
5858
"""Return AlbumInfo objects for photo"""
59+
if getattr(self, "_album_info"):
60+
return self._album_info
5961
# this is a little hacky but it works for `osxphotos import` use case
6062
if not getattr(self, "folders"):
61-
return []
62-
# self.folders is a rehydrated object so need access it's __dict__ to get the actual data
63-
return [
64-
AlbumInfoFromDict(title, folders)
65-
for title, folders in self.folders.__dict__.items()
66-
]
63+
self._album_info = []
64+
else:
65+
# self.folders is a rehydrated object so need access it's __dict__ to get the actual data
66+
self._album_info = [
67+
AlbumInfoFromDict(title, folders)
68+
for title, folders in self.folders.__dict__.items()
69+
]
70+
return self._album_info
71+
72+
@album_info.setter
73+
def album_info(self, value):
74+
"""If rehydrating class has album_info, then set it"""
75+
self._album_info = value
6776

6877
def asdict(self) -> dict[str, Any]:
6978
"""Return the PhotoInfo dictionary"""

Diff for: osxphotos/photoinfo_protocol.py

+4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ def date_modified(self) -> datetime.datetime | None: ...
4141
@property
4242
def tzoffset(self) -> int: ...
4343

44+
@property
45+
def tzname(self) -> str: ...
46+
4447
@property
4548
def path(self) -> str | None: ...
4649

@@ -346,6 +349,7 @@ def __getattr__(self, name):
346349
"fingerprint",
347350
"syndicated",
348351
"shared_moment_info",
352+
"tzname",
349353
]:
350354
return None
351355
elif name in [

Diff for: osxphotos/timezones.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def known_timezone_names() -> list[str]:
3838
class Timezone:
3939
"""Create Timezone object from either name (str) or offset from GMT (int)"""
4040

41-
def __init__(self, tz: Union[str, int]):
41+
def __init__(self, tz: Union[str, int, float]):
4242
with objc.autorelease_pool():
4343
self._from_offset = False
4444
if isinstance(tz, str):
@@ -48,8 +48,10 @@ def __init__(self, tz: Union[str, int]):
4848
) or Foundation.NSTimeZone.timeZoneWithName_(tz)
4949
if not self.timezone:
5050
raise ValueError(f"Invalid timezone: {tz}")
51-
elif isinstance(tz, int):
52-
self.timezone = Foundation.NSTimeZone.timeZoneForSecondsFromGMT_(tz)
51+
elif isinstance(tz, (int, float)):
52+
self.timezone = Foundation.NSTimeZone.timeZoneForSecondsFromGMT_(
53+
int(tz)
54+
)
5355
self._from_offset = True
5456
else:
5557
raise TypeError("Timezone must be a string or an int")
@@ -71,6 +73,12 @@ def offset_str(self) -> str:
7173
def abbreviation(self) -> str:
7274
return self.timezone.abbreviation()
7375

76+
def offset_for_date(self, dt: datetime.datetime) -> int:
77+
return self.timezone.secondsFromGMTForDate_(dt)
78+
79+
def offset_str_for_date(self, dt: datetime.datetime) -> str:
80+
return format_offset_time(self.offset_for_date(dt))
81+
7482
def tzinfo(self, dt: datetime.datetime) -> zoneinfo.ZoneInfo:
7583
"""Return zoneinfo.ZoneInfo object for the timezone at the given datetime"""
7684
if not self._from_offset:

Diff for: tests/config_timewarp_ventura.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ def is_dst() -> bool:
298298
f"{TEST_LIBRARY_TIMEWARP}/originals/7/7E9DF2EE-A5B0-4077-80EC-30565221A3B9.jpeg"
299299
),
300300
"",
301-
get_local_utc_offset_str("2023-08-21 09:18:13"),
301+
get_local_utc_offset_str(get_file_timestamp(
302+
f"{TEST_LIBRARY_TIMEWARP}/originals/7/7E9DF2EE-A5B0-4077-80EC-30565221A3B9.jpeg"
303+
)),
302304
"",
303305
),
304306
},

0 commit comments

Comments
 (0)