Skip to content

Commit

Permalink
Refactor utitools (#1700)
Browse files Browse the repository at this point in the history
* Add date to ExifInfo

* Fix test for timezone

* Added photos_datetime_local

* Converted albuminfo to new datetime methods

* Converted comments/likes to new datetime methods

* Converted comments/likes to new datetime methods

* Converted iPhoto code to new date methods

* Converted iPhoto code to new date methods

* Refactored date code in ShareInfo

* added .workingon

* Refactored date code in FingerprintQuery, added tests

* Fix test for linux

* Refactored PhotosDB date code, updated tests

* Added date_original

* Test updates

* test updates

* test updates

* More test fixes

* Test updates

* Test updates

* Removed dt_to_local

* Removed dt_to_local

* Initial reset implementation

* Refactored to use utitools

* Removed ubuntu tests

* Updated makelive version
  • Loading branch information
RhetTbull authored Oct 1, 2024
1 parent 601e870 commit 40aa37b
Show file tree
Hide file tree
Showing 107 changed files with 4,657 additions and 3,879 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ venv/
.python-version
cov.xml
pyrightconfig.json
.workingon
7 changes: 7 additions & 0 deletions API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -995,6 +995,13 @@ Returns the original filename of the photo when it was imported to Photos. **No

Returns the create date of the photo as a datetime.datetime object

#### `date_original`

Returns the original creation date of the photo as a datetime.datetime object.
If user changed the photo's date in Photos, this will return the original date Photos assigned
as the creation date at the time of import.
Photos 5+ only; on Photos < 5.0, this will return the same value as `date`.

#### `date_added`

Returns the date the photo was added to the Photos library as a timezone aware datetime.datetime object, or None if the data added cannot be determined
Expand Down
6 changes: 0 additions & 6 deletions osxphotos/_constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
""" Constants used by osxphotos """

from __future__ import annotations

import logging
Expand All @@ -14,10 +12,6 @@

OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"

# Time delta: add this to Photos times to get unix time
# Apple Epoch is Jan 1, 2001
TIME_DELTA = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()

# which Photos library database versions have been tested
# Photos 2.0 (10.12.6) == 2622
# Photos 3.0 (10.13.6) == 3301
Expand Down
48 changes: 6 additions & 42 deletions osxphotos/albuminfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
_PHOTOS_5_VERSION,
TIME_DELTA,
AlbumSortOrder,
)
from .datetime_utils import get_local_tz
from .photos_datetime import photos_datetime_local

__all__ = [
"sort_list_by_keys",
Expand Down Expand Up @@ -67,9 +66,6 @@ def __init__(self, db, uuid):
self._creation_date_timestamp = self._db._dbalbum_details[uuid]["creation_date"]
self._start_date_timestamp = self._db._dbalbum_details[uuid]["start_date"]
self._end_date_timestamp = self._db._dbalbum_details[uuid]["end_date"]
self._local_tz = get_local_tz(
datetime.fromtimestamp(self._creation_date_timestamp + TIME_DELTA)
)

@property
def uuid(self):
Expand All @@ -82,20 +78,9 @@ def creation_date(self):
try:
return self._creation_date
except AttributeError:
try:
self._creation_date = (
datetime.fromtimestamp(
self._creation_date_timestamp + TIME_DELTA
).astimezone(tz=self._local_tz)
if self._creation_date_timestamp
else datetime(1970, 1, 1, 0, 0, 0).astimezone(
tz=timezone(timedelta(0))
)
)
except ValueError:
self._creation_date = datetime(1970, 1, 1, 0, 0, 0).astimezone(
tz=timezone(timedelta(0))
)
self._creation_date = photos_datetime_local(
self._creation_date_timestamp, True
)
return self._creation_date

@property
Expand All @@ -105,16 +90,7 @@ def start_date(self):
try:
return self._start_date
except AttributeError:
try:
self._start_date = (
datetime.fromtimestamp(
self._start_date_timestamp + TIME_DELTA
).astimezone(tz=self._local_tz)
if self._start_date_timestamp
else None
)
except ValueError:
self._start_date = None
self._start_date = photos_datetime_local(self._start_date_timestamp, False)
return self._start_date

@property
Expand All @@ -125,16 +101,7 @@ def end_date(self):
try:
return self._end_date
except AttributeError:
try:
self._end_date = (
datetime.fromtimestamp(
self._end_date_timestamp + TIME_DELTA
).astimezone(tz=self._local_tz)
if self._end_date_timestamp
else None
)
except ValueError:
self._end_date = None
self._end_date = photos_datetime_local(self._end_date_timestamp, False)
return self._end_date

@property
Expand Down Expand Up @@ -324,9 +291,6 @@ def __init__(self, db, uuid):
self._start_date_timestamp = self._creation_date_timestamp
self._end_date_timestamp = self._creation_date_timestamp
self._title = import_session[2]
self._local_tz = get_local_tz(
datetime.fromtimestamp(self._creation_date_timestamp + TIME_DELTA)
)

@property
def title(self):
Expand Down
53 changes: 38 additions & 15 deletions osxphotos/cli/timewarp.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from osxphotos.exiftool import get_exiftool_path
from osxphotos.photodates import (
get_photo_date_added,
reset_photo_date_time_tz,
set_photo_date_added,
set_photo_date_from_filename,
update_photo_date_time,
Expand Down Expand Up @@ -80,7 +81,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 +131,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,22 +152,20 @@ 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.
If the parse pattern includes a time zone, the photo's time will be converted from
the specified time zone to the local time zone. osxphotos import does not
currently support setting the time zone of imported photos.
See also `osxphotos help timewarp` for more information on the timewarp
command which can be used to change the time zone of photos after import.
the specified time zone to the local time zone. The timewarp command does not currently
setting the timezone when parsing the filename.
""" # noqa: E501
Expand Down Expand Up @@ -244,6 +243,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 +411,7 @@ def timewarp(
plain,
timestamp,
force,
reset,
):
"""Adjust date/time/timezone of photos in Apple Photos.
Expand All @@ -429,6 +436,7 @@ def timewarp(
parse_date,
pull_exif,
push_exif,
reset,
time_delta,
time,
timezone,
Expand All @@ -437,7 +445,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 +461,11 @@ def timewarp(
if add_to_album and not compare_exif:
raise click.UsageError("--add-to-album must be used with --compare-exif.")

if reset and float(PhotosLibrary().version) < 8.0:
raise click.UsageError(
"--reset may only be used with Photos version 8.0 and later (macOS Ventura and later)"
)

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

if any([compare_exif, push_exif, pull_exif]):
Expand Down Expand Up @@ -491,6 +504,7 @@ def timewarp(
parse_date,
pull_exif,
push_exif,
reset,
time_delta,
time,
timezone,
Expand Down Expand Up @@ -550,6 +564,12 @@ def timewarp(
library_path=library,
)

reset_photo_date_time_ = partial(
reset_photo_date_time_tz,
library_path=library,
verbose=verbose,
)

if function:
update_photo_from_function_ = partial(
update_photo_from_function,
Expand All @@ -575,7 +595,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 Expand Up @@ -638,10 +658,11 @@ def timewarp(
)
sys.exit(0)

if timezone:
tz_updater = PhotoTimeZoneUpdater(
timezone, verbose=verbose, library_path=library
)
tz_updater = (
PhotoTimeZoneUpdater(timezone, verbose=verbose, library_path=library)
if timezone
else None
)

if any([push_exif, pull_exif, function]):
# ExifDateTimeUpdater used to get photo path for --function
Expand All @@ -660,6 +681,8 @@ def timewarp(
)
for photo in photos:
set_crash_data("photo", f"{photo.uuid} {photo.filename}")
if reset:
reset_photo_date_time_(photo)
if parse_date:
set_photo_date_from_filename_(photo, photo.filename, parse_date)
if pull_exif:
Expand Down
91 changes: 69 additions & 22 deletions osxphotos/exifinfo.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,77 @@
""" ExifInfo class to expose EXIF info from the library """

from __future__ import annotations

import datetime
from dataclasses import dataclass
from typing import Any

from osxphotos.photos_datetime import photos_datetime

__all__ = ["ExifInfo"]
__all__ = ["ExifInfo", "exifinfo_factory"]


@dataclass(frozen=True)
class ExifInfo:
"""EXIF info associated with a photo from the Photos library"""

flash_fired: bool
iso: int
metering_mode: int
sample_rate: int
track_format: int
white_balance: int
aperture: float
bit_rate: float
duration: float
exposure_bias: float
focal_length: float
fps: float
latitude: float
longitude: float
shutter_speed: float
camera_make: str
camera_model: str
codec: str
lens_model: str
"""Original EXIF info associated with a photo from the Photos library"""

flash_fired: bool | None = None
iso: int | None = None
metering_mode: int | None = None
sample_rate: int | None = None
track_format: int | None = None
white_balance: int | None = None
aperture: float | None = None
bit_rate: float | None = None
duration: float | None = None
exposure_bias: float | None = None
focal_length: float | None = None
fps: float | None = None
latitude: float | None = None
longitude: float | None = None
shutter_speed: float | None = None
camera_make: str | None = None
camera_model: str | None = None
codec: str | None = None
lens_model: str | None = None
date: datetime.datetime | None = None
tzoffset: int | None = None
tzname: str | None = None


def exifinfo_factory(data: dict[str, Any] | None) -> ExifInfo:
"""Create an ExifInfo object from a dictionary of EXIF data"""
if data is None:
return ExifInfo()

exif_info = ExifInfo(
iso=data["ZISO"],
flash_fired=True if data["ZFLASHFIRED"] == 1 else False,
metering_mode=data["ZMETERINGMODE"],
sample_rate=data["ZSAMPLERATE"],
track_format=data["ZTRACKFORMAT"],
white_balance=data["ZWHITEBALANCE"],
aperture=data["ZAPERTURE"],
bit_rate=data["ZBITRATE"],
duration=data["ZDURATION"],
exposure_bias=data["ZEXPOSUREBIAS"],
focal_length=data["ZFOCALLENGTH"],
fps=data["ZFPS"],
latitude=data["ZLATITUDE"],
longitude=data["ZLONGITUDE"],
shutter_speed=data["ZSHUTTERSPEED"],
camera_make=data["ZCAMERAMAKE"],
camera_model=data["ZCAMERAMODEL"],
codec=data["ZCODEC"],
lens_model=data["ZLENSMODEL"],
# ZDATECREATED, ZTIMEZONEOFFSET, ZTIMEZONENAME added in Ventura / Photos 8 so may not be present
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
Loading

0 comments on commit 40aa37b

Please sign in to comment.