Feature add locations 929 (#932)
* Initial implementation of add-locations command #929 * Updated help for add-locations * Fixed handling of shared albums in sync * Added test for add-locations
This commit is contained in:
@@ -44,6 +44,7 @@ if args.get("--debug", False):
|
||||
print("Debugging enabled", file=sys.stderr)
|
||||
|
||||
from .about import about
|
||||
from .add_locations import add_locations
|
||||
from .albums import albums
|
||||
from .cli import cli_main
|
||||
from .common import get_photos_db, load_uuid_from_file
|
||||
@@ -73,6 +74,7 @@ install_traceback()
|
||||
|
||||
__all__ = [
|
||||
"about",
|
||||
"add_locations",
|
||||
"albums",
|
||||
"cli_main",
|
||||
"debug_dump",
|
||||
|
||||
190
osxphotos/cli/add_locations.py
Normal file
190
osxphotos/cli/add_locations.py
Normal file
@@ -0,0 +1,190 @@
|
||||
"""Add missing location data to photos in Photos.app using nearest neighbor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
import click
|
||||
import photoscript
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import QUERY_OPTIONS, THEME_OPTION, query_options_from_kwargs
|
||||
from .param_types import TimeOffset
|
||||
from .rich_progress import rich_progress
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
|
||||
def get_location(
|
||||
photos: list[osxphotos.PhotoInfo], idx: int, window: datetime.timedelta
|
||||
) -> osxphotos.PhotoInfo | None:
|
||||
"""Find nearest neighbor with location data within window of time.
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo object
|
||||
idx: index of photo in list of photos
|
||||
window: window of time to search for nearest neighbor
|
||||
|
||||
Returns:
|
||||
nearest neighbor PhotoInfo object or None if no neighbor found
|
||||
"""
|
||||
idx_back = None
|
||||
idx_forward = None
|
||||
if idx > 0:
|
||||
# search backwards in time
|
||||
for i in range(idx - 1, -1, -1):
|
||||
if (
|
||||
photos[idx].date - photos[i].date <= window
|
||||
and None not in photos[i].location
|
||||
):
|
||||
idx_back = i
|
||||
break
|
||||
|
||||
if idx < len(photos) - 1:
|
||||
# search forwards in time
|
||||
for i in range(idx + 1, len(photos)):
|
||||
if (
|
||||
photos[i].date - photos[idx].date <= window
|
||||
and None not in photos[i].location
|
||||
):
|
||||
idx_forward = i
|
||||
break
|
||||
|
||||
if idx_back is not None and idx_forward is not None:
|
||||
# found location in both directions
|
||||
# use location closest in time
|
||||
if (
|
||||
photos[idx].date - photos[idx_back].date
|
||||
< photos[idx_forward].date - photos[idx].date
|
||||
):
|
||||
return photos[idx_back]
|
||||
else:
|
||||
return photos[idx_forward]
|
||||
elif idx_back is not None:
|
||||
return photos[idx_back]
|
||||
elif idx_forward is not None:
|
||||
return photos[idx_forward]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@click.command(name="add-locations")
|
||||
@click.option(
|
||||
"--window",
|
||||
"-w",
|
||||
type=TimeOffset(),
|
||||
default="1 hr",
|
||||
help="Window of time to search for nearest neighbor; "
|
||||
"searches +/- window of time. Default is 1 hour. "
|
||||
"Format is one of 'HH:MM:SS', 'D days', 'H hours' (or hr), 'M minutes' (or min), "
|
||||
"'S seconds' (or sec), 'S' (where S is seconds).",
|
||||
)
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
help="Don't actually add location, just print what would be done. "
|
||||
"Most useful with --verbose.",
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
||||
@click.option(
|
||||
"--timestamp", "-T", is_flag=True, help="Add time stamp to verbose output."
|
||||
)
|
||||
@QUERY_OPTIONS
|
||||
@THEME_OPTION
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def add_locations(ctx, cli_ob, window, dry_run, verbose_, timestamp, theme, **kwargs):
|
||||
"""Add missing location data to photos in Photos.app using nearest neighbor.
|
||||
|
||||
This command will search for photos that are missing location data and look
|
||||
for the nearest neighbor photo within a given window of time that contains
|
||||
location information. If a photo is found within the window of time, the
|
||||
location of the nearest neighbor will be used to update the location of the
|
||||
photo.
|
||||
|
||||
For example, if you took pictures with your iPhone and also with a camera that
|
||||
doesn't have location information, you can use this command to add location
|
||||
information to the photos taken with the camera from those taken with the
|
||||
iPhone.
|
||||
|
||||
If you have many photos with missing location information but no nearest neighbor
|
||||
within the window of time, you could add location information to some photos manually
|
||||
then run this command again to add location information to the remaining photos.
|
||||
|
||||
You can specify a subset of photos to update using the query options. For example,
|
||||
`--selected` to update only the selected photos, `--added-after 2020-01-01` to update
|
||||
only photos added after Jan 1, 2020, etc.
|
||||
|
||||
Example:
|
||||
|
||||
Add location data to all photos with missing location data within a ±2 hour window:
|
||||
|
||||
`osxphotos add-locations --window "2 hr" --verbose`
|
||||
|
||||
The add-locations command assumes that photos already have the correct date and time.
|
||||
If you have photos that are missing both location data and date/time information,
|
||||
you can use `osxphotos timewarp` to add date/time information to the photos and then
|
||||
use `osxphotos add-locations` to add location information.
|
||||
See `osxphotos help timewarp` for more information.
|
||||
"""
|
||||
color_theme = get_theme(theme)
|
||||
verbose = verbose_print(
|
||||
verbose_, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
|
||||
verbose("Searching for photos with missing location data...")
|
||||
|
||||
# load photos database
|
||||
photosdb = osxphotos.PhotosDB(verbose=verbose)
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
photos = photosdb.query(query_options)
|
||||
|
||||
# sort photos by date
|
||||
photos = sorted(photos, key=lambda p: p.date)
|
||||
|
||||
num_photos = len(photos)
|
||||
missing_location = 0
|
||||
found_location = 0
|
||||
verbose(f"Processing {len(photos)} photos, window = ±{window}...")
|
||||
with rich_progress(console=get_verbose_console(), mock=verbose_) as progress:
|
||||
task = progress.add_task(
|
||||
f"Processing [num]{num_photos}[/] {pluralize(len(photos), 'photo', 'photos')}, window = ±{window}",
|
||||
total=num_photos,
|
||||
)
|
||||
for idx, photo in enumerate(photos):
|
||||
if None in photo.location:
|
||||
missing_location += 1
|
||||
verbose(
|
||||
f"Processing [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
)
|
||||
if neighbor := get_location(photos, idx, window):
|
||||
verbose(
|
||||
f"Adding location {neighbor.location} to [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
f" from [filename]{neighbor.original_filename}[/] ([uuid]{neighbor.uuid}[/])"
|
||||
)
|
||||
found_location += 1
|
||||
if not dry_run:
|
||||
photoscript.Photo(photo.uuid).location = neighbor.location
|
||||
else:
|
||||
verbose(
|
||||
f"No location found for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
)
|
||||
progress.advance(task)
|
||||
rich_click_echo(
|
||||
f"Done. Processed: [num]{num_photos}[/] photos, "
|
||||
f"missing location: [num]{missing_location}[/], "
|
||||
f"found location: [num]{found_location}[/] "
|
||||
)
|
||||
@@ -11,6 +11,7 @@ from osxphotos._constants import PROFILE_SORT_KEYS
|
||||
from osxphotos._version import __version__
|
||||
|
||||
from .about import about
|
||||
from .add_locations import add_locations
|
||||
from .albums import albums
|
||||
from .common import DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN
|
||||
from .debug_dump import debug_dump
|
||||
@@ -105,6 +106,7 @@ def cli_main(ctx, db, json_, profile, profile_sort, **kwargs):
|
||||
# install CLI commands
|
||||
for command in [
|
||||
about,
|
||||
add_locations,
|
||||
albums,
|
||||
debug_dump,
|
||||
diff,
|
||||
|
||||
Reference in New Issue
Block a user