Skip to content

Commit

Permalink
Fix for #1661
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Jan 30, 2025
1 parent c4da6cf commit a0fac23
Show file tree
Hide file tree
Showing 5 changed files with 430 additions and 26 deletions.
117 changes: 96 additions & 21 deletions osxphotos/cli/import_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,14 @@
)
from osxphotos.photosalbum import PhotosAlbumPhotoScript, PhotosAlbumPhotoScriptByPath
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.phototz import PhotoTimeZoneUpdater
from osxphotos.sqlite_utils import sqlite_columns
from osxphotos.sqlitekvstore import SQLiteKVStore
from osxphotos.strpdatetime_parts import (
date_str_matches_date_time_codes,
fmt_has_date_time_codes,
)
from osxphotos.timezones import Timezone
from osxphotos.unicode import normalize_unicode
from osxphotos.utils import get_last_library_path, pluralize

Expand Down Expand Up @@ -732,22 +734,39 @@ def get_help(self, ctx):
"--sidecar-ignore-date",
"-i",
is_flag=True,
help="Do not use date in sidecar to set photo date/time. "
"Setting the timezone from sidecar files is not currently supported so when using --sidecar "
"or --sidecar-filename, the date/time found in the sidecar will be converted to the local timezone "
"and that value will be used to set the photo date/time. "
"If your photos have correct timezone information in the embedded metadata you can use "
"--sidecar-ignore-date to ignore the date/time in the sidecar and use the date/time from the "
"file (which will be read by Photos on import).",
help="Do not use date/time in sidecar to set photo date/time. "
"There may be situations where you want to apply the metadata from the sidecar but ignore the date/time in the sidecar. "
"One of these is when using --exportdb to restore from the export database. "
"The export database stores the named timezone of the photo and will set it "
"but the sidecar metadata standards do not support storing the named timezone. "
"Thus if using both --exportdb and --sidecar, you may want to use --sidecar-ignore-date "
"otherwise the date/time in the sidecar will overwrite the date/time (and timezone name) from the export database. "
"Additionally, Google Takeout does not preserve timezone information in the sidecar and converts all times "
"to UTC. If you are importing photos from Google Takeout and the photos contain the correct date/time and timezone offset, "
"you may want to use --sidecar-ignore-date to ignore the date/time in the sidecar and use the date/time from the photo metadata.",
)
@click.option(
"--timezone",
"-Z",
is_flag=True,
help="Set the named timezone of the imported photos in the Photos database. "
"Photos does not provide a way to set the timezone of a photo directly. "
"Upon import, osxphotos can read the timezone from the export database if using --exportdb "
"or infer the timezone from the photo latitude/longitude if using --exiftool or --sidecar. "
"The --timezone option will set the named timezone of the photo by directly writing the timezone name "
"to the Photos database. This has been well tested but because it uses an undocumented Photos API, "
"it may be possible to corrupt the Photos database. If you want to set the timezone of the imported photos, "
"use --timezone. ",
)
@click.option(
"--exportdb",
"-B",
metavar="EXPORTDB_PATH",
type=click.Path(exists=True),
help="Use an osxphotos export database (created by 'osxphotos export') "
"to set metadata (title, description, keywords, location, album). "
"See also --exportdir, --sidecar, --sidecar-filename, --exiftool.",
"to set metadata (title, description, keywords, location, album, date/time, timezone). "
"See also --exportdir, --sidecar, --sidecar-filename, --exiftool. "
"If using with --sidecar, it is recommended you also use --sidecar-ignore-date.",
)
@click.option(
"--exportdir",
Expand Down Expand Up @@ -982,6 +1001,7 @@ def import_main(
stop_on_error: int | None,
theme: str | None,
timestamp: bool,
timezone: bool,
title: str | None,
verbose_flag: bool,
walk: bool,
Expand Down Expand Up @@ -1071,6 +1091,7 @@ def import_cli(
stop_on_error: int | None = None,
theme: str | None = None,
timestamp: bool = False,
timezone: bool = False,
title: str | None = None,
verbose_flag: bool = False,
walk: bool = False,
Expand Down Expand Up @@ -1238,6 +1259,7 @@ def import_cli(
signature=signature,
stop_on_error=stop_on_error,
verbose=verbose,
timezone=timezone,
)

import_db.close()
Expand Down Expand Up @@ -1600,6 +1622,8 @@ def set_photo_metadata_from_exportdb(
merge_keywords: bool,
verbose: Callable[..., None],
dry_run: bool,
library: str,
timezone: bool,
):
"""Set photo's metadata by reading metadata from exportdb"""
photoinfo = None
Expand All @@ -1616,7 +1640,15 @@ def set_photo_metadata_from_exportdb(
f"Setting metadata and location from export database for [filename]{filepath.name}[/]"
)
set_photo_metadata_from_metadata(
photo, filepath, metadata, merge_keywords, False, verbose, dry_run
photo,
filepath,
metadata,
merge_keywords,
False,
verbose,
dry_run,
library,
timezone,
)
else:
verbose(
Expand All @@ -1631,12 +1663,21 @@ def set_photo_metadata_from_exiftool(
merge_keywords: bool,
verbose: Callable[..., None],
dry_run: bool,
library: str,
):
"""Set photo's metadata by reading metadata from file with exiftool"""
verbose(f"Setting metadata and location from EXIF for [filename]{filepath.name}[/]")
metadata = metadata_from_exiftool(filepath, exiftool_path)
set_photo_metadata_from_metadata(
photo, filepath, metadata, merge_keywords, True, verbose, dry_run
photo,
filepath,
metadata,
merge_keywords,
True,
verbose,
dry_run,
library,
False,
)


Expand All @@ -1648,6 +1689,8 @@ def set_photo_metadata_from_metadata(
ignore_date: bool,
verbose: Callable[..., None],
dry_run: bool,
library: str,
timezone: bool,
) -> MetaData:
"""Set metadata from a MetaData object"""
if any([metadata.title, metadata.description, metadata.keywords]):
Expand All @@ -1673,10 +1716,7 @@ def set_photo_metadata_from_metadata(
verbose(f"No location to set for [filename]{filepath.name}[/]")

if metadata.date is not None and not ignore_date:
verbose(
f"Set date for [filename]{filepath.name}[/]: [time]{metadata.date.isoformat()}[/]"
)
set_photo_date(photo, metadata, verbose, dry_run)
set_photo_date(photo, metadata, verbose, dry_run, library, timezone)

return metadata

Expand All @@ -1690,6 +1730,8 @@ def set_photo_metadata_from_sidecar(
merge_keywords: bool,
verbose: Callable[..., None],
dry_run: bool,
library: str,
timezone: bool,
):
"""Set photo's metadata by reading metadata from sidecar. If sidecar format is XMP, exiftool must be installed."""
verbose(
Expand All @@ -1701,7 +1743,15 @@ def set_photo_metadata_from_sidecar(
rich_echo_error(f"Error reading sidecar [filename]{sidecar.name}[/]: {e}")
return
set_photo_metadata_from_metadata(
photo, filepath, metadata, merge_keywords, sidecar_ignore_date, verbose, dry_run
photo,
filepath,
metadata,
merge_keywords,
sidecar_ignore_date,
verbose,
dry_run,
library,
timezone,
)


Expand All @@ -1710,12 +1760,28 @@ def set_photo_date(
metadata: MetaData,
verbose: Callable[..., None],
dry_run: bool,
) -> datetime.datetime:
"""Set photo date from metadata"""
print(f"Setting date for {photo=} to {metadata.tz_offset_sec=} {metadata.tzname=}")
library: str,
timezone: bool,
):
"""Set photo date from metadata.
Args:
photo: Photo object
metadata: MetaData object
verbose: Callable to print verbose output
dry_run: if True, do not actually set date
library: path to Photos library
timezone: if True, update timezone of photo
"""
if photo and not dry_run:
# ZZZ
verbose(
f"Set date for [filename]{photo.filename}[/] ([uuid]{photo.uuid}[/]): [time]{metadata.date.isoformat()}[/]"
)
photo.date = metadata.date
if timezone and (metadata.tz_offset_sec is not None or metadata.tzname):
tz = Timezone(metadata.tzname or metadata.tz_offset_sec)
tz_updater = PhotoTimeZoneUpdater(tz, verbose=verbose, library_path=library)
tz_updater.update_photo(photo)


def set_photo_title(
Expand Down Expand Up @@ -1967,6 +2033,8 @@ def apply_photo_metadata(
sidecar_ignore_date: bool,
title: str | None,
verbose: Callable[..., None],
library: str,
timezone: bool,
):
"""Set metdata for photo"""

Expand All @@ -1986,10 +2054,12 @@ def apply_photo_metadata(
merge_keywords,
verbose,
dry_run,
library,
timezone,
)
if exiftool:
set_photo_metadata_from_exiftool(
photo, filepath, exiftool_path, merge_keywords, verbose, dry_run
photo, filepath, exiftool_path, merge_keywords, verbose, dry_run, library
)

if sidecar_file:
Expand All @@ -2002,6 +2072,8 @@ def apply_photo_metadata(
merge_keywords,
verbose,
dry_run,
library,
timezone,
)

if title:
Expand Down Expand Up @@ -3043,6 +3115,7 @@ def import_files(
auto_live: bool,
stop_on_error: int | None,
signature: str | None,
timezone: bool,
) -> tuple[int, int, int]:
"""Import files into Photos library
Expand Down Expand Up @@ -3275,6 +3348,8 @@ def import_files(
sidecar_ignore_date=sidecar_ignore_date,
title=title,
verbose=verbose,
library=last_library,
timezone=timezone,
)

apply_photo_albums(
Expand Down
1 change: 0 additions & 1 deletion osxphotos/exif_datetime_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ def update_photos_from_exif(
dtinfo = self.get_date_time_offset_from_exif(
_photo.path, use_file_modify_date=use_file_modify_date
)
print(f"{dtinfo=}")
if dtinfo.used_file_modify_date:
self.verbose(
"EXIF date/time missing, using file modify date/time for "
Expand Down
1 change: 1 addition & 0 deletions osxphotos/metadata_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ def metadata_from_metadata_dict(metadata: dict) -> MetaData:
tz_offset_sec=tz_offset,
height=height,
width=width,
tzname=None, # the sidecar doesn't store the timezone name
)


Expand Down
8 changes: 5 additions & 3 deletions osxphotos/timezones.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def known_timezone_names() -> list[str]:
class Timezone:
"""Create Timezone object from either name (str) or offset from GMT (int)"""

def __init__(self, tz: Union[str, int]):
def __init__(self, tz: Union[str, int, float]):
with objc.autorelease_pool():
self._from_offset = False
if isinstance(tz, str):
Expand All @@ -48,8 +48,10 @@ def __init__(self, tz: Union[str, int]):
) or Foundation.NSTimeZone.timeZoneWithName_(tz)
if not self.timezone:
raise ValueError(f"Invalid timezone: {tz}")
elif isinstance(tz, int):
self.timezone = Foundation.NSTimeZone.timeZoneForSecondsFromGMT_(tz)
elif isinstance(tz, (int, float)):
self.timezone = Foundation.NSTimeZone.timeZoneForSecondsFromGMT_(
int(tz)
)
self._from_offset = True
else:
raise TypeError("Timezone must be a string or an int")
Expand Down
Loading

0 comments on commit a0fac23

Please sign in to comment.