Feature timewarp function (#678)
* Added constraints, fixed themes for timewarp * Updated progress to use rich_progress * Added --function to timewarp, #676
This commit is contained in:
46
examples/timewarp_function_example.py
Normal file
46
examples/timewarp_function_example.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""Example function for use with `osxphotos timewarp --function`
|
||||||
|
|
||||||
|
Call this as: `osxphotos timewarp --function timewarp_function_example.py::get_date_time_timezone`
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Callable, Optional, Tuple
|
||||||
|
|
||||||
|
from photoscript import Photo
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_time_timezone(
|
||||||
|
photo: Photo, path: Optional[str], tz_sec: int, tz_name: str, verbose: Callable
|
||||||
|
) -> Tuple[datetime, int]:
|
||||||
|
"""Example function for use with `osxphotos timewarp --function`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
photo: Photo object
|
||||||
|
path: path to photo, which may be None if photo is not on disk
|
||||||
|
tz_sec: timezone offset from UTC in seconds
|
||||||
|
tz_name: timezone name
|
||||||
|
verbose: function to print verbose messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple of (new date/time as datetime.datetime, and new timezone offset from UTC in seconds as int)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this example adds 3 hours to the date/time and subtracts 1 hour from the timezone
|
||||||
|
|
||||||
|
# the photo's date/time can be accessed as photo.date
|
||||||
|
# photo.date is a datetime.datetime object
|
||||||
|
# the date/time is naive (timezone unaware) as will be in local timezone
|
||||||
|
date = photo.date
|
||||||
|
|
||||||
|
# add 3 hours
|
||||||
|
date = date + timedelta(hours=3)
|
||||||
|
|
||||||
|
# subtract 1 hour from timezone
|
||||||
|
timezone = tz_sec - 3600
|
||||||
|
|
||||||
|
# verbose(msg) prints a message to the console if user used --verbose option
|
||||||
|
# otherwise it does nothing
|
||||||
|
# photo's filename can be access as photo.filename
|
||||||
|
verbose(f"Updating {photo.filename} date/time: {date} and timezone: {timezone}")
|
||||||
|
|
||||||
|
return date, timezone
|
||||||
@@ -5,7 +5,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from typing import Callable
|
from typing import Callable, Optional
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from photoscript import Photo, PhotosLibrary
|
from photoscript import Photo, PhotosLibrary
|
||||||
@@ -20,7 +20,7 @@ from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
|||||||
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
|
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
|
||||||
from osxphotos.timeutils import update_datetime
|
from osxphotos.timeutils import update_datetime
|
||||||
from osxphotos.timezones import Timezone
|
from osxphotos.timezones import Timezone
|
||||||
from osxphotos.utils import pluralize
|
from osxphotos.utils import noop, pluralize
|
||||||
|
|
||||||
from .click_rich_echo import (
|
from .click_rich_echo import (
|
||||||
rich_click_echo,
|
rich_click_echo,
|
||||||
@@ -34,7 +34,15 @@ from .color_themes import get_theme
|
|||||||
from .common import THEME_OPTION
|
from .common import THEME_OPTION
|
||||||
from .darkmode import is_dark_mode
|
from .darkmode import is_dark_mode
|
||||||
from .help import HELP_WIDTH, rich_text
|
from .help import HELP_WIDTH, rich_text
|
||||||
from .param_types import DateOffset, DateTimeISO8601, TimeOffset, TimeString, UTCOffset
|
from .param_types import (
|
||||||
|
DateOffset,
|
||||||
|
DateTimeISO8601,
|
||||||
|
FunctionCall,
|
||||||
|
TimeOffset,
|
||||||
|
TimeString,
|
||||||
|
UTCOffset,
|
||||||
|
)
|
||||||
|
from .rich_progress import rich_progress
|
||||||
from .verbose import get_verbose_console, verbose_print
|
from .verbose import get_verbose_console, verbose_print
|
||||||
|
|
||||||
# format for pretty printing date/times
|
# format for pretty printing date/times
|
||||||
@@ -103,6 +111,48 @@ def update_photo_time_for_new_timezone(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_photo_from_function(
|
||||||
|
library_path: str,
|
||||||
|
function: Callable,
|
||||||
|
verbose_print: Callable,
|
||||||
|
photo: Photo,
|
||||||
|
path: Optional[str],
|
||||||
|
):
|
||||||
|
"""Update photo from function call"""
|
||||||
|
photo_tz_sec, _, photo_tz_name = PhotoTimeZone(
|
||||||
|
library_path=library_path
|
||||||
|
).get_timezone(photo)
|
||||||
|
dt_new, tz_new = function(
|
||||||
|
photo=photo,
|
||||||
|
path=path,
|
||||||
|
tz_sec=photo_tz_sec,
|
||||||
|
tz_name=photo_tz_name,
|
||||||
|
verbose=verbose_print,
|
||||||
|
)
|
||||||
|
if dt_new != photo.date:
|
||||||
|
old_date = photo.date
|
||||||
|
photo.date = dt_new
|
||||||
|
verbose_print(
|
||||||
|
f"Updated date/time for photo [filename]{photo.filename}[/filename] "
|
||||||
|
f"([uuid]{photo.uuid}[/uuid]) from: [time]{old_date}[/time] to [time]{dt_new}[/time]"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
verbose_print(
|
||||||
|
f"Skipped date/time update for photo [filename]{photo.filename}[/filename] "
|
||||||
|
f"([uuid]{photo.uuid}[/uuid]): nothing to do"
|
||||||
|
)
|
||||||
|
if tz_new != photo_tz_sec:
|
||||||
|
tz_updater = PhotoTimeZoneUpdater(
|
||||||
|
timezone=Timezone(tz_new), verbose=verbose_print, library_path=library_path
|
||||||
|
)
|
||||||
|
tz_updater.update_photo(photo)
|
||||||
|
else:
|
||||||
|
verbose_print(
|
||||||
|
f"Skipped timezone update for photo [filename]{photo.filename}[/filename] "
|
||||||
|
f"([uuid]{photo.uuid}[/uuid]): nothing to do"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TimeWarpCommand(click.Command):
|
class TimeWarpCommand(click.Command):
|
||||||
"""Custom click.Command that overrides get_help() to show additional help info for export"""
|
"""Custom click.Command that overrides get_help() to show additional help info for export"""
|
||||||
|
|
||||||
@@ -267,9 +317,19 @@ For this to work, you'll need to install the third-party exiftool (https://exift
|
|||||||
"Requires the third-party exiftool utility be installed (see https://exiftool.org/). "
|
"Requires the third-party exiftool utility be installed (see https://exiftool.org/). "
|
||||||
"See also --push-exif.",
|
"See also --push-exif.",
|
||||||
)
|
)
|
||||||
# constraint=RequireAtLeast(1),
|
@click.option(
|
||||||
# @constraint(mutually_exclusive, ["date", "date_delta"])
|
"--function",
|
||||||
# @constraint(mutually_exclusive, ["time", "time_delta"])
|
"-F",
|
||||||
|
metavar="filename.py::function",
|
||||||
|
nargs=1,
|
||||||
|
type=FunctionCall(),
|
||||||
|
multiple=False,
|
||||||
|
help="Run python function to determine the date/time/timezone to apply to a photo. "
|
||||||
|
"Use this in format: --function filename.py::function where filename.py is a python "
|
||||||
|
"file you've created and function is the name of the function in the python file you want to call. The function will be "
|
||||||
|
"passed information about the photo being processed and is expected to return "
|
||||||
|
"a naive datetime.datetime object with time in local time and UTC timezone offset in seconds. "
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--match-time",
|
"--match-time",
|
||||||
"-m",
|
"-m",
|
||||||
@@ -326,9 +386,6 @@ For this to work, you'll need to install the third-party exiftool (https://exift
|
|||||||
help="Terminal width in characters.",
|
help="Terminal width in characters.",
|
||||||
hidden=True,
|
hidden=True,
|
||||||
)
|
)
|
||||||
# @constraint(mutually_exclusive, ["plain", "mono", "dark", "light"])
|
|
||||||
# @constraint(If("match_time", then=requires_one), ["timezone"])
|
|
||||||
# @constraint(If("add_to_album", then=requires_one), ["compare_exif"])
|
|
||||||
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
||||||
@THEME_OPTION
|
@THEME_OPTION
|
||||||
@click.option(
|
@click.option(
|
||||||
@@ -346,6 +403,7 @@ def timewarp(
|
|||||||
compare_exif,
|
compare_exif,
|
||||||
push_exif,
|
push_exif,
|
||||||
pull_exif,
|
pull_exif,
|
||||||
|
function,
|
||||||
match_time,
|
match_time,
|
||||||
use_file_time,
|
use_file_time,
|
||||||
add_to_album,
|
add_to_album,
|
||||||
@@ -366,6 +424,39 @@ def timewarp(
|
|||||||
See Timewarp Overview below for additional information.
|
See Timewarp Overview below for additional information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# check constraints
|
||||||
|
if not any(
|
||||||
|
[
|
||||||
|
date,
|
||||||
|
date_delta,
|
||||||
|
time,
|
||||||
|
time_delta,
|
||||||
|
timezone,
|
||||||
|
inspect,
|
||||||
|
compare_exif,
|
||||||
|
push_exif,
|
||||||
|
pull_exif,
|
||||||
|
function,
|
||||||
|
]
|
||||||
|
):
|
||||||
|
raise click.UsageError(
|
||||||
|
"At least one of --date, --date-delta, --time, --time-delta, "
|
||||||
|
"--timezone, --inspect, --compare-exif, --push-exif, --pull-exif, --function "
|
||||||
|
"must be specified."
|
||||||
|
)
|
||||||
|
|
||||||
|
if date and date_delta:
|
||||||
|
raise click.UsageError("--date and --date-delta are mutually exclusive.")
|
||||||
|
|
||||||
|
if time and time_delta:
|
||||||
|
raise click.UsageError("--time and --time-delta are mutually exclusive.")
|
||||||
|
|
||||||
|
if match_time and not timezone:
|
||||||
|
raise click.UsageError("--match-time must be used with --timezone.")
|
||||||
|
|
||||||
|
if add_to_album and not compare_exif:
|
||||||
|
raise click.UsageError("--add-to-album must be used with --compare-exif.")
|
||||||
|
|
||||||
color_theme = get_theme(theme)
|
color_theme = get_theme(theme)
|
||||||
verbose_ = verbose_print(
|
verbose_ = verbose_print(
|
||||||
verbose,
|
verbose,
|
||||||
@@ -376,15 +467,21 @@ def timewarp(
|
|||||||
file=output_file,
|
file=output_file,
|
||||||
)
|
)
|
||||||
# set console for rich_echo to be same as for verbose_
|
# set console for rich_echo to be same as for verbose_
|
||||||
|
# TODO: this is a hack, find a better way to do this
|
||||||
terminal_width = terminal_width or (1000 if output_file else None)
|
terminal_width = terminal_width or (1000 if output_file else None)
|
||||||
if output_file:
|
if output_file:
|
||||||
set_rich_console(Console(file=output_file, width=terminal_width))
|
set_rich_console(Console(file=output_file, width=terminal_width))
|
||||||
elif terminal_width:
|
elif terminal_width:
|
||||||
set_rich_console(
|
set_rich_console(
|
||||||
Console(file=sys.stdout, force_terminal=True, width=terminal_width)
|
Console(
|
||||||
|
file=sys.stdout,
|
||||||
|
theme=color_theme,
|
||||||
|
force_terminal=True,
|
||||||
|
width=terminal_width,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
set_rich_console(get_verbose_console())
|
set_rich_console(get_verbose_console(theme=color_theme))
|
||||||
set_rich_theme(color_theme)
|
set_rich_theme(color_theme)
|
||||||
|
|
||||||
if any([compare_exif, push_exif, pull_exif]):
|
if any([compare_exif, push_exif, pull_exif]):
|
||||||
@@ -426,6 +523,16 @@ def timewarp(
|
|||||||
verbose_print=verbose_,
|
verbose_print=verbose_,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if function:
|
||||||
|
update_photo_from_function_ = partial(
|
||||||
|
update_photo_from_function,
|
||||||
|
library_path=library,
|
||||||
|
function=function[0],
|
||||||
|
verbose_print=verbose_,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
update_photo_from_function_ = noop
|
||||||
|
|
||||||
if inspect:
|
if inspect:
|
||||||
tzinfo = PhotoTimeZone(library_path=library)
|
tzinfo = PhotoTimeZone(library_path=library)
|
||||||
if photos:
|
if photos:
|
||||||
@@ -456,10 +563,11 @@ def timewarp(
|
|||||||
)
|
)
|
||||||
for photo in photos:
|
for photo in photos:
|
||||||
diff_results = (
|
diff_results = (
|
||||||
photocomp.compare_exif_with_markup(photo)
|
photocomp.compare_exif_no_markup(photo)
|
||||||
if not plain
|
if plain
|
||||||
else photocomp.compare_exif_no_markup(photo)
|
else photocomp.compare_exif_with_markup(photo)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not plain:
|
if not plain:
|
||||||
filename = (
|
filename = (
|
||||||
f"[change]{photo.filename}[/change]"
|
f"[change]{photo.filename}[/change]"
|
||||||
@@ -497,7 +605,8 @@ def timewarp(
|
|||||||
timezone, verbose=verbose_, library_path=library
|
timezone, verbose=verbose_, library_path=library
|
||||||
)
|
)
|
||||||
|
|
||||||
if any([push_exif, pull_exif]):
|
if any([push_exif, pull_exif, function]):
|
||||||
|
# ExifDateTimeUpdater used to get photo path for --function
|
||||||
exif_updater = ExifDateTimeUpdater(
|
exif_updater = ExifDateTimeUpdater(
|
||||||
library_path=library,
|
library_path=library,
|
||||||
verbose=verbose_,
|
verbose=verbose_,
|
||||||
@@ -505,11 +614,13 @@ def timewarp(
|
|||||||
plain=plain,
|
plain=plain,
|
||||||
)
|
)
|
||||||
|
|
||||||
rich_echo(f"Processing {len(photos)} {pluralize(len(photos), 'photo', 'photos')}")
|
num_photos = len(photos)
|
||||||
# send progress bar output to /dev/null if verbose to hide the progress bar
|
with rich_progress(console=get_verbose_console(), mock=verbose) as progress:
|
||||||
fp = open(os.devnull, "w") if verbose else None
|
task = progress.add_task(
|
||||||
with click.progressbar(photos, file=fp) as bar:
|
f"Processing [num]{num_photos}[/] {pluralize(len(photos), 'photo', 'photos')}",
|
||||||
for p in bar:
|
total=num_photos,
|
||||||
|
)
|
||||||
|
for p in photos:
|
||||||
if pull_exif:
|
if pull_exif:
|
||||||
exif_updater.update_photos_from_exif(
|
exif_updater.update_photos_from_exif(
|
||||||
p, use_file_modify_date=use_file_time
|
p, use_file_modify_date=use_file_time
|
||||||
@@ -522,6 +633,10 @@ def timewarp(
|
|||||||
update_photo_time_for_new_timezone_(photo=p, new_timezone=timezone)
|
update_photo_time_for_new_timezone_(photo=p, new_timezone=timezone)
|
||||||
if timezone:
|
if timezone:
|
||||||
tz_updater.update_photo(p)
|
tz_updater.update_photo(p)
|
||||||
|
if function:
|
||||||
|
verbose_(f"Calling function [bold]{function[1]}")
|
||||||
|
photo_path = exif_updater.get_photo_path(p)
|
||||||
|
update_photo_from_function_(photo=p, path=photo_path)
|
||||||
if push_exif:
|
if push_exif:
|
||||||
# this should be the last step in the if chain to ensure all Photos data is updated
|
# this should be the last step in the if chain to ensure all Photos data is updated
|
||||||
# before exiftool is run
|
# before exiftool is run
|
||||||
@@ -533,8 +648,7 @@ def timewarp(
|
|||||||
if exif_error:
|
if exif_error:
|
||||||
rich_echo_error(f"[error]Error running exiftool: {exif_error}[/]")
|
rich_echo_error(f"[error]Error running exiftool: {exif_error}[/]")
|
||||||
|
|
||||||
if fp is not None:
|
progress.advance(task)
|
||||||
fp.close()
|
|
||||||
|
|
||||||
rich_echo("Done.")
|
rich_echo("Done.")
|
||||||
|
|
||||||
|
|||||||
@@ -43,15 +43,18 @@ def noop(*args, **kwargs):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_verbose_console() -> Console:
|
def get_verbose_console(theme: t.Optional[Theme] = None) -> Console:
|
||||||
"""Get console object
|
"""Get console object or create one if not already created
|
||||||
|
|
||||||
|
Args:
|
||||||
|
theme: optional rich.theme.Theme object to use for formatting
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Console object
|
Console object
|
||||||
"""
|
"""
|
||||||
global _console
|
global _console
|
||||||
if _console.console is None:
|
if _console.console is None:
|
||||||
_console.console = Console(force_terminal=True)
|
_console.console = Console(force_terminal=True, theme=theme)
|
||||||
return _console.console
|
return _console.console
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -247,6 +247,18 @@ class ExifDateTimeUpdater:
|
|||||||
exif, use_file_modify_date=use_file_modify_date
|
exif, use_file_modify_date=use_file_modify_date
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_photo_path(self, photo: Photo) -> Optional[str]:
|
||||||
|
"""Get the path to a photo
|
||||||
|
|
||||||
|
Args:
|
||||||
|
photo: photoscript.Photo object to act on
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: path to photo or None if not found
|
||||||
|
"""
|
||||||
|
_photo = self.db.get_photo(photo.uuid)
|
||||||
|
return _photo.path if _photo else None
|
||||||
|
|
||||||
|
|
||||||
def get_exif_date_time_offset(
|
def get_exif_date_time_offset(
|
||||||
exif: Dict, use_file_modify_date: bool = False
|
exif: Dict, use_file_modify_date: bool = False
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ class PhotoTimeZoneUpdater:
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
self.verbose(
|
self.verbose(
|
||||||
f"Updated timezone for photo [filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid]) "
|
f"Updated timezone for photo [filename]{photo.filename}[/filename] ([uuid]{photo.uuid}[/uuid]) "
|
||||||
+ f"from [tz]{[tz_name]}[/tz], offset=[tz]{tz_offset}[/tz] "
|
+ f"from [tz]{tz_name}[/tz], offset=[tz]{tz_offset}[/tz] "
|
||||||
+ f"to [tz]{self.tz_name}[/tz], offset=[tz]{self.tz_offset}[/tz]"
|
+ f"to [tz]{self.tz_name}[/tz], offset=[tz]{self.tz_offset}[/tz]"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
""" Test data for timewarp command on Catalina/Photos 5 """
|
""" Test data for timewarp command on Catalina/Photos 5 """
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import pathlib
|
||||||
|
|
||||||
from tests.parse_timewarp_output import CompareValues, InspectValues
|
from tests.parse_timewarp_output import CompareValues, InspectValues
|
||||||
|
|
||||||
TEST_LIBRARY_TIMEWARP = "tests/TestTimeWarp-10.15.7.photoslibrary"
|
TEST_LIBRARY_TIMEWARP = "tests/TestTimeWarp-10.15.7.photoslibrary"
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_timestamp(file: str) -> str:
|
||||||
|
"""Get timestamp of file"""
|
||||||
|
return datetime.datetime.fromtimestamp(pathlib.Path(file).stat().st_mtime).strftime(
|
||||||
|
"%Y-%m-%d %H:%M:%S"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
CATALINA_PHOTOS_5 = {
|
CATALINA_PHOTOS_5 = {
|
||||||
"filenames": {
|
"filenames": {
|
||||||
"pumpkins": "IMG_6522.jpeg",
|
"pumpkins": "IMG_6522.jpeg",
|
||||||
@@ -258,7 +267,9 @@ CATALINA_PHOTOS_5 = {
|
|||||||
"post": CompareValues(
|
"post": CompareValues(
|
||||||
"IMG_6506.jpeg",
|
"IMG_6506.jpeg",
|
||||||
"7E9DF2EE-A5B0-4077-80EC-30565221A3B9",
|
"7E9DF2EE-A5B0-4077-80EC-30565221A3B9",
|
||||||
"2021-10-08 16:11:09",
|
get_file_timestamp(
|
||||||
|
f"{TEST_LIBRARY_TIMEWARP}/originals/7/7E9DF2EE-A5B0-4077-80EC-30565221A3B9.jpeg"
|
||||||
|
),
|
||||||
"",
|
"",
|
||||||
"-0700",
|
"-0700",
|
||||||
"",
|
"",
|
||||||
@@ -302,7 +313,7 @@ CATALINA_PHOTOS_5 = {
|
|||||||
"parameters": [("-0200", "2021-10-04 23:00:00-0200")]
|
"parameters": [("-0200", "2021-10-04 23:00:00-0200")]
|
||||||
},
|
},
|
||||||
"video_push_exif": {
|
"video_push_exif": {
|
||||||
# IMG_6501.jpeg
|
# IMG_6551.mov
|
||||||
"pre": CompareValues(
|
"pre": CompareValues(
|
||||||
"IMG_6551.mov",
|
"IMG_6551.mov",
|
||||||
"16BEC0BE-4188-44F1-A8F1-7250E978AD12",
|
"16BEC0BE-4188-44F1-A8F1-7250E978AD12",
|
||||||
@@ -321,7 +332,7 @@ CATALINA_PHOTOS_5 = {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
"video_pull_exif": {
|
"video_pull_exif": {
|
||||||
# IMG_6501.jpeg
|
# IMG_6551.jpeg
|
||||||
"pre": CompareValues(
|
"pre": CompareValues(
|
||||||
"IMG_6551.mov",
|
"IMG_6551.mov",
|
||||||
"16BEC0BE-4188-44F1-A8F1-7250E978AD12",
|
"16BEC0BE-4188-44F1-A8F1-7250E978AD12",
|
||||||
@@ -339,4 +350,16 @@ CATALINA_PHOTOS_5 = {
|
|||||||
"-0200",
|
"-0200",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
"function": {
|
||||||
|
# IMG_6501.jpeg
|
||||||
|
"uuid": "2F00448D-3C0D-477A-9B10-5F21DCAB405A",
|
||||||
|
"expected": InspectValues(
|
||||||
|
"IMG_6501.jpeg",
|
||||||
|
"2F00448D-3C0D-477A-9B10-5F21DCAB405A",
|
||||||
|
"2020-09-01 18:53:02-0700",
|
||||||
|
"2020-09-01 18:53:02-0700",
|
||||||
|
"-0700",
|
||||||
|
"GMT-0700",
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ def ask_user_to_make_selection(
|
|||||||
video: set to True if asking for a video instead of a photo
|
video: set to True if asking for a video instead of a photo
|
||||||
"""
|
"""
|
||||||
# needs to be called with a suspend_capture fixture
|
# needs to be called with a suspend_capture fixture
|
||||||
photo_or_video = "photo" if not video else "video"
|
photo_or_video = "video" if video else "photo"
|
||||||
tries = 0
|
tries = 0
|
||||||
while tries < retry:
|
while tries < retry:
|
||||||
with suspend_capture:
|
with suspend_capture:
|
||||||
@@ -935,3 +935,35 @@ def test_video_pull_exif(photoslib, suspend_capture, output_file):
|
|||||||
)
|
)
|
||||||
output_values = parse_compare_exif(output_file)
|
output_values = parse_compare_exif(output_file)
|
||||||
assert output_values[0] == post_test
|
assert output_values[0] == post_test
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timewarp
|
||||||
|
def test_select_pears_3(photoslib, suspend_capture):
|
||||||
|
"""Force user to select the right photo for following tests"""
|
||||||
|
assert ask_user_to_make_selection(photoslib, suspend_capture, "pears")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timewarp
|
||||||
|
def test_function(photoslib, suspend_capture, output_file):
|
||||||
|
"""Test timewarp function"""
|
||||||
|
from osxphotos.cli.timewarp import timewarp
|
||||||
|
|
||||||
|
expected = TEST_DATA["function"]["expected"]
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(
|
||||||
|
timewarp,
|
||||||
|
[
|
||||||
|
"--function",
|
||||||
|
"tests/timewarp_function_example.py::get_date_time_timezone",
|
||||||
|
],
|
||||||
|
terminal_width=TERMINAL_WIDTH,
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
result = runner.invoke(
|
||||||
|
timewarp,
|
||||||
|
["--inspect", "--plain", "-o", output_file],
|
||||||
|
terminal_width=TERMINAL_WIDTH,
|
||||||
|
)
|
||||||
|
output_values = parse_inspect_output(output_file)
|
||||||
|
assert output_values[0] == expected
|
||||||
|
|||||||
46
tests/timewarp_function_example.py
Normal file
46
tests/timewarp_function_example.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
"""Example function for use with `osxphotos timewarp --function`
|
||||||
|
|
||||||
|
Call this as: `osxphotos timewarp --function timewarp_function_example.py::get_date_time_timezone`
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Callable, Optional, Tuple
|
||||||
|
|
||||||
|
from photoscript import Photo
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_time_timezone(
|
||||||
|
photo: Photo, path: Optional[str], tz_sec: int, tz_name: str, verbose: Callable
|
||||||
|
) -> Tuple[datetime, int]:
|
||||||
|
"""Example function for use with `osxphotos timewarp --function`
|
||||||
|
|
||||||
|
Args:
|
||||||
|
photo: Photo object
|
||||||
|
path: path to photo, which may be None if photo is not on disk
|
||||||
|
tz_sec: timezone offset from UTC in seconds
|
||||||
|
tz_name: timezone name
|
||||||
|
verbose: function to print verbose messages
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple of (new date/time as datetime.datetime, and new timezone offset from UTC in seconds as int)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this example adds 3 hours to the date/time and subtracts 1 hour from the timezone
|
||||||
|
|
||||||
|
# the photo's date/time can be accessed as photo.date
|
||||||
|
# photo.date is a datetime.datetime object
|
||||||
|
# the date/time is naive (timezone unaware) as will be in local timezone
|
||||||
|
date = photo.date
|
||||||
|
|
||||||
|
# add 3 hours
|
||||||
|
date = date + timedelta(hours=3)
|
||||||
|
|
||||||
|
# subtract 1 hour from timezone
|
||||||
|
timezone = tz_sec - 3600
|
||||||
|
|
||||||
|
# verbose(msg) prints a message to the console if user used --verbose option
|
||||||
|
# otherwise it does nothing
|
||||||
|
# photo's filename can be access as photo.filename
|
||||||
|
verbose(f"Updating {photo.filename} date/time: {date} and timezone: {timezone}")
|
||||||
|
|
||||||
|
return date, timezone
|
||||||
Reference in New Issue
Block a user