From 51f69585be60d12f912ba08f138b9c1f74481dbd Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sat, 23 Jan 2021 22:25:49 -0800 Subject: [PATCH] Refactored __main__, added sphinx docs --- cli.py | 2 +- docs/Makefile | 20 + docs/README.md | 13 + docs/cli.rst | 6 + docs/conf.py | 55 + docs/index.rst | 23 + docs/make.bat | 35 + docs/modules.rst | 5 + docs/osxphotos.pdf | Bin 0 -> 194989 bytes docs/reference.rst | 13 + osxphotos/__main__.py | 3800 +---------------- osxphotos/_constants.py | 3 + osxphotos/_version.py | 2 +- osxphotos/cli.py | 3522 +++++++++++++++ osxphotos/cli_help.py | 286 ++ tests/search_info_test_data_10_15_7.json | 2 +- tests/sidecars/15uNd7%8RguTEgNPKHfTWw.xmp | 2 +- ...Nd7%8RguTEgNPKHfTWw_albums_as_keywords.xmp | 2 +- tests/sidecars/15uNd7%8RguTEgNPKHfTWw_ext.xmp | 2 +- ...5uNd7%8RguTEgNPKHfTWw_keyword_template.xmp | 2 +- ...d7%8RguTEgNPKHfTWw_persons_as_keywords.xmp | 2 +- tests/sidecars/3Jn73XpSQQCluzRBMWRsMA.xmp | 2 +- ...73XpSQQCluzRBMWRsMA_albums_as_keywords.xmp | 2 +- tests/sidecars/3Jn73XpSQQCluzRBMWRsMA_ext.xmp | 2 +- ...Jn73XpSQQCluzRBMWRsMA_keyword_template.xmp | 2 +- ...3XpSQQCluzRBMWRsMA_persons_as_keywords.xmp | 2 +- .../6191423D-8DB8-4D4C-92BE-9BBBA308AAC4.xmp | 2 +- ...C-92BE-9BBBA308AAC4_albums_as_keywords.xmp | 2 +- ...91423D-8DB8-4D4C-92BE-9BBBA308AAC4_ext.xmp | 2 +- ...D4C-92BE-9BBBA308AAC4_keyword_template.xmp | 2 +- ...-92BE-9BBBA308AAC4_persons_as_keywords.xmp | 2 +- tests/sidecars/6bxcNnzRQKGnK4uPrCJ9UQ.xmp | 2 +- ...cNnzRQKGnK4uPrCJ9UQ_albums_as_keywords.xmp | 2 +- tests/sidecars/6bxcNnzRQKGnK4uPrCJ9UQ_ext.xmp | 2 +- ...bxcNnzRQKGnK4uPrCJ9UQ_keyword_template.xmp | 2 +- ...NnzRQKGnK4uPrCJ9UQ_persons_as_keywords.xmp | 2 +- tests/sidecars/8SOE9s0XQVGsuq4ONohTng.xmp | 2 +- ...E9s0XQVGsuq4ONohTng_albums_as_keywords.xmp | 2 +- tests/sidecars/8SOE9s0XQVGsuq4ONohTng_ext.xmp | 2 +- ...SOE9s0XQVGsuq4ONohTng_keyword_template.xmp | 2 +- ...9s0XQVGsuq4ONohTng_persons_as_keywords.xmp | 2 +- .../A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C.xmp | 2 +- ...F-9AC9-5AFEFE2D3A5C_albums_as_keywords.xmp | 2 +- ...DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C_ext.xmp | 2 +- ...31F-9AC9-5AFEFE2D3A5C_keyword_template.xmp | 2 +- ...-9AC9-5AFEFE2D3A5C_persons_as_keywords.xmp | 2 +- .../D79B8D77-BFFC-460B-9312-034F2877D35B.xmp | 2 +- ...B-9312-034F2877D35B_albums_as_keywords.xmp | 2 +- ...9B8D77-BFFC-460B-9312-034F2877D35B_ext.xmp | 2 +- ...60B-9312-034F2877D35B_keyword_template.xmp | 2 +- ...-9312-034F2877D35B_persons_as_keywords.xmp | 2 +- .../DC99FBDD-7A52-4100-A5BB-344131646C30.xmp | 2 +- ...0-A5BB-344131646C30_albums_as_keywords.xmp | 2 +- ...99FBDD-7A52-4100-A5BB-344131646C30_ext.xmp | 2 +- ...100-A5BB-344131646C30_keyword_template.xmp | 2 +- ...-A5BB-344131646C30_persons_as_keywords.xmp | 2 +- .../E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51.xmp | 2 +- ...1-A72B-8B8FAC227D51_albums_as_keywords.xmp | 2 +- ...BC5C36-7CD1-40A1-A72B-8B8FAC227D51_ext.xmp | 2 +- ...0A1-A72B-8B8FAC227D51_keyword_template.xmp | 2 +- ...-A72B-8B8FAC227D51_persons_as_keywords.xmp | 2 +- .../F12384F6-CD17-4151-ACBA-AE0E3688539E.xmp | 2 +- ...1-ACBA-AE0E3688539E_albums_as_keywords.xmp | 2 +- ...2384F6-CD17-4151-ACBA-AE0E3688539E_ext.xmp | 2 +- ...151-ACBA-AE0E3688539E_keyword_template.xmp | 2 +- ...-ACBA-AE0E3688539E_persons_as_keywords.xmp | 2 +- tests/test_cli.py | 292 +- tests/test_cli_utils.py | 6 +- utils/update_readme.py | 2 +- 69 files changed, 4186 insertions(+), 4001 deletions(-) create mode 100644 docs/Makefile create mode 100644 docs/README.md create mode 100644 docs/cli.rst create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat create mode 100644 docs/modules.rst create mode 100644 docs/osxphotos.pdf create mode 100644 docs/reference.rst create mode 100644 osxphotos/cli.py create mode 100644 osxphotos/cli_help.py diff --git a/cli.py b/cli.py index f3aacdb4..d9bf43c3 100644 --- a/cli.py +++ b/cli.py @@ -12,7 +12,7 @@ """ -from osxphotos.__main__ import cli +from osxphotos.cli import cli if __name__ == "__main__": cli() diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..3562a200 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,13 @@ +# Building the documentation + +I'm still trying to learn sphinx and come up with a workflow for building docs. Right now it's pretty kludgy. + +- `pip install sphinx` +- `pip install sphinx-rtd-theme` +- `pip install m2r2` +- Download and install [MacTeX](https://tug.org/mactex/) +- Add `/Library/TeX/texbin` to your `$PATH` +- `cd docs` +- `make html` +- `make latexpdf` +- `cp _build/latex/osxphotos.pdf .` diff --git a/docs/cli.rst b/docs/cli.rst new file mode 100644 index 00000000..b63dfe3a --- /dev/null +++ b/docs/cli.rst @@ -0,0 +1,6 @@ +osxphotos command line interface (CLI) +====================================== + +.. click:: osxphotos.cli:cli + :prog: osxphotos + :nested: full \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..8087458f --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,55 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +import sphinx_rtd_theme + +sys.path.insert(0, os.path.abspath("..")) + + +# -- Project information ----------------------------------------------------- + +project = "osxphotos" +copyright = "2021, Rhet Turnbull" +author = "Rhet Turnbull" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["sphinx_click", "sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", "m2r2"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "alabaster" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..c05f4535 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,23 @@ +.. osxphotos documentation master file, created by + sphinx-quickstart on Sat Jan 23 13:27:27 2021. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to osxphotos's documentation! +===================================== + +.. include:: ../README.rst + +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + cli + reference + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..2119f510 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst new file mode 100644 index 00000000..3867ec04 --- /dev/null +++ b/docs/modules.rst @@ -0,0 +1,5 @@ +osxphotos +=========== + +.. toctree:: + :maxdepth: 4 diff --git a/docs/osxphotos.pdf b/docs/osxphotos.pdf new file mode 100644 index 0000000000000000000000000000000000000000..bc44c965ad8a289f71fe8e0324009bf7f095b7fb GIT binary patch literal 194989 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 literal 0 HcmV?d00001 diff --git a/docs/reference.rst b/docs/reference.rst new file mode 100644 index 00000000..0aa546ff --- /dev/null +++ b/docs/reference.rst @@ -0,0 +1,13 @@ +osxphotos package +=================== + +osxphotos module +------------------------------ + +.. autoclass:: osxphotos.PhotosDB + :members: + :undoc-members: + +.. autoclass:: osxphotos.PhotoInfo + :members: + :undoc-members: diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 0b2f187d..2738753f 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -1,3802 +1,6 @@ -""" command line interface for osxphotos """ -import csv -import datetime -import json -import os -import os.path -import pathlib -import pprint -import sys -import time -import unicodedata - -import click -import osxmetadata -import yaml - -import osxphotos - -from ._constants import ( - _EXIF_TOOL_URL, - _OSXPHOTOS_NONE_SENTINEL, - _PHOTOS_4_VERSION, - _UNKNOWN_PLACE, - CLI_COLOR_ERROR, - CLI_COLOR_WARNING, - DEFAULT_EDITED_SUFFIX, - DEFAULT_JPEG_QUALITY, - DEFAULT_ORIGINAL_SUFFIX, - EXTENDED_ATTRIBUTE_NAMES, - EXTENDED_ATTRIBUTE_NAMES_QUOTED, - OSXPHOTOS_URL, - SIDECAR_EXIFTOOL, - SIDECAR_JSON, - SIDECAR_XMP, - UNICODE_FORMAT, -) -from ._version import __version__ -from .configoptions import ( - ConfigOptions, - ConfigOptionsInvalidError, - ConfigOptionsLoadError, -) -from .datetime_formatter import DateTimeFormatter -from .exiftool import get_exiftool_path -from .export_db import ExportDB, ExportDBInMemory -from .fileutil import FileUtil, FileUtilNoOp -from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath -from .photoinfo import ExportResults -from .photokit import check_photokit_authorization, request_photokit_authorization -from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED -from .utils import get_preferred_uti_extension - -# global variable to control verbose output -# set via --verbose/-V -VERBOSE = False - -# name of export DB -OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db" - - -def verbose_(*args, **kwargs): - """ print output if verbose flag set """ - if VERBOSE: - styled_args = [] - for arg in args: - if type(arg) == str: - if "error" in arg.lower(): - arg = click.style(arg, fg=CLI_COLOR_ERROR) - elif "warning" in arg.lower(): - arg = click.style(arg, fg=CLI_COLOR_WARNING) - styled_args.append(arg) - click.echo(*styled_args, **kwargs) - - -def normalize_unicode(value): - """ normalize unicode data """ - if value is not None: - if isinstance(value, tuple): - return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value) - elif isinstance(value, str): - return unicodedata.normalize(UNICODE_FORMAT, value) - else: - return value - else: - return None - - -def get_photos_db(*db_options): - """Return path to photos db, select first non-None db_options - If no db_options are non-None, try to find library to use in - the following order: - - last library opened - - system library - - ~/Pictures/Photos Library.photoslibrary - - failing above, returns None - """ - if db_options: - for db in db_options: - if db is not None: - return db - - # if get here, no valid database paths passed, so try to figure out which to use - db = osxphotos.utils.get_last_library_path() - if db is not None: - click.echo(f"Using last opened Photos library: {db}", err=True) - return db - - db = osxphotos.utils.get_system_library_path() - if db is not None: - click.echo(f"Using system Photos library: {db}", err=True) - return db - - db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary") - if os.path.isdir(db): - click.echo(f"Using Photos library: {db}", err=True) - return db - else: - return None - - -class DateTimeISO8601(click.ParamType): - - name = "DATETIME" - - def convert(self, value, param, ctx): - try: - return datetime.datetime.fromisoformat(value) - except Exception: - self.fail( - f"Invalid value for --{param.name}: invalid datetime format {value}. " - "Valid format: YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]" - ) - - -# Click CLI object & context settings -class CLI_Obj: - def __init__(self, db=None, json=False, debug=False): - if debug: - osxphotos._set_debug(True) - self.db = db - self.json = json - - -class ExportCommand(click.Command): - """ Custom click.Command that overrides get_help() to show additional help info for export """ - - def get_help(self, ctx): - help_text = super().get_help(ctx) - formatter = click.HelpFormatter() - - # passed to click.HelpFormatter.write_dl for formatting - - formatter.write("\n\n") - formatter.write_text("** Export **") - formatter.write_text( - "When exporting photos, osxphotos creates a database in the top-level " - + f"export folder called '{OSXPHOTOS_EXPORT_DB}'. This database preserves state information " - + "used for determining which files need to be updated when run with --update. It is recommended " - + "that if you later move the export folder tree you also move the database file." - ) - formatter.write("\n") - formatter.write_text( - "The --update option will only copy new or updated files from the library " - + "to the export folder. If a file is changed in the export folder (for example, you edited the " - + "exported image), osxphotos will detect this as a difference and re-export the original image " - + "from the library thus overwriting the changes. If using --update, the exported library " - + "should be treated as a backup, not a working copy where you intend to make changes. " - + "If you do edit or process the exported files and do not want them to be overwritten with" - + "subsequent --update, use --ignore-signature which will match filename but not file signature when " - + "exporting." - ) - formatter.write("\n") - formatter.write_text( - "Note: The number of files reported for export and the number actually exported " - + "may differ due to live photos, associated raw images, and edited photos which are reported " - + "in the total photos exported." - ) - formatter.write("\n") - formatter.write_text( - "Implementation note: To determine which files need to be updated, " - + f"osxphotos stores file signature information in the '{OSXPHOTOS_EXPORT_DB}' database. " - + "The signature includes size, modification time, and filename. In order to minimize " - + "run time, --update does not do a full comparison (diff) of the files nor does it compare " - + "hashes of the files. In normal usage, this is sufficient for updating the library. " - + "You can always run export without the --update option to re-export the entire library thus " - + f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database." - ) - formatter.write("\n\n") - formatter.write_text("** Extended Attributes **") - formatter.write("\n") - formatter.write_text( - """ -Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write -additional metadata to extended attributes in the file. These options will only work -if the destination filesystem supports extended attributes (most do). -For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template' -or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'. -For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly -find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar. -Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute. -Unlike EXIF metadata, extended attributes do not modify the actual file. Most cloud storage services -do not synch extended attributes. Dropbox does sync them and any changes to a file's extended attributes -will cause Dropbox to re-sync the files. - -The following attributes may be used with '--xattr-template': - - """ - ) - formatter.write_dl( - [ - ( - attr, - f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})", - ) - for attr in EXTENDED_ATTRIBUTE_NAMES - ] - ) - formatter.write("\n") - formatter.write_text( - "For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys" - ) - formatter.write("\n\n") - formatter.write_text("** Templating System **") - formatter.write("\n") - formatter.write_text( - """ -Several options, such as --directory, allow you to specify a template which -will be rendered to substitute template fields with values from the photo. -For example, '{created.month}' would be replaced with the month name of the -photo creation date. e.g. 'November'. - -Some options supporting templates may be repeated e.g., --keyword-template -'{label}' --keyword-template '{media_type}' to add both labels and media -types to the keywords. - -The general format for a template is '{TEMPLATE_FIELD,DEFAULT}'. The full template format is: -'{DELIM+TEMPLATE_FIELD(PATH_SEP)[OLD,NEW]?VALUE_IF_TRUE,DEFAULT}' - -With a few exceptions (like '{created.strftime}') everything but the TEMPLATE_FIELD -is optional. - -- 'DELIM+' Multi-value template fields such as '{keyword}' may be expanded 'in place' -with an optional delimiter using the template form '{DELIM+TEMPLATE_FIELD}'. -For example, a photo with keywords 'foo' and 'bar': - -'{keyword}' renders to 'foo' and 'bar' - -'{,+keyword}' renders to: 'foo,bar' - -'{; +keyword}' renders to: 'foo; bar' - -'{+keyword}' renders to 'foobar' - -- 'TEMPLATE_FIELD' The name of the template field, for example 'keyword' - -- '(PATH_SEP)' Some template fields such as '{folder_album}' are "path-like" in -that they join multiple elements into a single path-like string. For example, -if photo is in album Album1 in folder Folder1, '{folder_album}' results in -'Folder1/Album1'. This is so these template fields may be used as paths in ---directory. If you intend to use such a field as a string, e.g. in the -filename, you may specify a different path separator using the form: -'{TEMPLATE_FIELD(PATH_SEP)}'. For example, using the example above, -'{folder_album(-)}' would result in 'Folder1-Album1' and '{folder_album()}' -would result in 'Folder1Album1'. - -- '[OLD,NEW]' Use the [OLD,NEW] option to replace text "OLD" in the template value -with text "NEW". For example, if you have album names with '/' in the album name you -could replace '/' with "-" using the template '{album[/,-]}'. This would replace -any occurence of "/" in the album name with "-"; album "Vacation/2019" would thus -become "Vacation-2019". You may specify more than one pair of OLD,NEW values by -listing them delimited by '|'. For example: '{album[/,-|:,-]}' to replace both -'/' and ':' by '-'. You can also use the [OLD,NEW] syntax to delete a character by -omitting the NEW value as in '{album[/,]}'. - -- '?' Some template fields such as 'hdr' are boolean and resolve to True or False. -These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g. -{hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR image -and 'not_hdr' otherwise. - -- ',DEFAULT' The ',' and DEFAULT value are optional. If TEMPLATE_FIELD results -in a null (empty) value, the template will result in default value of '_'. -You may specify an alternate default value by appending ',DEFAULT' after -template_field. Example: '{title,no_title}' would result in 'no_title' if the photo -had no title. Example: '{created.year}/{place.address,NO_ADDRESS}' but there was -no address associated with the photo, the resulting output would be: -'2020/NO_ADDRESS/photoname.jpg'. If specified, the default value may not -contain a brace symbol ('{' or '}'). - -Again, if you do not specify a default value and the template substitution has no -value, '_' (underscore) will be used as the default value. For example, in the -above example, this would result in '2020/_/photoname.jpg' if address was -null. - -You may specify a null default (e.g. "" or empty string) by omitting the value -after the comma, e.g. {title,} which would render to "" if title had no value thus -effectively deleting the template from the resulting string. - -You may include other text in the template string outside the {} -and use more than one template field in a single string, -e.g. '{created.year} - {created.month}' (e.g. '2020 - November'). - -Some templates may resolve to more than one value. For example, a photo can -have multiple keywords so '{keyword}' can result in multiple values. If used -in a filename or directory, these templates may result in more than one copy -of the photo being exported. For example, if photo has keywords "foo" and -"bar", --directory '{keyword}' will result in copies of the photo being -exported to 'foo/image_name.jpeg' and 'bar/image_name.jpeg'. - -Some template fields such as '{media_type}' use the 'DEFAULT' value to allow -customization of the output. For example, '{media_type}' resolves to the -special media type of the photo such as 'panorama' or 'selfie'. You may use -the 'DEFAULT' value to override these in form: -'{media_type,video=vidéo;time_lapse=vidéo_accélérée}'. In this example, if -photo is a time_lapse photo, 'media_type' would resolve to 'vidéo_accélérée' -instead of 'time_lapse' and video would resolve to 'vidéo' if photo is an -ordinary video. - -With the --directory and --filename options you may specify a template for the -export directory or filename, respectively. The directory will be appended to -the export path specified in the export DEST argument to export. For example, -if template is '{created.year}/{created.month}', and export destination DEST -is '/Users/maria/Pictures/export', the actual export directory for a photo -would be '/Users/maria/Pictures/export/2020/March' if the photo was created in -March 2020. - -The templating system may also be used with the --keyword-template option to -set keywords on export (with --exiftool or --sidecar), for example, to set a -new keyword in format 'folder/subfolder/album' to preserve the folder/album -structure, you can use --keyword-template "{folder_album}" - -In the template, valid template substitutions will be replaced by the -corresponding value from the table below. Invalid substitutions will result -in an error. - -If you want the actual text of the template substition to appear in the -rendered name, use double braces, e.g. '{{' or '}}', thus using -'{created.year}/{{name}}' for --directory would result in output of -2020/{name}/photoname.jpg -""" - ) - formatter.write("\n") - formatter.write_text( - "With the --directory and --filename options you may specify a template for the " - + "export directory or filename, respectively. " - + "The directory will be appended to the export path specified " - + "in the export DEST argument to export. For example, if template is " - + "'{created.year}/{created.month}', and export destination DEST is " - + "'/Users/maria/Pictures/export', " - + "the actual export directory for a photo would be '/Users/maria/Pictures/export/2020/March' " - + "if the photo was created in March 2020. " - ) - formatter.write("\n") - formatter.write_text( - "The templating system may also be used with the --keyword-template option " - + "to set keywords on export (with --exiftool or --sidecar), " - + "for example, to set a new keyword in format 'folder/subfolder/album' to " - + 'preserve the folder/album structure, you can use --keyword-template "{folder_album}"' - ) - formatter.write("\n") - formatter.write_text( - "In the template, valid template substitutions will be replaced by " - + "the corresponding value from the table below. Invalid substitutions will result in a " - + "an error and the script will abort." - ) - formatter.write("\n") - formatter.write_text( - "If you want the actual text of the template substition to appear " - + "in the rendered name, use double braces, e.g. '{{' or '}}', thus " - + "using '{created.year}/{{name}}' for --directory " - + "would result in output of 2020/{name}/photoname.jpg" - ) - formatter.write("\n") - formatter.write_text( - "You may specify an optional default value to use if the substitution does not contain a value " - + "(e.g. the value is null) " - + "by specifying the default value after a ',' in the template string: " - + "for example, if template is '{created.year}/{place.address,NO_ADDRESS}' " - + "but there was no address associated with the photo, the resulting output would be: " - + "'2020/NO_ADDRESS/photoname.jpg'. " - + "If specified, the default value may not contain a brace symbol ('{' or '}')." - ) - formatter.write("\n") - formatter.write_text( - "If you do not specify a default value and the template substitution " - + "has no value, '_' (underscore) will be used as the default value. For example, in the " - + "above example, this would result in '2020/_/photoname.jpg' if address was null." - ) - formatter.write("\n") - formatter.write_text( - 'You may specify a null default (e.g. "" or empty string) by omitting the value after ' - + 'the comma, e.g. {title,} which would render to "" if title had no value.' - ) - formatter.write("\n") - templ_tuples = [("Substitution", "Description")] - templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items()) - formatter.write_dl(templ_tuples) - - formatter.write("\n") - formatter.write_text( - "The following substitutions may result in multiple values. Thus " - + "if specified for --directory these could result in multiple copies of a photo being " - + "being exported, one to each directory. For example: " - + "--directory '{created.year}/{album}' could result in the same photo being exported " - + "to each of the following directories if the photos were created in 2019 " - + "and were in albums 'Vacation' and 'Family': " - + "2019/Vacation, 2019/Family" - ) - formatter.write("\n") - templ_tuples = [("Substitution", "Description")] - templ_tuples.extend( - (k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items() - ) - - formatter.write_dl(templ_tuples) - help_text += formatter.getvalue() - return help_text - - -CTX_SETTINGS = dict(help_option_names=["-h", "--help"]) -DB_OPTION = click.option( - "--db", - required=False, - metavar="", - default=None, - help=( - "Specify Photos database path. " - "Path to Photos library/database can be specified using either --db " - "or directly as PHOTOS_LIBRARY positional argument. " - "If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library " - "to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary" - ), - type=click.Path(exists=True), -) - -DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True)) - -JSON_OPTION = click.option( - "--json", - "json_", - required=False, - is_flag=True, - default=False, - help="Print output in JSON format.", -) - - -def deleted_options(f): - o = click.option - options = [ - o( - "--deleted", - is_flag=True, - help="Include photos from the 'Recently Deleted' folder.", - ), - o( - "--deleted-only", - is_flag=True, - help="Include only photos from the 'Recently Deleted' folder.", - ), - ] - for o in options[::-1]: - f = o(f) - return f - - -def query_options(f): - o = click.option - options = [ - o( - "--keyword", - metavar="KEYWORD", - default=None, - multiple=True, - help="Search for photos with keyword KEYWORD. " - 'If more than one keyword, treated as "OR", e.g. find photos matching any keyword', - ), - o( - "--person", - metavar="PERSON", - default=None, - multiple=True, - help="Search for photos with person PERSON. " - 'If more than one person, treated as "OR", e.g. find photos matching any person', - ), - o( - "--album", - metavar="ALBUM", - default=None, - multiple=True, - help="Search for photos in album ALBUM. " - 'If more than one album, treated as "OR", e.g. find photos matching any album', - ), - o( - "--folder", - metavar="FOLDER", - default=None, - multiple=True, - help="Search for photos in an album in folder FOLDER. " - 'If more than one folder, treated as "OR", e.g. find photos in any FOLDER. ' - "Only searches top level folders (e.g. does not look at subfolders)", - ), - o( - "--uuid", - metavar="UUID", - default=None, - multiple=True, - help="Search for photos with UUID(s).", - ), - o( - "--uuid-from-file", - metavar="FILE", - default=None, - multiple=False, - help="Search for photos with UUID(s) loaded from FILE. " - "Format is a single UUID per line. Lines preceeded with # are ignored.", - type=click.Path(exists=True), - ), - o( - "--title", - metavar="TITLE", - default=None, - multiple=True, - help="Search for TITLE in title of photo.", - ), - o("--no-title", is_flag=True, help="Search for photos with no title."), - o( - "--description", - metavar="DESC", - default=None, - multiple=True, - help="Search for DESC in description of photo.", - ), - o( - "--no-description", - is_flag=True, - help="Search for photos with no description.", - ), - o( - "--place", - metavar="PLACE", - default=None, - multiple=True, - help="Search for PLACE in photo's reverse geolocation info", - ), - o( - "--no-place", - is_flag=True, - help="Search for photos with no associated place name info (no reverse geolocation info)", - ), - o( - "--label", - metavar="LABEL", - multiple=True, - help="Search for photos with image classification label LABEL (Photos 5 only). " - 'If more than one label, treated as "OR", e.g. find photos matching any label', - ), - o( - "--uti", - metavar="UTI", - default=None, - multiple=False, - help="Search for photos whose uniform type identifier (UTI) matches UTI", - ), - o( - "-i", - "--ignore-case", - is_flag=True, - help="Case insensitive search for title, description, place, keyword, person, or album.", - ), - o("--edited", is_flag=True, help="Search for photos that have been edited."), - o( - "--external-edit", - is_flag=True, - help="Search for photos edited in external editor.", - ), - o("--favorite", is_flag=True, help="Search for photos marked favorite."), - o( - "--not-favorite", - is_flag=True, - help="Search for photos not marked favorite.", - ), - o("--hidden", is_flag=True, help="Search for photos marked hidden."), - o("--not-hidden", is_flag=True, help="Search for photos not marked hidden."), - o( - "--shared", - is_flag=True, - help="Search for photos in shared iCloud album (Photos 5 only).", - ), - o( - "--not-shared", - is_flag=True, - help="Search for photos not in shared iCloud album (Photos 5 only).", - ), - o( - "--burst", - is_flag=True, - help="Search for photos that were taken in a burst.", - ), - o( - "--not-burst", - is_flag=True, - help="Search for photos that are not part of a burst.", - ), - o("--live", is_flag=True, help="Search for Apple live photos"), - o( - "--not-live", - is_flag=True, - help="Search for photos that are not Apple live photos.", - ), - o("--portrait", is_flag=True, help="Search for Apple portrait mode photos."), - o( - "--not-portrait", - is_flag=True, - help="Search for photos that are not Apple portrait mode photos.", - ), - o("--screenshot", is_flag=True, help="Search for screenshot photos."), - o( - "--not-screenshot", - is_flag=True, - help="Search for photos that are not screenshot photos.", - ), - o("--slow-mo", is_flag=True, help="Search for slow motion videos."), - o( - "--not-slow-mo", - is_flag=True, - help="Search for photos that are not slow motion videos.", - ), - o("--time-lapse", is_flag=True, help="Search for time lapse videos."), - o( - "--not-time-lapse", - is_flag=True, - help="Search for photos that are not time lapse videos.", - ), - o("--hdr", is_flag=True, help="Search for high dynamic range (HDR) photos."), - o("--not-hdr", is_flag=True, help="Search for photos that are not HDR photos."), - o( - "--selfie", - is_flag=True, - help="Search for selfies (photos taken with front-facing cameras).", - ), - o("--not-selfie", is_flag=True, help="Search for photos that are not selfies."), - o("--panorama", is_flag=True, help="Search for panorama photos."), - o( - "--not-panorama", - is_flag=True, - help="Search for photos that are not panoramas.", - ), - o( - "--has-raw", - is_flag=True, - help="Search for photos with both a jpeg and raw version", - ), - o( - "--only-movies", - is_flag=True, - help="Search only for movies (default searches both images and movies).", - ), - o( - "--only-photos", - is_flag=True, - help="Search only for photos/images (default searches both images and movies).", - ), - o( - "--from-date", - help="Search by start item date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601).", - type=DateTimeISO8601(), - ), - o( - "--to-date", - help="Search by end item date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601).", - type=DateTimeISO8601(), - ), - o("--has-comment", is_flag=True, help="Search for photos that have comments."), - o("--no-comment", is_flag=True, help="Search for photos with no comments."), - o("--has-likes", is_flag=True, help="Search for photos that have likes."), - o("--no-likes", is_flag=True, help="Search for photos with no likes."), - o( - "--is-reference", - is_flag=True, - help="Search for photos that were imported as referenced files (not copied into Photos library).", - ), - ] - for o in options[::-1]: - f = o(f) - return f - - -@click.group(context_settings=CTX_SETTINGS) -@DB_OPTION -@JSON_OPTION -@click.option("--debug", required=False, is_flag=True, default=False, hidden=True) -@click.version_option(__version__, "--version", "-v") -@click.pass_context -def cli(ctx, db, json_, debug): - ctx.obj = CLI_Obj(db=db, json=json_, debug=debug) - - -@cli.command(cls=ExportCommand) -@DB_OPTION -@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.") -@query_options -@click.option( - "--missing", - is_flag=True, - help="Export only photos missing from the Photos library; must be used with --download-missing.", -) -@deleted_options -@click.option( - "--update", - is_flag=True, - help="Only export new or updated files. See notes below on export and --update.", -) -@click.option( - "--ignore-signature", - is_flag=True, - help="When used with --update, ignores file signature when updating files. " - "This is useful if you have processed or edited exported photos changing the " - "file signature (size & modification date). In this case, --update would normally " - "re-export the processed files but with --ignore-signature, files which exist " - "in the export directory will not be re-exported.", -) -@click.option( - "--dry-run", - is_flag=True, - help="Dry run (test) the export but don't actually export any files; most useful with --verbose.", -) -@click.option( - "--export-as-hardlink", - is_flag=True, - help="Hardlink files instead of copying them. " - "Cannot be used with --exiftool which creates copies of the files with embedded EXIF data. " - "Note: on APFS volumes, files are cloned when exporting giving many of the same " - "advantages as hardlinks without having to use --export-as-hardlink.", -) -@click.option( - "--touch-file", - is_flag=True, - help="Sets the file's modification time to match photo date.", -) -@click.option( - "--overwrite", - is_flag=True, - help="Overwrite existing files. " - "Default behavior is to add (1), (2), etc to filename if file already exists. " - "Use this with caution as it may create name collisions on export. " - "(e.g. if two files happen to have the same name)", -) -@click.option( - "--export-by-date", - is_flag=True, - help="Automatically create output folders to organize photos by date created " - "(e.g. DEST/2019/12/20/photoname.jpg).", -) -@click.option( - "--skip-edited", - is_flag=True, - help="Do not export edited version of photo if an edited version exists.", -) -@click.option( - "--skip-original-if-edited", - is_flag=True, - help="Do not export original if there is an edited version (exports only the edited version).", -) -@click.option( - "--skip-bursts", - is_flag=True, - help="Do not export all associated burst images in the library if a photo is a burst photo. ", -) -@click.option( - "--skip-live", - is_flag=True, - help="Do not export the associated live video component of a live photo.", -) -@click.option( - "--skip-raw", - is_flag=True, - help="Do not export associated raw images of a RAW+JPEG pair. " - "Note: this does not skip raw photos if the raw photo does not have an associated jpeg image " - "(e.g. the raw file was imported to Photos without a jpeg preview).", -) -@click.option( - "--current-name", - is_flag=True, - help="Use photo's current filename instead of original filename for export. " - "Note: Starting with Photos 5, all photos are renamed upon import. By default, " - "photos are exported with the the original name they had before import.", -) -@click.option( - "--convert-to-jpeg", - is_flag=True, - help="Convert all non-jpeg images (e.g. raw, HEIC, PNG, etc) " - "to JPEG upon export. Only works if your Mac has a GPU.", -) -@click.option( - "--jpeg-quality", - type=click.FloatRange(0.0, 1.0), - help="Value in range 0.0 to 1.0 to use with --convert-to-jpeg. " - "A value of 1.0 specifies best quality, " - "a value of 0.0 specifies maximum compression. " - f"Defaults to {DEFAULT_JPEG_QUALITY}", -) -@click.option( - "--download-missing", - is_flag=True, - help="Attempt to download missing photos from iCloud. The current implementation uses Applescript " - "to interact with Photos to export the photo which will force Photos to download from iCloud if " - "the photo does not exist on disk. This will be slow and will require internet connection. " - "This obviously only works if the Photos library is synched to iCloud. " - "Note: --download-missing does not currently export all burst images; " - "only the primary photo will be exported--associated burst images will be skipped.", -) -@click.option( - "--sidecar", - default=None, - multiple=True, - metavar="FORMAT", - type=click.Choice(["xmp", "json", "exiftool"], case_sensitive=False), - help="Create sidecar for each photo exported; valid FORMAT values: xmp, json, exiftool; " - "--sidecar xmp: create XMP sidecar used by Digikam, Adobe Lightroom, etc. " - "The sidecar file is named in format photoname.ext.xmp " - "The XMP sidecar exports the following tags: Description, Title, Keywords/Tags, " - "Subject (set to Keywords + PersonInImage), PersonInImage, CreateDate, ModifyDate, " - "GPSLongitude, Face Regions (Metadata Working Group and Microsoft Photo)." - f"\n--sidecar json: create JSON sidecar useable by exiftool ({_EXIF_TOOL_URL}) " - "The sidecar file can be used to apply metadata to the file with exiftool, for example: " - '"exiftool -j=photoname.jpg.json photoname.jpg" ' - "The sidecar file is named in format photoname.ext.json; " - "format includes tag groups (equivalent to running 'exiftool -G -j'). " - "\n--sidecar exiftool: create JSON sidecar compatible with output of 'exiftool -j'. " - "Unlike '--sidecar json', '--sidecar exiftool' does not export tag groups. " - "Sidecar filename is in format photoname.ext.json; " - "For a list of tags exported in the JSON and exiftool sidecar, see '--exiftool'.", -) -@click.option( - "--sidecar-drop-ext", - is_flag=True, - help="Drop the photo's extension when naming sidecar files. " - "By default, sidecar files are named in format 'photo_filename.photo_ext.sidecar_ext', " - "e.g. 'IMG_1234.JPG.xmp'. Use '--sidecar-drop-ext' to ignore the photo extension. " - "Resulting sidecar files will have name in format 'IMG_1234.xmp'. " - "Warning: this may result in sidecar filename collisions if there are files of different " - "types but the same name in the output directory, e.g. 'IMG_1234.JPG' and 'IMG_1234.MOV'.", -) -@click.option( - "--exiftool", - is_flag=True, - help="Use exiftool to write metadata directly to exported photos. " - "To use this option, exiftool must be installed and in the path. " - "exiftool may be installed from https://exiftool.org/. " - "Cannot be used with --export-as-hardlink. Writes the following metadata: " - "EXIF:ImageDescription, XMP:Description (see also --description-template); " - "XMP:Title; XMP:TagsList, IPTC:Keywords, XMP:Subject " - "(see also --keyword-template, --person-keyword, --album-keyword); " - "XMP:PersonInImage; EXIF:GPSLatitudeRef; EXIF:GPSLongitudeRef; EXIF:GPSLatitude; EXIF:GPSLongitude; " - "EXIF:GPSPosition; EXIF:DateTimeOriginal; EXIF:OffsetTimeOriginal; " - "EXIF:ModifyDate (see --ignore-date-modified); IPTC:DateCreated; IPTC:TimeCreated; " - "(video files only): QuickTime:CreationDate; QuickTime:CreateDate; QuickTime:ModifyDate (see also --ignore-date-modified); " - "QuickTime:GPSCoordinates; UserData:GPSCoordinates.", -) -@click.option( - "--exiftool-path", - metavar="EXIFTOOL_PATH", - type=click.Path(exists=True), - help="Optionally specify path to exiftool; if not provided, will look for exiftool in $PATH.", -) -@click.option( - "--exiftool-option", - multiple=True, - metavar="OPTION", - help="Optional flag/option to pass to exiftool when using --exiftool. " - "For example, --exiftool-option '-m' to ignore minor warnings. " - "Specify these as you would on the exiftool command line. " - "See exiftool docs at https://exiftool.org/exiftool_pod.html for full list of options. " - "More than one option may be specified by repeating the option, e.g. " - "--exiftool-option '-m' --exiftool-option '-F'. ", -) -@click.option( - "--exiftool-merge-keywords", - is_flag=True, - help="Merge any keywords found in the original file with keywords used for '--exiftool' and '--sidecar'.", -) -@click.option( - "--exiftool-merge-persons", - is_flag=True, - help="Merge any persons found in the original file with persons used for '--exiftool' and '--sidecar'.", -) -@click.option( - "--ignore-date-modified", - is_flag=True, - help="If used with --exiftool or --sidecar, will ignore the photo " - "modification date and set EXIF:ModifyDate to EXIF:DateTimeOriginal; " - "this is consistent with how Photos handles the EXIF:ModifyDate tag.", -) -@click.option( - "--person-keyword", - is_flag=True, - help="Use person in image as keyword/tag when exporting metadata.", -) -@click.option( - "--album-keyword", - is_flag=True, - help="Use album name as keyword/tag when exporting metadata.", -) -@click.option( - "--keyword-template", - metavar="TEMPLATE", - multiple=True, - default=None, - help="For use with --exiftool, --sidecar; specify a template string to use as " - "keyword in the form '{name,DEFAULT}' " - "This is the same format as --directory. For example, if you wanted to add " - "the full path to the folder and album photo is contained in as a keyword when exporting " - 'you could specify --keyword-template "{folder_album}" ' - 'You may specify more than one template, for example --keyword-template "{folder_album}" ' - '--keyword-template "{created.year}" ' - "See Templating System below.", -) -@click.option( - "--description-template", - metavar="TEMPLATE", - multiple=False, - default=None, - help="For use with --exiftool, --sidecar; specify a template string to use as " - "description in the form '{name,DEFAULT}' " - "This is the same format as --directory. For example, if you wanted to append " - "'exported with osxphotos on [today's date]' to the description, you could specify " - '--description-template "{descr} exported with osxphotos on {today.date}" ' - "See Templating System below.", -) -@click.option( - "--finder-tag-template", - metavar="TEMPLATE", - multiple=True, - default=None, - help="Set MacOS Finder tags to TEMPLATE. These tags can be searched in the Finder or Spotlight with " - "'tag:tagname' format. For example, '--finder-tag-template \"{label}\"' to set Finder tags to photo labels. " - "You may specify multiple TEMPLATE values by using '--finder-tag-template' multiple times. " - "See also '--finder-tag-keywords and Extended Attributes below.'.", -) -@click.option( - "--finder-tag-keywords", - is_flag=True, - help="Set MacOS Finder tags to keywords; any keywords specified via '--keyword-template', '--person-keyword', etc. " - "will also be used as Finder tags. See also '--finder-tag-template and Extended Attributes below.'.", -) -@click.option( - "--xattr-template", - nargs=2, - metavar="ATTRIBUTE TEMPLATE", - multiple=True, - help="Set extended attribute ATTRIBUTE to TEMPLATE value. Valid attributes are: " - f"{', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}. " - "For example, to set Finder comment to the photo's title and description: " - '\'--xattr-template findercomment "{title}; {descr}" ' - "See Extended Attributes below for additional details on this option.", -) -@click.option( - "--directory", - metavar="DIRECTORY", - default=None, - help="Optional template for specifying name of output directory in the form '{name,DEFAULT}'. " - "See below for additional details on templating system.", -) -@click.option( - "--filename", - "filename_template", - metavar="FILENAME", - default=None, - help="Optional template for specifying name of output file in the form '{name,DEFAULT}'. " - "File extension will be added automatically--do not include an extension in the FILENAME template. " - "See below for additional details on templating system.", -) -@click.option( - "--jpeg-ext", - multiple=False, - metavar="EXTENSION", - type=click.Choice(["jpeg", "jpg", "JPEG", "JPG"], case_sensitive=True), - help="Specify file extension for JPEG files. Photos uses .jpeg for edited images but many images " - "are imported with .jpg or .JPG which can result in multiple different extensions used for JPEG files " - "upon export. Use --jpg-ext to specify a single extension to use for all exported JPEG images. " - "Valid values are jpeg, jpg, JPEG, JPG; e.g. '--jpg-ext jpg' to use '.jpg' for all JPEGs.", -) -@click.option( - "--strip", - is_flag=True, - help="Optionally strip leading and trailing whitespace from any rendered templates. " - 'For example, if --filename template is "{title,} {original_name}" and image has no ' - "title, resulting file would have a leading space but if used with --strip, this will " - "be removed.", -) -@click.option( - "--edited-suffix", - metavar="SUFFIX", - help="Optional suffix template for naming edited photos. Default name for edited photos is in form " - "'photoname_edited.ext'. For example, with '--edited-suffix _bearbeiten', the edited photo " - f"would be named 'photoname_bearbeiten.ext'. The default suffix is '{DEFAULT_EDITED_SUFFIX}'. " - "Multi-value templates (see Templating System) are not permitted with --edited-suffix.", -) -@click.option( - "--original-suffix", - metavar="SUFFIX", - help="Optional suffix template for naming original photos. Default name for original photos is in form " - "'filename.ext'. For example, with '--original-suffix _original', the original photo " - "would be named 'filename_original.ext'. The default suffix is '' (no suffix). " - "Multi-value templates (see Templating System) are not permitted with --original-suffix.", -) -@click.option( - "--use-photos-export", - is_flag=True, - help="Force the use of AppleScript or PhotoKit to export even if not missing (see also '--download-missing' and '--use-photokit').", -) -@click.option( - "--use-photokit", - is_flag=True, - help="Use with '--download-missing' or '--use-photos-export' to use direct Photos interface instead of AppleScript to export. " - "Highly experimental alpha feature; does not work with iTerm2 (use with Terminal.app). " - "This is faster and more reliable than the default AppleScript interface.", -) -@click.option( - "--report", - metavar="", - help="Write a CSV formatted report of all files that were exported.", - type=click.Path(), -) -@click.option( - "--cleanup", - is_flag=True, - help="Cleanup export directory by deleting any files which were not included in this export set. " - "For example, photos which had previously been exported and were subsequently deleted in Photos.", -) -@click.option( - "--exportdb", - metavar="EXPORTDB_FILE", - default=None, - help=( - "Specify alternate name for database file which stores state information for export and --update. " - f"If --exportdb is not specified, export database will be saved to '{OSXPHOTOS_EXPORT_DB}' " - "in the export directory. Must be specified as filename only, not a path, as export database " - "will be saved in export directory." - ), - type=click.Path(), -) -@click.option( - "--load-config", - required=False, - metavar="", - default=None, - help=( - "Load options from file as written with --save-config. " - "This allows you to save a complex export command to file for later reuse. " - "For example: 'osxphotos export --save-config osxphotos.toml' then " - " 'osxphotos export /path/to/export --load-config osxphotos.toml'. " - "If any other command line options are used in conjunction with --load-config, " - "they will override the corresponding values in the config file." - ), - type=click.Path(exists=True), -) -@click.option( - "--save-config", - required=False, - metavar="", - default=None, - help=("Save options to file for use with --load-config. File format is TOML."), - type=click.Path(), -) -@click.option( - "--beta", is_flag=True, default=False, hidden=True, help="Enable beta options." -) -@DB_ARGUMENT -@click.argument("dest", nargs=1, type=click.Path(exists=True)) -@click.pass_obj -@click.pass_context -def export( - ctx, - cli_obj, - db, - photos_library, - keyword, - person, - album, - folder, - uuid, - uuid_from_file, - title, - no_title, - description, - no_description, - uti, - ignore_case, - edited, - external_edit, - favorite, - not_favorite, - hidden, - not_hidden, - shared, - not_shared, - from_date, - to_date, - verbose, - missing, - update, - ignore_signature, - dry_run, - export_as_hardlink, - touch_file, - overwrite, - export_by_date, - skip_edited, - skip_original_if_edited, - skip_bursts, - skip_live, - skip_raw, - person_keyword, - album_keyword, - keyword_template, - description_template, - finder_tag_template, - finder_tag_keywords, - xattr_template, - current_name, - convert_to_jpeg, - jpeg_quality, - sidecar, - sidecar_drop_ext, - only_photos, - only_movies, - burst, - not_burst, - live, - not_live, - download_missing, - dest, - exiftool, - exiftool_path, - exiftool_option, - exiftool_merge_keywords, - exiftool_merge_persons, - ignore_date_modified, - portrait, - not_portrait, - screenshot, - not_screenshot, - slow_mo, - not_slow_mo, - time_lapse, - not_time_lapse, - hdr, - not_hdr, - selfie, - not_selfie, - panorama, - not_panorama, - has_raw, - directory, - filename_template, - jpeg_ext, - strip, - edited_suffix, - original_suffix, - place, - no_place, - has_comment, - no_comment, - has_likes, - no_likes, - label, - deleted, - deleted_only, - use_photos_export, - use_photokit, - report, - cleanup, - exportdb, - load_config, - save_config, - is_reference, - beta, -): - """Export photos from the Photos database. - Export path DEST is required. - Optionally, query the Photos database using 1 or more search options; - if more than one option is provided, they are treated as "AND" - (e.g. search for photos matching all options). - If no query options are provided, all photos will be exported. - By default, all versions of all photos will be exported including edited - versions, live photo movies, burst photos, and associated raw images. - See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options - to modify this behavior. - """ - - # NOTE: because of the way ConfigOptions works, Click options must not - # set defaults which are not None or False. If defaults need to be set - # do so below after load_config and save_config are handled. - cfg = ConfigOptions( - "export", - locals(), - ignore=["ctx", "cli_obj", "dest", "load_config", "save_config"], - ) - - global VERBOSE - VERBOSE = bool(verbose) - - if load_config: - try: - cfg.load_from_file(load_config) - except ConfigOptionsLoadError as e: - click.echo( - click.style( - f"Error parsing {load_config} config file: {e.message}", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - raise click.Abort() - - # re-set the local vars to the corresponding config value - # this isn't elegant but avoids having to rewrite this function to use cfg.varname for every parameter - db = cfg.db - photos_library = cfg.photos_library - keyword = cfg.keyword - person = cfg.person - album = cfg.album - folder = cfg.folder - uuid = cfg.uuid - uuid_from_file = cfg.uuid_from_file - title = cfg.title - no_title = cfg.no_title - description = cfg.description - no_description = cfg.no_description - uti = cfg.uti - ignore_case = cfg.ignore_case - edited = cfg.edited - external_edit = cfg.external_edit - favorite = cfg.favorite - not_favorite = cfg.not_favorite - hidden = cfg.hidden - not_hidden = cfg.not_hidden - shared = cfg.shared - not_shared = cfg.not_shared - from_date = cfg.from_date - to_date = cfg.to_date - verbose = cfg.verbose - missing = cfg.missing - update = cfg.update - ignore_signature = cfg.ignore_signature - dry_run = cfg.dry_run - export_as_hardlink = cfg.export_as_hardlink - touch_file = cfg.touch_file - overwrite = cfg.overwrite - export_by_date = cfg.export_by_date - skip_edited = cfg.skip_edited - skip_original_if_edited = cfg.skip_original_if_edited - skip_bursts = cfg.skip_bursts - skip_live = cfg.skip_live - skip_raw = cfg.skip_raw - person_keyword = cfg.person_keyword - album_keyword = cfg.album_keyword - keyword_template = cfg.keyword_template - description_template = cfg.description_template - finder_tag_template = cfg.finder_tag_template - finder_tag_keywords = cfg.finder_tag_keywords - xattr_template = cfg.xattr_template - current_name = cfg.current_name - convert_to_jpeg = cfg.convert_to_jpeg - jpeg_quality = cfg.jpeg_quality - sidecar = cfg.sidecar - sidecar_drop_ext = cfg.sidecar_drop_ext - only_photos = cfg.only_photos - only_movies = cfg.only_movies - burst = cfg.burst - not_burst = cfg.not_burst - live = cfg.live - not_live = cfg.not_live - download_missing = cfg.download_missing - exiftool = cfg.exiftool - exiftool_path = cfg.exiftool_path - exiftool_option = cfg.exiftool_option - exiftool_merge_keywords = cfg.exiftool_merge_keywords - exiftool_merge_persons = cfg.exiftool_merge_persons - ignore_date_modified = cfg.ignore_date_modified - portrait = cfg.portrait - not_portrait = cfg.not_portrait - screenshot = cfg.screenshot - not_screenshot = cfg.not_screenshot - slow_mo = cfg.slow_mo - not_slow_mo = cfg.not_slow_mo - time_lapse = cfg.time_lapse - not_time_lapse = cfg.not_time_lapse - hdr = cfg.hdr - not_hdr = cfg.not_hdr - selfie = cfg.selfie - not_selfie = cfg.not_selfie - panorama = cfg.panorama - not_panorama = cfg.not_panorama - has_raw = cfg.has_raw - directory = cfg.directory - filename_template = cfg.filename_template - jpeg_ext = cfg.jpeg_ext - strip = cfg.strip - edited_suffix = cfg.edited_suffix - original_suffix = cfg.original_suffix - place = cfg.place - no_place = cfg.no_place - has_comment = cfg.has_comment - no_comment = cfg.no_comment - has_likes = cfg.has_likes - no_likes = cfg.no_likes - label = cfg.label - deleted = cfg.deleted - deleted_only = cfg.deleted_only - use_photos_export = cfg.use_photos_export - use_photokit = cfg.use_photokit - report = cfg.report - cleanup = cfg.cleanup - exportdb = cfg.exportdb - beta = cfg.beta - - # config file might have changed verbose - VERBOSE = bool(verbose) - verbose_(f"Loaded options from file {load_config}") - - verbose_(f"osxphotos version {__version__}") - - # validate options - exclusive_options = [ - ("favorite", "not_favorite"), - ("hidden", "not_hidden"), - ("title", "no_title"), - ("description", "no_description"), - ("only_photos", "only_movies"), - ("burst", "not_burst"), - ("live", "not_live"), - ("portrait", "not_portrait"), - ("screenshot", "not_screenshot"), - ("slow_mo", "not_slow_mo"), - ("time_lapse", "not_time_lapse"), - ("hdr", "not_hdr"), - ("selfie", "not_selfie"), - ("panorama", "not_panorama"), - ("export_by_date", "directory"), - ("export_as_hardlink", "exiftool"), - ("place", "no_place"), - ("deleted", "deleted_only"), - ("skip_edited", "skip_original_if_edited"), - ("export_as_hardlink", "convert_to_jpeg"), - ("export_as_hardlink", "download_missing"), - ("shared", "not_shared"), - ("has_comment", "no_comment"), - ("has_likes", "no_likes"), - ] - dependent_options = [ - ("missing", ("download_missing", "use_photos_export")), - ("jpeg_quality", ("convert_to_jpeg")), - ("ignore_signature", ("update")), - ("exiftool_option", ("exiftool")), - ("exiftool_merge_keywords", ("exiftool", "sidecar")), - ("exiftool_merge_persons", ("exiftool", "sidecar")), - ] - try: - cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True) - except ConfigOptionsInvalidError as e: - click.echo( - click.style( - f"Incompatible export options: {e.message}", fg=CLI_COLOR_ERROR - ), - err=True, - ) - raise click.Abort() - - if all(x in [s.lower() for s in sidecar] for x in ["json", "exiftool"]): - click.echo( - click.style( - "Cannot use --sidecar json with --sidecar exiftool due to name collisions", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - raise click.Abort() - - if xattr_template: - for attr, _ in xattr_template: - if attr not in EXTENDED_ATTRIBUTE_NAMES: - click.echo( - click.style( - f"Invalid attribute '{attr}' for --xattr-template; " - f"valid values are {', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - raise click.Abort() - - if save_config: - verbose_(f"Saving options to file {save_config}") - cfg.write_to_file(save_config) - - # set defaults for options that need them - jpeg_quality = DEFAULT_JPEG_QUALITY if jpeg_quality is None else jpeg_quality - edited_suffix = DEFAULT_EDITED_SUFFIX if edited_suffix is None else edited_suffix - original_suffix = ( - DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix - ) - - if not os.path.isdir(dest): - click.echo( - click.style(f"DEST {dest} must be valid path", fg=CLI_COLOR_ERROR), err=True - ) - raise click.Abort() - - dest = str(pathlib.Path(dest).resolve()) - - if report and os.path.isdir(report): - click.echo( - click.style( - f"report is a directory, must be file name", fg=CLI_COLOR_ERROR - ), - err=True, - ) - raise click.Abort() - - # if use_photokit and not check_photokit_authorization(): - # click.echo( - # "Requesting access to use your Photos library. Click 'OK' on the dialog box to grant access." - # ) - # request_photokit_authorization() - # click.confirm("Have you granted access?") - # if not check_photokit_authorization(): - # click.echo( - # "Failed to get access to the Photos library which is needed with `--use-photokit`." - # ) - # return - - # initialize export flags - # by default, will export all versions of photos unless skip flag is set - (export_edited, export_bursts, export_live, export_raw) = [ - not x for x in [skip_edited, skip_bursts, skip_live, skip_raw] - ] - - # verify exiftool installed and in path if path not provided and exiftool will be used - # NOTE: this won't catch use of {exiftool:} in a template - # but those will raise error during template eval if exiftool path not set - if ( - any([exiftool, exiftool_merge_keywords, exiftool_merge_persons]) - and not exiftool_path - ): - try: - exiftool_path = get_exiftool_path() - except FileNotFoundError: - click.echo( - click.style( - "Could not find exiftool. Please download and install" - " from https://exiftool.org/", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - ctx.exit(2) - - if any([exiftool, exiftool_merge_keywords, exiftool_merge_persons]): - verbose_(f"exiftool path: {exiftool_path}") - - isphoto = ismovie = True # default searches for everything - if only_movies: - isphoto = False - if only_photos: - ismovie = False - - # load UUIDs if necessary and append to any uuids passed with --uuid - if uuid_from_file: - uuid_list = list(uuid) # Click option is a tuple - uuid_list.extend(load_uuid_from_file(uuid_from_file)) - uuid = tuple(uuid_list) - - # below needed for to make CliRunner work for testing - cli_db = cli_obj.db if cli_obj is not None else None - db = get_photos_db(*photos_library, db, cli_db) - if db is None: - click.echo(cli.commands["export"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - # sanity check exportdb - if exportdb and exportdb != OSXPHOTOS_EXPORT_DB: - if "/" in exportdb: - click.echo( - click.style( - f"Error: --exportdb must be specified as filename not path; " - + f"export database will saved in export directory '{dest}'.", - fg=CLI_COLOR_ERROR, - ) - ) - raise click.Abort() - elif pathlib.Path(pathlib.Path(dest) / OSXPHOTOS_EXPORT_DB).exists(): - click.echo( - click.style( - f"Warning: export database is '{exportdb}' but found '{OSXPHOTOS_EXPORT_DB}' in {dest}; using '{exportdb}'", - fg=CLI_COLOR_WARNING, - ) - ) - - # open export database and assign copy/link/unlink functions - export_db_path = os.path.join(dest, exportdb or OSXPHOTOS_EXPORT_DB) - - # check that export isn't in the parent or child of a previously exported library - other_db_files = find_files_in_branch(dest, OSXPHOTOS_EXPORT_DB) - if other_db_files: - click.echo( - click.style( - "WARNING: found other export database files in this destination directory branch. " - + "This likely means you are attempting to export files into a directory " - + "that is either the parent or a child directory of a previous export. " - + "Proceeding may cause your exported files to be overwritten.", - fg=CLI_COLOR_WARNING, - ), - err=True, - ) - click.echo( - f"You are exporting to {dest}, found {OSXPHOTOS_EXPORT_DB} files in:" - ) - for other_db in other_db_files: - click.echo(f"{other_db}") - click.confirm("Do you want to continue?", abort=True) - - if dry_run: - export_db = ExportDBInMemory(export_db_path) - fileutil = FileUtilNoOp - else: - export_db = ExportDB(export_db_path) - fileutil = FileUtil - - if verbose_: - if export_db.was_created: - verbose_(f"Created export database {export_db_path}") - else: - verbose_(f"Using export database {export_db_path}") - upgraded = export_db.was_upgraded - if upgraded: - verbose_( - f"Upgraded export database {export_db_path} from version {upgraded[0]} to {upgraded[1]}" - ) - - photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_, exiftool=exiftool_path) - - # enable beta features if requested - photosdb._beta = beta - - photos = _query( - photosdb=photosdb, - keyword=keyword, - person=person, - album=album, - folder=folder, - uuid=uuid, - title=title, - no_title=no_title, - description=description, - no_description=no_description, - ignore_case=ignore_case, - edited=edited, - external_edit=external_edit, - favorite=favorite, - not_favorite=not_favorite, - hidden=hidden, - not_hidden=not_hidden, - missing=missing, - not_missing=None, - shared=shared, - not_shared=not_shared, - isphoto=isphoto, - ismovie=ismovie, - uti=uti, - burst=burst, - not_burst=not_burst, - live=live, - not_live=not_live, - cloudasset=False, - not_cloudasset=False, - incloud=False, - not_incloud=False, - from_date=from_date, - to_date=to_date, - portrait=portrait, - not_portrait=not_portrait, - screenshot=screenshot, - not_screenshot=not_screenshot, - slow_mo=slow_mo, - not_slow_mo=not_slow_mo, - time_lapse=time_lapse, - not_time_lapse=not_time_lapse, - hdr=hdr, - not_hdr=not_hdr, - selfie=selfie, - not_selfie=not_selfie, - panorama=panorama, - not_panorama=not_panorama, - has_raw=has_raw, - place=place, - no_place=no_place, - label=label, - deleted=deleted, - deleted_only=deleted_only, - has_comment=has_comment, - no_comment=no_comment, - has_likes=has_likes, - no_likes=no_likes, - is_reference=is_reference, - ) - - if photos: - if export_bursts: - # add the burst_photos to the export set - photos_burst = [p for p in photos if p.burst] - for burst in photos_burst: - burst_set = [p for p in burst.burst_photos if not p.ismissing] - photos.extend(burst_set) - - num_photos = len(photos) - # TODO: photos or photo appears several times, pull into a separate function - photo_str = "photos" if num_photos > 1 else "photo" - click.echo(f"Exporting {num_photos} {photo_str} to {dest}...") - start_time = time.perf_counter() - # though the command line option is current_name, internally all processing - # logic uses original_name which is the boolean inverse of current_name - # because the original code used --original-name as an option - original_name = not current_name - - results = ExportResults() - # send progress bar output to /dev/null if verbose to hide the progress bar - fp = open(os.devnull, "w") if verbose else None - with click.progressbar(photos, file=fp) as bar: - for p in bar: - export_results = export_photo( - photo=p, - dest=dest, - verbose=verbose, - export_by_date=export_by_date, - sidecar=sidecar, - sidecar_drop_ext=sidecar_drop_ext, - update=update, - ignore_signature=ignore_signature, - export_as_hardlink=export_as_hardlink, - overwrite=overwrite, - export_edited=export_edited, - skip_original_if_edited=skip_original_if_edited, - original_name=original_name, - export_live=export_live, - download_missing=download_missing, - exiftool=exiftool, - exiftool_merge_keywords=exiftool_merge_keywords, - exiftool_merge_persons=exiftool_merge_persons, - directory=directory, - filename_template=filename_template, - export_raw=export_raw, - album_keyword=album_keyword, - person_keyword=person_keyword, - keyword_template=keyword_template, - description_template=description_template, - export_db=export_db, - fileutil=fileutil, - dry_run=dry_run, - touch_file=touch_file, - edited_suffix=edited_suffix, - original_suffix=original_suffix, - use_photos_export=use_photos_export, - convert_to_jpeg=convert_to_jpeg, - jpeg_quality=jpeg_quality, - ignore_date_modified=ignore_date_modified, - use_photokit=use_photokit, - exiftool_option=exiftool_option, - strip=strip, - jpeg_ext=jpeg_ext, - ) - results += export_results - - # all photo files (not including sidecars) that are part of this export set - # used below for applying Finder tags, etc. - photo_files = set( - export_results.exported - + export_results.new - + export_results.updated - + export_results.exif_updated - + export_results.converted_to_jpeg - + export_results.skipped - ) - - if finder_tag_keywords or finder_tag_template: - tags_written, tags_skipped = write_finder_tags( - p, - photo_files, - keywords=finder_tag_keywords, - keyword_template=keyword_template, - album_keyword=album_keyword, - person_keyword=person_keyword, - exiftool_merge_keywords=exiftool_merge_keywords, - finder_tag_template=finder_tag_template, - strip=strip, - ) - results.xattr_written.extend(tags_written) - results.xattr_skipped.extend(tags_skipped) - - if xattr_template: - xattr_written, xattr_skipped = write_extended_attributes( - p, photo_files, xattr_template, strip=strip - ) - results.xattr_written.extend(xattr_written) - results.xattr_skipped.extend(xattr_skipped) - - if fp is not None: - fp.close() - - if cleanup: - all_files = ( - results.exported - + results.skipped - + results.exif_updated - + results.touched - + results.converted_to_jpeg - + results.sidecar_json_written - + results.sidecar_json_skipped - + results.sidecar_exiftool_written - + results.sidecar_exiftool_skipped - + results.sidecar_xmp_written - + results.sidecar_xmp_skipped - # include missing so a file that was already in export directory - # but was missing on --update doesn't get deleted - # (better to have old version than none) - + results.missing - # include files that have error in case they exist from previous export - + [r[0] for r in results.error] - + [str(pathlib.Path(export_db_path).resolve())] - ) - click.echo(f"Cleaning up {dest}") - (cleaned_files, cleaned_dirs) = cleanup_files(dest, all_files, fileutil) - file_str = "files" if cleaned_files != 1 else "file" - dir_str = "directories" if cleaned_dirs != 1 else "directory" - click.echo(f"Deleted: {cleaned_files} {file_str}, {cleaned_dirs} {dir_str}") - - if report: - verbose_(f"Writing export report to {report}") - write_export_report(report, results) - - photo_str_total = "photos" if len(photos) != 1 else "photo" - if update: - summary = ( - f"Processed: {len(photos)} {photo_str_total}, " - f"exported: {len(results.new)}, " - f"updated: {len(results.updated)}, " - f"skipped: {len(results.skipped)}, " - f"updated EXIF data: {len(results.exif_updated)}, " - ) - else: - summary = ( - f"Processed: {len(photos)} {photo_str_total}, " - f"exported: {len(results.exported)}, " - ) - summary += f"missing: {len(results.missing)}, " - summary += f"error: {len(results.error)}" - if touch_file: - summary += f", touched date: {len(results.touched)}" - click.echo(summary) - stop_time = time.perf_counter() - click.echo(f"Elapsed time: {(stop_time-start_time):.3f} seconds") - else: - click.echo("Did not find any photos to export") - - export_db.close() - - -@cli.command() -@click.argument("topic", default=None, required=False, nargs=1) -@click.pass_context -def help(ctx, topic, **kw): - """ Print help; for help on commands: help . """ - if topic is None: - click.echo(ctx.parent.get_help()) - elif topic in cli.commands: - ctx.info_name = topic - click.echo_via_pager(cli.commands[topic].get_help(ctx)) - else: - click.echo(f"Invalid command: {topic}", err=True) - click.echo(ctx.parent.get_help()) - - -@cli.command() -@DB_OPTION -@JSON_OPTION -@query_options -@deleted_options -@click.option("--missing", is_flag=True, help="Search for photos missing from disk.") -@click.option( - "--not-missing", - is_flag=True, - help="Search for photos present on disk (e.g. not missing).", -) -@click.option( - "--cloudasset", - is_flag=True, - help="Search for photos that are part of an iCloud library", -) -@click.option( - "--not-cloudasset", - is_flag=True, - help="Search for photos that are not part of an iCloud library", -) -@click.option( - "--incloud", - is_flag=True, - help="Search for photos that are in iCloud (have been synched)", -) -@click.option( - "--not-incloud", - is_flag=True, - help="Search for photos that are not in iCloud (have not been synched)", -) -@DB_ARGUMENT -@click.pass_obj -@click.pass_context -def query( - ctx, - cli_obj, - db, - photos_library, - keyword, - person, - album, - folder, - uuid, - uuid_from_file, - title, - no_title, - description, - no_description, - ignore_case, - json_, - edited, - external_edit, - favorite, - not_favorite, - hidden, - not_hidden, - missing, - not_missing, - shared, - not_shared, - only_movies, - only_photos, - uti, - burst, - not_burst, - live, - not_live, - cloudasset, - not_cloudasset, - incloud, - not_incloud, - from_date, - to_date, - portrait, - not_portrait, - screenshot, - not_screenshot, - slow_mo, - not_slow_mo, - time_lapse, - not_time_lapse, - hdr, - not_hdr, - selfie, - not_selfie, - panorama, - not_panorama, - has_raw, - place, - no_place, - label, - deleted, - deleted_only, - has_comment, - no_comment, - has_likes, - no_likes, - is_reference, -): - """Query the Photos database using 1 or more search options; - if more than one option is provided, they are treated as "AND" - (e.g. search for photos matching all options). - """ - - # if no query terms, show help and return - # sanity check input args - nonexclusive = [ - keyword, - person, - album, - folder, - uuid, - uuid_from_file, - edited, - external_edit, - uti, - has_raw, - from_date, - to_date, - label, - is_reference, - ] - exclusive = [ - (favorite, not_favorite), - (hidden, not_hidden), - (missing, not_missing), - (any(title), no_title), - (any(description), no_description), - (only_photos, only_movies), - (burst, not_burst), - (live, not_live), - (cloudasset, not_cloudasset), - (incloud, not_incloud), - (portrait, not_portrait), - (screenshot, not_screenshot), - (slow_mo, not_slow_mo), - (time_lapse, not_time_lapse), - (hdr, not_hdr), - (selfie, not_selfie), - (panorama, not_panorama), - (any(place), no_place), - (deleted, deleted_only), - (shared, not_shared), - (has_comment, no_comment), - (has_likes, no_likes), - ] - # print help if no non-exclusive term or a double exclusive term is given - if any(all(bb) for bb in exclusive) or not any( - nonexclusive + [b ^ n for b, n in exclusive] - ): - click.echo("Incompatible query options", err=True) - click.echo(cli.commands["query"].get_help(ctx), err=True) - return - - # actually have something to query - isphoto = ismovie = True # default searches for everything - if only_movies: - isphoto = False - if only_photos: - ismovie = False - - # load UUIDs if necessary and append to any uuids passed with --uuid - if uuid_from_file: - uuid_list = list(uuid) # Click option is a tuple - uuid_list.extend(load_uuid_from_file(uuid_from_file)) - uuid = tuple(uuid_list) - - # below needed for to make CliRunner work for testing - cli_db = cli_obj.db if cli_obj is not None else None - db = get_photos_db(*photos_library, db, cli_db) - if db is None: - click.echo(cli.commands["query"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_) - photos = _query( - photosdb=photosdb, - keyword=keyword, - person=person, - album=album, - folder=folder, - uuid=uuid, - title=title, - no_title=no_title, - description=description, - no_description=no_description, - ignore_case=ignore_case, - edited=edited, - external_edit=external_edit, - favorite=favorite, - not_favorite=not_favorite, - hidden=hidden, - not_hidden=not_hidden, - missing=missing, - not_missing=not_missing, - shared=shared, - not_shared=not_shared, - isphoto=isphoto, - ismovie=ismovie, - uti=uti, - burst=burst, - not_burst=not_burst, - live=live, - not_live=not_live, - cloudasset=cloudasset, - not_cloudasset=not_cloudasset, - incloud=incloud, - not_incloud=not_incloud, - from_date=from_date, - to_date=to_date, - portrait=portrait, - not_portrait=not_portrait, - screenshot=screenshot, - not_screenshot=not_screenshot, - slow_mo=slow_mo, - not_slow_mo=not_slow_mo, - time_lapse=time_lapse, - not_time_lapse=not_time_lapse, - hdr=hdr, - not_hdr=not_hdr, - selfie=selfie, - not_selfie=not_selfie, - panorama=panorama, - not_panorama=not_panorama, - has_raw=has_raw, - place=place, - no_place=no_place, - label=label, - deleted=deleted, - deleted_only=deleted_only, - has_comment=has_comment, - no_comment=no_comment, - has_likes=has_likes, - no_likes=no_likes, - is_reference=is_reference, - ) - - # below needed for to make CliRunner work for testing - cli_json = cli_obj.json if cli_obj is not None else None - print_photo_info(photos, cli_json or json_) - - -def print_photo_info(photos, json=False): - dump = [] - if json: - for p in photos: - dump.append(p.json()) - click.echo(f"[{', '.join(dump)}]") - else: - # dump as CSV - csv_writer = csv.writer( - sys.stdout, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL - ) - # add headers - dump.append( - [ - "uuid", - "filename", - "original_filename", - "date", - "description", - "title", - "keywords", - "albums", - "persons", - "path", - "ismissing", - "hasadjustments", - "external_edit", - "favorite", - "hidden", - "shared", - "latitude", - "longitude", - "path_edited", - "isphoto", - "ismovie", - "uti", - "burst", - "live_photo", - "path_live_photo", - "iscloudasset", - "incloud", - "date_modified", - "portrait", - "screenshot", - "slow_mo", - "time_lapse", - "hdr", - "selfie", - "panorama", - "has_raw", - "uti_raw", - "path_raw", - "intrash", - ] - ) - for p in photos: - date_modified_iso = p.date_modified.isoformat() if p.date_modified else None - dump.append( - [ - p.uuid, - p.filename, - p.original_filename, - p.date.isoformat(), - p.description, - p.title, - ", ".join(p.keywords), - ", ".join(p.albums), - ", ".join(p.persons), - p.path, - p.ismissing, - p.hasadjustments, - p.external_edit, - p.favorite, - p.hidden, - p.shared, - p._latitude, - p._longitude, - p.path_edited, - p.isphoto, - p.ismovie, - p.uti, - p.burst, - p.live_photo, - p.path_live_photo, - p.iscloudasset, - p.incloud, - date_modified_iso, - p.portrait, - p.screenshot, - p.slow_mo, - p.time_lapse, - p.hdr, - p.selfie, - p.panorama, - p.has_raw, - p.uti_raw, - p.path_raw, - p.intrash, - ] - ) - for row in dump: - csv_writer.writerow(row) - - -def _query( - photosdb, - keyword=None, - person=None, - album=None, - folder=None, - uuid=None, - title=None, - no_title=None, - description=None, - no_description=None, - ignore_case=None, - edited=None, - external_edit=None, - favorite=None, - not_favorite=None, - hidden=None, - not_hidden=None, - missing=None, - not_missing=None, - shared=None, - not_shared=None, - isphoto=None, - ismovie=None, - uti=None, - burst=None, - not_burst=None, - live=None, - not_live=None, - cloudasset=None, - not_cloudasset=None, - incloud=None, - not_incloud=None, - from_date=None, - to_date=None, - portrait=None, - not_portrait=None, - screenshot=None, - not_screenshot=None, - slow_mo=None, - not_slow_mo=None, - time_lapse=None, - not_time_lapse=None, - hdr=None, - not_hdr=None, - selfie=None, - not_selfie=None, - panorama=None, - not_panorama=None, - has_raw=None, - place=None, - no_place=None, - label=None, - deleted=False, - deleted_only=False, - has_comment=False, - no_comment=False, - has_likes=False, - no_likes=False, - is_reference=False, -): - """Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands - - Args: - photosdb: PhotosDB object - """ - - if deleted or deleted_only: - photos = photosdb.photos( - uuid=uuid, - images=isphoto, - movies=ismovie, - from_date=from_date, - to_date=to_date, - intrash=True, - ) - else: - photos = [] - if not deleted_only: - photos += photosdb.photos( - uuid=uuid, - images=isphoto, - movies=ismovie, - from_date=from_date, - to_date=to_date, - ) - - person = normalize_unicode(person) - keyword = normalize_unicode(keyword) - album = normalize_unicode(album) - folder = normalize_unicode(folder) - title = normalize_unicode(title) - description = normalize_unicode(description) - place = normalize_unicode(place) - label = normalize_unicode(label) - - if album: - photos = get_photos_by_attribute(photos, "albums", album, ignore_case) - - if keyword: - photos = get_photos_by_attribute(photos, "keywords", keyword, ignore_case) - - if person: - photos = get_photos_by_attribute(photos, "persons", person, ignore_case) - - if label: - photos = get_photos_by_attribute(photos, "labels", label, ignore_case) - - if folder: - # search for photos in an album in folder - # finds photos that have albums whose top level folder matches folder - photo_list = [] - for f in folder: - photo_list.extend( - [ - p - for p in photos - if p.album_info - and f in [a.folder_names[0] for a in p.album_info if a.folder_names] - ] - ) - photos = photo_list - - if title: - # search title field for text - # if more than one, find photos with all title values in title - if ignore_case: - # case-insensitive - for t in title: - t = t.lower() - photos = [p for p in photos if p.title and t in p.title.lower()] - else: - for t in title: - photos = [p for p in photos if p.title and t in p.title] - elif no_title: - photos = [p for p in photos if not p.title] - - if description: - # search description field for text - # if more than one, find photos with all description values in description - if ignore_case: - # case-insensitive - for d in description: - d = d.lower() - photos = [ - p for p in photos if p.description and d in p.description.lower() - ] - else: - for d in description: - photos = [p for p in photos if p.description and d in p.description] - elif no_description: - photos = [p for p in photos if not p.description] - - if place: - # search place.names for text matching place - # if more than one place, find photos with all place values in description - if ignore_case: - # case-insensitive - for place_name in place: - place_name = place_name.lower() - photos = [ - p - for p in photos - if p.place - and any( - pname - for pname in p.place.names - if any( - pvalue for pvalue in pname if place_name in pvalue.lower() - ) - ) - ] - else: - for place_name in place: - photos = [ - p - for p in photos - if p.place - and any( - pname - for pname in p.place.names - if any(pvalue for pvalue in pname if place_name in pvalue) - ) - ] - elif no_place: - photos = [p for p in photos if not p.place] - - if edited: - photos = [p for p in photos if p.hasadjustments] - - if external_edit: - photos = [p for p in photos if p.external_edit] - - if favorite: - photos = [p for p in photos if p.favorite] - elif not_favorite: - photos = [p for p in photos if not p.favorite] - - if hidden: - photos = [p for p in photos if p.hidden] - elif not_hidden: - photos = [p for p in photos if not p.hidden] - - if missing: - photos = [p for p in photos if not p.path] - elif not_missing: - photos = [p for p in photos if p.path] - - if shared: - photos = [p for p in photos if p.shared] - elif not_shared: - photos = [p for p in photos if not p.shared] - - if shared: - photos = [p for p in photos if p.shared] - elif not_shared: - photos = [p for p in photos if not p.shared] - - if uti: - photos = [p for p in photos if uti in p.uti_original] - - if burst: - photos = [p for p in photos if p.burst] - elif not_burst: - photos = [p for p in photos if not p.burst] - - if live: - photos = [p for p in photos if p.live_photo] - elif not_live: - photos = [p for p in photos if not p.live_photo] - - if portrait: - photos = [p for p in photos if p.portrait] - elif not_portrait: - photos = [p for p in photos if not p.portrait] - - if screenshot: - photos = [p for p in photos if p.screenshot] - elif not_screenshot: - photos = [p for p in photos if not p.screenshot] - - if slow_mo: - photos = [p for p in photos if p.slow_mo] - elif not_slow_mo: - photos = [p for p in photos if not p.slow_mo] - - if time_lapse: - photos = [p for p in photos if p.time_lapse] - elif not_time_lapse: - photos = [p for p in photos if not p.time_lapse] - - if hdr: - photos = [p for p in photos if p.hdr] - elif not_hdr: - photos = [p for p in photos if not p.hdr] - - if selfie: - photos = [p for p in photos if p.selfie] - elif not_selfie: - photos = [p for p in photos if not p.selfie] - - if panorama: - photos = [p for p in photos if p.panorama] - elif not_panorama: - photos = [p for p in photos if not p.panorama] - - if cloudasset: - photos = [p for p in photos if p.iscloudasset] - elif not_cloudasset: - photos = [p for p in photos if not p.iscloudasset] - - if incloud: - photos = [p for p in photos if p.incloud] - elif not_incloud: - photos = [p for p in photos if not p.incloud] - - if has_raw: - photos = [p for p in photos if p.has_raw] - - if has_comment: - photos = [p for p in photos if p.comments] - elif no_comment: - photos = [p for p in photos if not p.comments] - - if has_likes: - photos = [p for p in photos if p.likes] - elif no_likes: - photos = [p for p in photos if not p.likes] - - if is_reference: - photos = [p for p in photos if p.isreference] - - return photos - - -def get_photos_by_attribute(photos, attribute, values, ignore_case): - """Search for photos based on values being in PhotoInfo.attribute - - Args: - photos: a list of PhotoInfo objects - attribute: str, name of PhotoInfo attribute to search (e.g. keywords, persons, etc) - values: list of values to search in property - ignore_case: ignore case when searching - - Returns: - list of PhotoInfo objects matching search criteria - """ - photos_search = [] - if ignore_case: - # case-insensitive - for x in values: - x = x.lower() - photos_search.extend( - p - for p in photos - if x in [attr.lower() for attr in getattr(p, attribute)] - ) - else: - for x in values: - photos_search.extend(p for p in photos if x in getattr(p, attribute)) - return photos_search - - -def export_photo( - photo=None, - dest=None, - verbose=None, - export_by_date=None, - sidecar=None, - sidecar_drop_ext=False, - update=None, - ignore_signature=None, - export_as_hardlink=None, - overwrite=None, - export_edited=None, - skip_original_if_edited=None, - original_name=None, - export_live=None, - download_missing=None, - exiftool=None, - exiftool_merge_keywords=False, - exiftool_merge_persons=False, - directory=None, - filename_template=None, - export_raw=None, - album_keyword=None, - person_keyword=None, - keyword_template=None, - description_template=None, - export_db=None, - fileutil=FileUtil, - dry_run=None, - touch_file=None, - edited_suffix="_edited", - original_suffix="", - use_photos_export=False, - convert_to_jpeg=False, - jpeg_quality=1.0, - ignore_date_modified=False, - use_photokit=False, - exiftool_option=None, - strip=False, - jpeg_ext=None, -): - """Helper function for export that does the actual export - - Args: - photo: PhotoInfo object - dest: destination path as string - verbose: boolean; print verbose output - export_by_date: boolean; create export folder in form dest/YYYY/MM/DD - sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export - sidecar_drop_ext: boolean; if True, drops photo extension from sidecar name - export_as_hardlink: boolean; hardlink files instead of copying them - overwrite: boolean; overwrite dest file if it already exists - original_name: boolean; use original filename instead of current filename - export_live: boolean; also export live video component if photo is a live photo - live video will have same name as photo but with .mov extension - download_missing: attempt download of missing iCloud photos - exiftool: use exiftool to write EXIF metadata directly to exported photo - directory: template used to determine output directory - filename_template: template use to determine output file - export_raw: boolean; if True exports raw image associate with the photo - export_edited: boolean; if True exports edited version of photo if there is one - skip_original_if_edited: boolean; if True does not export original if photo has been edited - album_keyword: boolean; if True, exports album names as keywords in metadata - person_keyword: boolean; if True, exports person names as keywords in metadata - keyword_template: list of strings; if provided use rendered template strings as keywords - description_template: string; optional template string that will be rendered for use as photo description - export_db: export database instance compatible with ExportDB_ABC - fileutil: file util class compatible with FileUtilABC - dry_run: boolean; if True, doesn't actually export or update any files - touch_file: boolean; sets file's modification time to match photo date - use_photos_export: boolean; if True forces the use of AppleScript to export even if photo not missing - convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg - jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression. - ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set - exiftool_option: optional list flags (e.g. ["-m", "-F"]) to pass to exiftool - exiftool_merge_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool) - exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool) - jpeg_ext: if not None, specify the extension to use for all JPEG images on export - - Returns: - list of path(s) of exported photo or None if photo was missing - - Raises: - ValueError on invalid filename_template - """ - global VERBOSE - VERBOSE = bool(verbose) - - results = ExportResults() - - export_original = not (skip_original_if_edited and photo.hasadjustments) - - # can't export edited if photo doesn't have edited versions - export_edited = export_edited if photo.hasadjustments else False - - # slow_mo photos will always have hasadjustments=True even if not edited - if photo.hasadjustments and photo.path_edited is None: - if photo.slow_mo: - export_original = True - export_edited = False - elif not download_missing: - # requested edited version but it's missing, download original - export_original = True - export_edited = False - verbose_( - f"Edited file for {photo.original_filename} is missing, exporting original" - ) - - # check for missing photos before downloading - missing_original = False - missing_edited = False - if download_missing: - if ( - (photo.ismissing or photo.path is None) - and not photo.iscloudasset - and not photo.incloud - ): - missing_original = True - if ( - photo.hasadjustments - and photo.path_edited is None - and not photo.iscloudasset - and not photo.incloud - ): - missing_edited = True - else: - if photo.ismissing or photo.path is None: - missing_original = True - if photo.hasadjustments and photo.path_edited is None: - missing_edited = True - - filenames = get_filenames_from_template( - photo, filename_template, original_name, strip=strip - ) - for filename in filenames: - rendered_suffix = "" - if original_suffix: - try: - rendered_suffix, unmatched = photo.render_template( - original_suffix, filename=True, strip=strip - ) - except ValueError: - raise click.BadOptionUsage( - "original_suffix", - f"Invalid template for --original-suffix '{original_suffix}'", - ) - if not rendered_suffix or unmatched: - raise click.BadOptionUsage( - "original_suffix", - f"Invalid template for --original-suffix '{original_suffix}': results={rendered_suffix} unmatched={unmatched}", - ) - if len(rendered_suffix) > 1: - raise click.BadOptionUsage( - "original_suffix", - f"Invalid template for --original-suffix: may not use multi-valued templates: '{original_suffix}': results={rendered_suffix}", - ) - rendered_suffix = rendered_suffix[0] - - original_filename = pathlib.Path(filename) - file_ext = ( - "." + jpeg_ext - if jpeg_ext and (photo.uti == "public.jpeg" or convert_to_jpeg) - else ".jpeg" - if convert_to_jpeg and photo.uti != "public.jpeg" - else original_filename.suffix - ) - original_filename = ( - original_filename.parent - / f"{original_filename.stem}{rendered_suffix}{file_ext}" - ) - original_filename = str(original_filename) - - verbose_( - f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename}" - ) - - dest_paths = get_dirnames_from_template( - photo, directory, export_by_date, dest, dry_run, strip=strip - ) - - sidecar = [s.lower() for s in sidecar] - sidecar_flags = 0 - if "json" in sidecar: - sidecar_flags |= SIDECAR_JSON - if "xmp" in sidecar: - sidecar_flags |= SIDECAR_XMP - if "exiftool" in sidecar: - sidecar_flags |= SIDECAR_EXIFTOOL - - # if download_missing and the photo is missing or path doesn't exist, - # try to download with Photos - use_photos_export = use_photos_export or ( - download_missing - and ( - photo.ismissing - or photo.path is None - or (export_edited and photo.path_edited is None) - ) - ) - - # export the photo to each path in dest_paths - for dest_path in dest_paths: - # TODO: if --skip-original-if-edited, it's possible edited version is on disk but - # original is missing, in which case we should download the edited version - if export_original: - if missing_original: - space = " " if not verbose else "" - verbose_( - f"{space}Skipping missing photo {photo.original_filename} ({photo.uuid})" - ) - results.missing.append( - str(pathlib.Path(dest_path) / original_filename) - ) - elif photo.intrash and (not photo.path or use_photos_export): - # skip deleted files if they're missing or using use_photos_export - # as AppleScript/PhotoKit cannot export deleted photos - space = " " if not verbose else "" - verbose_( - f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})" - ) - results.missing.append( - str(pathlib.Path(dest_path) / original_filename) - ) - else: - try: - export_results = photo.export2( - dest_path, - original_filename, - sidecar=sidecar_flags, - sidecar_drop_ext=sidecar_drop_ext, - live_photo=export_live, - raw_photo=export_raw, - export_as_hardlink=export_as_hardlink, - overwrite=overwrite, - use_photos_export=use_photos_export, - exiftool=exiftool, - merge_exif_keywords=exiftool_merge_keywords, - merge_exif_persons=exiftool_merge_persons, - use_albums_as_keywords=album_keyword, - use_persons_as_keywords=person_keyword, - keyword_template=keyword_template, - description_template=description_template, - update=update, - ignore_signature=ignore_signature, - export_db=export_db, - fileutil=fileutil, - dry_run=dry_run, - touch_file=touch_file, - convert_to_jpeg=convert_to_jpeg, - jpeg_quality=jpeg_quality, - ignore_date_modified=ignore_date_modified, - use_photokit=use_photokit, - verbose=verbose_, - exiftool_flags=exiftool_option, - jpeg_ext=jpeg_ext, - ) - results += export_results - for warning_ in export_results.exiftool_warning: - verbose_( - f"exiftool warning for file {warning_[0]}: {warning_[1]}" - ) - for error_ in export_results.exiftool_error: - click.echo( - click.style( - f"exiftool error for file {error_[0]}: {error_[1]}", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - for error_ in export_results.error: - click.echo( - click.style( - f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - - except Exception as e: - click.echo( - click.style( - f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {original_filename}: {e}", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - results.error.append( - (str(pathlib.Path(dest) / original_filename), e) - ) - else: - verbose_(f"Skipping original version of {photo.original_filename}") - - # if export-edited, also export the edited version - # verify the photo has adjustments and valid path to avoid raising an exception - if export_edited and photo.hasadjustments: - edited_filename = pathlib.Path(filename) - edited_ext = ( - "." + jpeg_ext - if jpeg_ext and photo.uti_edited == "public.jpeg" - else "." + get_preferred_uti_extension(photo.uti_edited) - if photo.uti_edited - else pathlib.Path(photo.path_edited).suffix - if photo.path_edited - else pathlib.Path(photo.filename).suffix - ) - # Big Sur uses .heic for some edited photos so need to check - # if extension isn't jpeg/jpg and using --convert-to-jpeg - if convert_to_jpeg and edited_ext.lower() not in [".jpg", ".jpeg"]: - edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg" - - if edited_suffix: - try: - rendered_suffix, unmatched = photo.render_template( - edited_suffix, filename=True, strip=strip - ) - except ValueError: - raise click.BadOptionUsage( - "edited_suffix", - f"Invalid template for --edited-suffix '{edited_suffix}'", - ) - if not rendered_suffix or unmatched: - raise click.BadOptionUsage( - "edited_suffix", - f"Invalid template for --edited-suffix '{edited_suffix}': results={rendered_suffix} unmatched={unmatched}", - ) - if len(rendered_suffix) > 1: - raise click.BadOptionUsage( - "edited_suffix", - f"Invalid template for --edited-suffix: may not use multi-valued templates: '{edited_suffix}': results={rendered_suffix}", - ) - rendered_suffix = rendered_suffix[0] - - edited_filename = ( - f"{edited_filename.stem}{rendered_suffix}{edited_ext}" - ) - else: - edited_filename = f"{edited_filename.stem}{edited_ext}" - - verbose_( - f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}" - ) - if missing_edited: - space = " " if not verbose else "" - verbose_( - f"{space}Skipping missing edited photo for {edited_filename}" - ) - results.missing.append( - str(pathlib.Path(dest_path) / edited_filename) - ) - elif photo.intrash and (not photo.path_edited or use_photos_export): - # skip deleted files if they're missing or using use_photos_export - # as AppleScript/PhotoKit cannot export deleted photos - space = " " if not verbose else "" - verbose_( - f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})" - ) - results.missing.append( - str(pathlib.Path(dest_path) / edited_filename) - ) - - else: - try: - export_results_edited = photo.export2( - dest_path, - edited_filename, - sidecar=sidecar_flags, - sidecar_drop_ext=sidecar_drop_ext, - export_as_hardlink=export_as_hardlink, - overwrite=overwrite, - edited=True, - use_photos_export=use_photos_export, - exiftool=exiftool, - merge_exif_keywords=exiftool_merge_keywords, - merge_exif_persons=exiftool_merge_persons, - use_albums_as_keywords=album_keyword, - use_persons_as_keywords=person_keyword, - keyword_template=keyword_template, - description_template=description_template, - update=update, - ignore_signature=ignore_signature, - export_db=export_db, - fileutil=fileutil, - dry_run=dry_run, - touch_file=touch_file, - convert_to_jpeg=convert_to_jpeg, - jpeg_quality=jpeg_quality, - ignore_date_modified=ignore_date_modified, - use_photokit=use_photokit, - verbose=verbose_, - exiftool_flags=exiftool_option, - jpeg_ext=jpeg_ext, - ) - results += export_results_edited - for warning_ in export_results_edited.exiftool_warning: - verbose_( - f"exiftool warning for file {warning_[0]}: {warning_[1]}" - ) - for error_ in export_results_edited.exiftool_error: - click.echo( - click.style( - f"exiftool error for file {error_[0]}: {error_[1]}", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - for error_ in export_results_edited.error: - click.echo( - click.style( - f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - except Exception as e: - click.echo( - click.style( - f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) {filename} as {edited_filename}: {e}", - fg=CLI_COLOR_ERROR, - ), - err=True, - ) - results.error.append( - (str(pathlib.Path(dest) / edited_filename), e) - ) - - if verbose: - if update: - for new in results.new: - verbose_(f"Exported new file {new}") - for updated in results.updated: - verbose_(f"Exported updated file {updated}") - for skipped in results.skipped: - verbose_(f"Skipped up to date file {skipped}") - else: - for exported in results.exported: - verbose_(f"Exported {exported}") - for touched in results.touched: - verbose_(f"Touched date on file {touched}") - - return results - - -def get_filenames_from_template(photo, filename_template, original_name, strip=False): - """get list of export filenames for a photo - - Args: - photo: a PhotoInfo instance - filename_template: a PhotoTemplate template string, may be None - original_name: boolean; if True, use photo's original filename instead of current filename - - Returns: - list of filenames - - Raises: - click.BadOptionUsage if template is invalid - """ - if filename_template: - photo_ext = pathlib.Path(photo.original_filename).suffix - try: - filenames, unmatched = photo.render_template( - filename_template, path_sep="_", filename=True, strip=strip - ) - except ValueError: - raise click.BadOptionUsage( - "filename_template", f"Invalid template '{filename_template}'" - ) - if not filenames or unmatched: - raise click.BadOptionUsage( - "filename_template", - f"Invalid template '{filename_template}': results={filenames} unmatched={unmatched}", - ) - filenames = [f"{file_}{photo_ext}" for file_ in filenames] - else: - filenames = ( - [photo.original_filename] - if (original_name and (photo.original_filename is not None)) - else [photo.filename] - ) - - filenames = [sanitize_filename(filename) for filename in filenames] - return filenames - - -def get_dirnames_from_template( - photo, directory, export_by_date, dest, dry_run, strip=False -): - """get list of directories to export a photo into, creates directories if they don't exist - - Args: - photo: a PhotoInstance object - directory: a PhotoTemplate template string, may be None - export_by_date: boolean; if True, creates output directories in form YYYY-MM-DD - dest: top-level destination directory - dry_run: boolean; if True, runs in dry-run mode and does not create output directories - - Returns: - list of export directories - - Raises: - click.BadOptionUsage if template is invalid - """ - - if export_by_date: - date_created = DateTimeFormatter(photo.date) - dest_path = os.path.join( - dest, date_created.year, date_created.mm, date_created.dd - ) - if not (dry_run or os.path.isdir(dest_path)): - os.makedirs(dest_path) - dest_paths = [dest_path] - elif directory: - # got a directory template, render it and check results are valid - try: - dirnames, unmatched = photo.render_template( - directory, dirname=True, strip=strip - ) - except ValueError: - raise click.BadOptionUsage("directory", f"Invalid template '{directory}'") - if not dirnames or unmatched: - raise click.BadOptionUsage( - "directory", - f"Invalid template '{directory}': results={dirnames} unmatched={unmatched}", - ) - - dest_paths = [] - for dirname in dirnames: - dirname = sanitize_filepath(dirname) - dest_path = os.path.join(dest, dirname) - if not is_valid_filepath(dest_path): - raise ValueError(f"Invalid file path: '{dest_path}'") - if not dry_run and not os.path.isdir(dest_path): - os.makedirs(dest_path) - dest_paths.append(dest_path) - else: - dest_paths = [dest] - return dest_paths - - -def find_files_in_branch(pathname, filename): - """Search a directory branch to find file(s) named filename - The branch searched includes all folders below pathname and - the parent tree of pathname but not pathname itself. - - e.g. find filename in children folders and parent folders - - Args: - pathname: str, full path of directory to search - filename: str, filename to search for - - Returns: list of full paths to any matching files - """ - - pathname = pathlib.Path(pathname).resolve() - files = [] - - # walk down the tree - for root, _, filenames in os.walk(pathname): - # for directory in directories: - # print(os.path.join(root, directory)) - for fname in filenames: - if fname == filename and pathlib.Path(root) != pathname: - files.append(os.path.join(root, fname)) - - # walk up the tree - path = pathlib.Path(pathname) - for root in path.parents: - filenames = os.listdir(root) - for fname in filenames: - filepath = os.path.join(root, fname) - if fname == filename and os.path.isfile(filepath): - files.append(os.path.join(root, fname)) - - return files - - -def load_uuid_from_file(filename): - """Load UUIDs from file. Does not validate UUIDs. - Format is 1 UUID per line, any line beginning with # is ignored. - Whitespace is stripped. - - Arguments: - filename: file name of the file containing UUIDs - - Returns: - list of UUIDs or empty list of no UUIDs in file - - Raises: - FileNotFoundError if file does not exist - """ - - if not pathlib.Path(filename).is_file(): - raise FileNotFoundError(f"Could not find file {filename}") - - uuid = [] - with open(filename, "r") as uuid_file: - for line in uuid_file: - line = line.strip() - if len(line) and line[0] != "#": - uuid.append(line) - return uuid - - -def write_export_report(report_file, results): - - """write CSV report with results from export - - Args: - report_file: path to report file - results: ExportResults object - """ - - # Collect results for reporting - # TODO: pull this in a separate write_report function - all_results = { - result: { - "filename": result, - "exported": 0, - "new": 0, - "updated": 0, - "skipped": 0, - "exif_updated": 0, - "touched": 0, - "converted_to_jpeg": 0, - "sidecar_xmp": 0, - "sidecar_json": 0, - "sidecar_exiftool": 0, - "missing": 0, - "error": "", - "exiftool_warning": "", - "exiftool_error": "", - "extended_attributes_written": 0, - "extended_attributes_skipped": 0, - } - for result in results.all_files() - } - - for result in results.exported: - all_results[result]["exported"] = 1 - - for result in results.new: - all_results[result]["new"] = 1 - - for result in results.updated: - all_results[result]["updated"] = 1 - - for result in results.skipped: - all_results[result]["skipped"] = 1 - - for result in results.exif_updated: - all_results[result]["exif_updated"] = 1 - - for result in results.touched: - all_results[result]["touched"] = 1 - - for result in results.converted_to_jpeg: - all_results[result]["converted_to_jpeg"] = 1 - - for result in results.sidecar_xmp_written: - all_results[result]["sidecar_xmp"] = 1 - all_results[result]["exported"] = 1 - - for result in results.sidecar_xmp_skipped: - all_results[result]["sidecar_xmp"] = 1 - all_results[result]["skipped"] = 1 - - for result in results.sidecar_json_written: - all_results[result]["sidecar_json"] = 1 - all_results[result]["exported"] = 1 - - for result in results.sidecar_json_skipped: - all_results[result]["sidecar_json"] = 1 - all_results[result]["skipped"] = 1 - - for result in results.sidecar_exiftool_written: - all_results[result]["sidecar_exiftool"] = 1 - all_results[result]["exported"] = 1 - - for result in results.sidecar_exiftool_skipped: - all_results[result]["sidecar_exiftool"] = 1 - all_results[result]["skipped"] = 1 - - for result in results.missing: - all_results[result]["missing"] = 1 - - for result in results.error: - all_results[result[0]]["error"] = result[1] - - for result in results.exiftool_warning: - all_results[result[0]]["exiftool_warning"] = result[1] - - for result in results.exiftool_error: - all_results[result[0]]["exiftool_error"] = result[1] - - for result in results.xattr_written: - all_results[result]["extended_attributes_written"] = 1 - - for result in results.xattr_skipped: - all_results[result]["extended_attributes_skipped"] = 1 - - report_columns = [ - "filename", - "exported", - "new", - "updated", - "skipped", - "exif_updated", - "touched", - "converted_to_jpeg", - "sidecar_xmp", - "sidecar_json", - "sidecar_exiftool", - "missing", - "error", - "exiftool_warning", - "exiftool_error", - "extended_attributes_written", - "extended_attributes_skipped", - ] - - try: - with open(report_file, "w") as csvfile: - writer = csv.DictWriter(csvfile, fieldnames=report_columns) - writer.writeheader() - for data in [result for result in all_results.values()]: - writer.writerow(data) - except IOError: - click.echo( - click.style("Could not open output file for writing", fg=CLI_COLOR_ERROR), - err=True, - ) - raise click.Abort() - - -def cleanup_files(dest_path, files_to_keep, fileutil): - """cleanup dest_path by deleting and files and empty directories - not in files_to_keep - - Args: - dest_path: path to directory to clean - files_to_keep: list of full file paths to keep (not delete) - fileutile: FileUtil object - - Returns: - tuple of (number of files deleted, number of directories deleted) - """ - keepers = {filename.lower(): 1 for filename in files_to_keep} - - deleted_files = 0 - for p in pathlib.Path(dest_path).rglob("*"): - path = str(p).lower() - if p.is_file() and path not in keepers: - verbose_(f"Deleting {p}") - fileutil.unlink(p) - deleted_files += 1 - - # delete empty directories - deleted_dirs = 0 - for p in pathlib.Path(dest_path).rglob("*"): - path = str(p).lower() - # if directory and directory is empty - if p.is_dir() and not next(p.iterdir(), False): - verbose_(f"Deleting empty directory {p}") - fileutil.rmdir(p) - deleted_dirs += 1 - - return (deleted_files, deleted_dirs) - - -def write_finder_tags( - photo, - files, - keywords=False, - keyword_template=None, - album_keyword=None, - person_keyword=None, - exiftool_merge_keywords=None, - finder_tag_template=None, - strip=False, -): - """Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written - - Args: - photo: a PhotoInfo object - files: list of file paths to write Finder tags to - keywords: if True, sets Finder tags to all keywords including any evaluated from keyword_template, album_keyword, person_keyword, exiftool_merge_keywords - keyword_template: list of keyword templates to evaluate for determining keywords - album_keyword: if True, use album names as keywords - person_keyword: if True, use person in image as keywords - exiftool_merge_keywords: if True, include any keywords in the exif data of the source image as keywords - finder_tag_template: list of templates to evaluate for determining Finder tags - - Returns: - (list of file paths that were updated with new Finder tags, list of file paths skipped because Finder tags didn't need updating) - """ - - tags = [] - written = [] - skipped = [] - if keywords: - # match whatever keywords would've been used in --exiftool or --sidecar - exif = photo._exiftool_dict( - use_albums_as_keywords=album_keyword, - use_persons_as_keywords=person_keyword, - keyword_template=keyword_template, - merge_exif_keywords=exiftool_merge_keywords, - ) - try: - if exif["IPTC:Keywords"]: - tags.extend(exif["IPTC:Keywords"]) - except KeyError: - pass - - if finder_tag_template: - rendered_tags = [] - for template_str in finder_tag_template: - try: - rendered, unmatched = photo.render_template( - template_str, - none_str=_OSXPHOTOS_NONE_SENTINEL, - path_sep="/", - strip=strip, - ) - except ValueError: - raise click.BadOptionUsage( - "finder_tag_template", - f"Invalid template for --finder-tag-template': {template_str}", - ) - - if unmatched: - click.echo( - click.style( - f"Warning: unmatched template substitution for template: {template_str} {unmatched}", - fg=CLI_COLOR_WARNING, - ), - err=True, - ) - rendered_tags.extend(rendered) - - # filter out any template values that didn't match by looking for sentinel - rendered_tags = [ - tag for tag in rendered_tags if _OSXPHOTOS_NONE_SENTINEL not in tag - ] - tags.extend(rendered_tags) - - tags = [osxmetadata.Tag(tag) for tag in set(tags)] - for f in files: - md = osxmetadata.OSXMetaData(f) - if sorted(md.tags) != sorted(tags): - verbose_(f"Writing Finder tags to {f}") - md.tags = tags - written.append(f) - else: - verbose_(f"Skipping Finder tags for {f}: nothing to do") - skipped.append(f) - - return (written, skipped) - - -def write_extended_attributes(photo, files, xattr_template, strip=False): - """ Writes extended attributes to exported files - - Args: - photo: a PhotoInfo object - xattr_template: list of tuples: (attribute name, attribute template) - - Returns: - tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating) - """ - - attributes = {} - for xattr, template_str in xattr_template: - try: - rendered, unmatched = photo.render_template( - template_str, - none_str=_OSXPHOTOS_NONE_SENTINEL, - path_sep="/", - strip=strip, - ) - except ValueError: - raise click.BadOptionUsage( - "xattr_template", - f"Invalid template for --xattr-template': {template_str}", - ) - if unmatched: - click.echo( - click.style( - f"Warning: unmatched template substitution for template: {template_str} {unmatched}", - fg=CLI_COLOR_WARNING, - ), - err=True, - ) - # filter out any template values that didn't match by looking for sentinel - rendered = [ - value for value in rendered if _OSXPHOTOS_NONE_SENTINEL not in value - ] - try: - attributes[xattr].extend(rendered) - except KeyError: - attributes[xattr] = rendered - - written = set() - skipped = set() - for f in files: - md = osxmetadata.OSXMetaData(f) - for attr, value in attributes.items(): - islist = osxmetadata.ATTRIBUTES[attr].list - if value: - value = ", ".join(value) if not islist else sorted(value) - file_value = md.get_attribute(attr) - - if file_value and islist: - file_value = sorted(file_value) - - if (not file_value and not value) or file_value == value: - # if both not set or both equal, nothing to do - # get_attribute returns None if not set and value will be [] if not set so can't directly compare - verbose_(f"Skipping extended attribute {attr} for {f}: nothing to do") - skipped.add(f) - else: - verbose_(f"Writing extended attribute {attr} to {f}") - md.set_attribute(attr, value) - written.add(f) - - return list(written), [f for f in skipped if f not in written] - - -@cli.command(hidden=True) -@DB_OPTION -@DB_ARGUMENT -@click.option( - "--dump", - metavar="ATTR", - help="Name of PhotosDB attribute to print; " - + "can also use albums, persons, keywords, photos to dump related attributes.", - multiple=True, -) -@click.option( - "--uuid", - metavar="UUID", - help="Use with '--dump photos' to dump only certain UUIDs", - multiple=True, -) -@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.") -@click.pass_obj -@click.pass_context -def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose): - """ Print out debug info """ - - global VERBOSE - VERBOSE = bool(verbose) - - db = get_photos_db(*photos_library, db, cli_obj.db) - if db is None: - click.echo(cli.commands["debug-dump"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - start_t = time.perf_counter() - print(f"Opening database: {db}") - photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_) - stop_t = time.perf_counter() - print(f"Done; took {(stop_t-start_t):.2f} seconds") - - for attr in dump: - if attr == "albums": - print("_dbalbums_album:") - pprint.pprint(photosdb._dbalbums_album) - print("_dbalbums_uuid:") - pprint.pprint(photosdb._dbalbums_uuid) - print("_dbalbum_details:") - pprint.pprint(photosdb._dbalbum_details) - print("_dbalbum_folders:") - pprint.pprint(photosdb._dbalbum_folders) - print("_dbfolder_details:") - pprint.pprint(photosdb._dbfolder_details) - elif attr == "keywords": - print("_dbkeywords_keyword:") - pprint.pprint(photosdb._dbkeywords_keyword) - print("_dbkeywords_uuid:") - pprint.pprint(photosdb._dbkeywords_uuid) - elif attr == "persons": - print("_dbfaces_uuid:") - pprint.pprint(photosdb._dbfaces_uuid) - print("_dbfaces_pk:") - pprint.pprint(photosdb._dbfaces_pk) - print("_dbpersons_pk:") - pprint.pprint(photosdb._dbpersons_pk) - print("_dbpersons_fullname:") - pprint.pprint(photosdb._dbpersons_fullname) - elif attr == "photos": - if uuid: - for uuid_ in uuid: - print(f"_dbphotos['{uuid_}']:") - try: - pprint.pprint(photosdb._dbphotos[uuid_]) - except KeyError: - print(f"Did not find uuid {uuid_} in _dbphotos") - else: - print("_dbphotos:") - pprint.pprint(photosdb._dbphotos) - else: - try: - val = getattr(photosdb, attr) - print(f"{attr}:") - pprint.pprint(val) - except Exception: - print(f"Did not find attribute {attr} in PhotosDB") - - -@cli.command() -@DB_OPTION -@JSON_OPTION -@DB_ARGUMENT -@click.pass_obj -@click.pass_context -def keywords(ctx, cli_obj, db, json_, photos_library): - """ Print out keywords found in the Photos library. """ - - # below needed for to make CliRunner work for testing - cli_db = cli_obj.db if cli_obj is not None else None - db = get_photos_db(*photos_library, db, cli_db) - if db is None: - click.echo(cli.commands["keywords"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - photosdb = osxphotos.PhotosDB(dbfile=db) - keywords = {"keywords": photosdb.keywords_as_dict} - if json_ or cli_obj.json: - click.echo(json.dumps(keywords, ensure_ascii=False)) - else: - click.echo(yaml.dump(keywords, sort_keys=False, allow_unicode=True)) - - -@cli.command() -@DB_OPTION -@JSON_OPTION -@DB_ARGUMENT -@click.pass_obj -@click.pass_context -def albums(ctx, cli_obj, db, json_, photos_library): - """ Print out albums found in the Photos library. """ - - # below needed for to make CliRunner work for testing - cli_db = cli_obj.db if cli_obj is not None else None - db = get_photos_db(*photos_library, db, cli_db) - if db is None: - click.echo(cli.commands["albums"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - photosdb = osxphotos.PhotosDB(dbfile=db) - albums = {"albums": photosdb.albums_as_dict} - if photosdb.db_version > _PHOTOS_4_VERSION: - albums["shared albums"] = photosdb.albums_shared_as_dict - - if json_ or cli_obj.json: - click.echo(json.dumps(albums, ensure_ascii=False)) - else: - click.echo(yaml.dump(albums, sort_keys=False, allow_unicode=True)) - - -@cli.command() -@DB_OPTION -@JSON_OPTION -@DB_ARGUMENT -@click.pass_obj -@click.pass_context -def persons(ctx, cli_obj, db, json_, photos_library): - """ Print out persons (faces) found in the Photos library. """ - - # below needed for to make CliRunner work for testing - cli_db = cli_obj.db if cli_obj is not None else None - db = get_photos_db(*photos_library, db, cli_db) - if db is None: - click.echo(cli.commands["persons"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - photosdb = osxphotos.PhotosDB(dbfile=db) - persons = {"persons": photosdb.persons_as_dict} - if json_ or cli_obj.json: - click.echo(json.dumps(persons, ensure_ascii=False)) - else: - click.echo(yaml.dump(persons, sort_keys=False, allow_unicode=True)) - - -@cli.command() -@DB_OPTION -@JSON_OPTION -@DB_ARGUMENT -@click.pass_obj -@click.pass_context -def labels(ctx, cli_obj, db, json_, photos_library): - """ Print out image classification labels found in the Photos library. """ - - # below needed for to make CliRunner work for testing - cli_db = cli_obj.db if cli_obj is not None else None - db = get_photos_db(*photos_library, db, cli_db) - if db is None: - click.echo(cli.commands["labels"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - photosdb = osxphotos.PhotosDB(dbfile=db) - labels = {"labels": photosdb.labels_as_dict} - if json_ or cli_obj.json: - click.echo(json.dumps(labels, ensure_ascii=False)) - else: - click.echo(yaml.dump(labels, sort_keys=False, allow_unicode=True)) - - -@cli.command() -@DB_OPTION -@JSON_OPTION -@DB_ARGUMENT -@click.pass_obj -@click.pass_context -def info(ctx, cli_obj, db, json_, photos_library): - """ Print out descriptive info of the Photos library database. """ - - db = get_photos_db(*photos_library, db, cli_obj.db) - if db is None: - click.echo(cli.commands["info"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - photosdb = osxphotos.PhotosDB(dbfile=db) - info = {"database_path": photosdb.db_path, "database_version": photosdb.db_version} - photos = photosdb.photos(movies=False) - not_shared_photos = [p for p in photos if not p.shared] - info["photo_count"] = len(not_shared_photos) - - hidden = [p for p in photos if p.hidden] - info["hidden_photo_count"] = len(hidden) - - movies = photosdb.photos(images=False, movies=True) - not_shared_movies = [p for p in movies if not p.shared] - info["movie_count"] = len(not_shared_movies) - - if photosdb.db_version > _PHOTOS_4_VERSION: - shared_photos = [p for p in photos if p.shared] - info["shared_photo_count"] = len(shared_photos) - - shared_movies = [p for p in movies if p.shared] - info["shared_movie_count"] = len(shared_movies) - - keywords = photosdb.keywords_as_dict - info["keywords_count"] = len(keywords) - info["keywords"] = keywords - - albums = photosdb.albums_as_dict - info["albums_count"] = len(albums) - info["albums"] = albums - - if photosdb.db_version > _PHOTOS_4_VERSION: - albums_shared = photosdb.albums_shared_as_dict - info["shared_albums_count"] = len(albums_shared) - info["shared_albums"] = albums_shared - - persons = photosdb.persons_as_dict - - info["persons_count"] = len(persons) - info["persons"] = persons - - if cli_obj.json or json_: - click.echo(json.dumps(info, ensure_ascii=False)) - else: - click.echo(yaml.dump(info, sort_keys=False, allow_unicode=True)) - - -@cli.command() -@DB_OPTION -@JSON_OPTION -@DB_ARGUMENT -@click.pass_obj -@click.pass_context -def places(ctx, cli_obj, db, json_, photos_library): - """ Print out places found in the Photos library. """ - - # below needed for to make CliRunner work for testing - cli_db = cli_obj.db if cli_obj is not None else None - db = get_photos_db(*photos_library, db, cli_db) - if db is None: - click.echo(cli.commands["places"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - photosdb = osxphotos.PhotosDB(dbfile=db) - place_names = {} - for photo in photosdb.photos(movies=True): - if photo.place: - try: - place_names[photo.place.name] += 1 - except Exception: - place_names[photo.place.name] = 1 - else: - try: - place_names[_UNKNOWN_PLACE] += 1 - except Exception: - place_names[_UNKNOWN_PLACE] = 1 - - # sort by place count - places = { - "places": { - name: place_names[name] - for name in sorted( - place_names.keys(), key=lambda key: place_names[key], reverse=True - ) - } - } - - # below needed for to make CliRunner work for testing - cli_json = cli_obj.json if cli_obj is not None else None - if json_ or cli_json: - click.echo(json.dumps(places, ensure_ascii=False)) - else: - click.echo(yaml.dump(places, sort_keys=False, allow_unicode=True)) - - -@cli.command() -@DB_OPTION -@JSON_OPTION -@deleted_options -@DB_ARGUMENT -@click.pass_obj -@click.pass_context -def dump(ctx, cli_obj, db, json_, deleted, deleted_only, photos_library): - """ Print list of all photos & associated info from the Photos library. """ - - db = get_photos_db(*photos_library, db, cli_obj.db) - if db is None: - click.echo(cli.commands["dump"].get_help(ctx), err=True) - click.echo("\n\nLocated the following Photos library databases: ", err=True) - _list_libraries() - return - - # check exclusive options - if deleted and deleted_only: - click.echo("Incompatible dump options", err=True) - click.echo(cli.commands["dump"].get_help(ctx), err=True) - return - - photosdb = osxphotos.PhotosDB(dbfile=db) - if deleted or deleted_only: - photos = photosdb.photos(movies=True, intrash=True) - else: - photos = [] - if not deleted_only: - photos += photosdb.photos(movies=True) - - print_photo_info(photos, json_ or cli_obj.json) - - -@cli.command(name="list") -@JSON_OPTION -@click.pass_obj -@click.pass_context -def list_libraries(ctx, cli_obj, json_): - """ Print list of Photos libraries found on the system. """ - - # implemented in _list_libraries so it can be called by other CLI functions - # without errors due to passing ctx and cli_obj - _list_libraries(json_=json_ or cli_obj.json, error=False) - - -def _list_libraries(json_=False, error=True): - """Print list of Photos libraries found on the system. - If json_ == True, print output as JSON (default = False)""" - - photo_libs = osxphotos.utils.list_photo_libraries() - sys_lib = osxphotos.utils.get_system_library_path() - last_lib = osxphotos.utils.get_last_library_path() - - if json_: - libs = { - "photo_libraries": photo_libs, - "system_library": sys_lib, - "last_library": last_lib, - } - click.echo(json.dumps(libs, ensure_ascii=False)) - else: - last_lib_flag = sys_lib_flag = False - - for lib in photo_libs: - if lib == sys_lib: - click.echo(f"(*)\t{lib}", err=error) - sys_lib_flag = True - elif lib == last_lib: - click.echo(f"(#)\t{lib}", err=error) - last_lib_flag = True - else: - click.echo(f"\t{lib}", err=error) - - if sys_lib_flag or last_lib_flag: - click.echo("\n", err=error) - if sys_lib_flag: - click.echo("(*)\tSystem Photos Library", err=error) - if last_lib_flag: - click.echo("(#)\tLast opened Photos Library", err=error) - - -@cli.command(name="about") -@click.pass_obj -@click.pass_context -def about(ctx, cli_obj): - """ Print information about osxphotos including license. """ - license = """ -MIT License - -Copyright (c) 2019-2021 Rhet Turnbull - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - click.echo(f"osxphotos, version {__version__}") - click.echo("") - click.echo(f"Source code available at: {OSXPHOTOS_URL}") - click.echo(license) +"""Command line interface for osxphotos """ +from .cli import cli if __name__ == "__main__": cli() # pylint: disable=no-value-for-parameter diff --git a/osxphotos/_constants.py b/osxphotos/_constants.py index 9b45b457..e237a4e5 100644 --- a/osxphotos/_constants.py +++ b/osxphotos/_constants.py @@ -206,3 +206,6 @@ EXTENDED_ATTRIBUTE_NAMES = [ "keywords", ] EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES] + +# name of export DB +OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db" \ No newline at end of file diff --git a/osxphotos/_version.py b/osxphotos/_version.py index dd8bccec..dfc9e411 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.40.3" +__version__ = "0.40.4" diff --git a/osxphotos/cli.py b/osxphotos/cli.py new file mode 100644 index 00000000..54fad796 --- /dev/null +++ b/osxphotos/cli.py @@ -0,0 +1,3522 @@ +"""Command line interface for osxphotos """ + +import csv +import datetime +import json +import os +import os.path +import pathlib +import pprint +import sys +import time +import unicodedata + +import click +import osxmetadata +import yaml + +import osxphotos + +from ._constants import ( + _EXIF_TOOL_URL, + _OSXPHOTOS_NONE_SENTINEL, + _PHOTOS_4_VERSION, + _UNKNOWN_PLACE, + CLI_COLOR_ERROR, + CLI_COLOR_WARNING, + DEFAULT_EDITED_SUFFIX, + DEFAULT_JPEG_QUALITY, + DEFAULT_ORIGINAL_SUFFIX, + EXTENDED_ATTRIBUTE_NAMES, + EXTENDED_ATTRIBUTE_NAMES_QUOTED, + OSXPHOTOS_EXPORT_DB, + OSXPHOTOS_URL, + SIDECAR_EXIFTOOL, + SIDECAR_JSON, + SIDECAR_XMP, + UNICODE_FORMAT, +) +from ._version import __version__ +from .cli_help import ExportCommand +from .configoptions import ( + ConfigOptions, + ConfigOptionsInvalidError, + ConfigOptionsLoadError, +) +from .datetime_formatter import DateTimeFormatter +from .exiftool import get_exiftool_path +from .export_db import ExportDB, ExportDBInMemory +from .fileutil import FileUtil, FileUtilNoOp +from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath +from .photoinfo import ExportResults +from .photokit import check_photokit_authorization, request_photokit_authorization +from .utils import get_preferred_uti_extension + +# global variable to control verbose output +# set via --verbose/-V +VERBOSE = False + + +def verbose_(*args, **kwargs): + """ print output if verbose flag set """ + if VERBOSE: + styled_args = [] + for arg in args: + if type(arg) == str: + if "error" in arg.lower(): + arg = click.style(arg, fg=CLI_COLOR_ERROR) + elif "warning" in arg.lower(): + arg = click.style(arg, fg=CLI_COLOR_WARNING) + styled_args.append(arg) + click.echo(*styled_args, **kwargs) + + +def normalize_unicode(value): + """ normalize unicode data """ + if value is not None: + if isinstance(value, tuple): + return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value) + elif isinstance(value, str): + return unicodedata.normalize(UNICODE_FORMAT, value) + else: + return value + else: + return None + + +def get_photos_db(*db_options): + """Return path to photos db, select first non-None db_options + If no db_options are non-None, try to find library to use in + the following order: + - last library opened + - system library + - ~/Pictures/Photos Library.photoslibrary + - failing above, returns None + """ + if db_options: + for db in db_options: + if db is not None: + return db + + # if get here, no valid database paths passed, so try to figure out which to use + db = osxphotos.utils.get_last_library_path() + if db is not None: + click.echo(f"Using last opened Photos library: {db}", err=True) + return db + + db = osxphotos.utils.get_system_library_path() + if db is not None: + click.echo(f"Using system Photos library: {db}", err=True) + return db + + db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary") + if os.path.isdir(db): + click.echo(f"Using Photos library: {db}", err=True) + return db + else: + return None + + +class DateTimeISO8601(click.ParamType): + + name = "DATETIME" + + def convert(self, value, param, ctx): + try: + return datetime.datetime.fromisoformat(value) + except Exception: + self.fail( + f"Invalid value for --{param.name}: invalid datetime format {value}. " + "Valid format: YYYY-MM-DD[*HH[:MM[:SS[.fff[fff]]]][+HH:MM[:SS[.ffffff]]]]" + ) + + +# Click CLI object & context settings +class CLI_Obj: + def __init__(self, db=None, json=False, debug=False): + if debug: + osxphotos._set_debug(True) + self.db = db + self.json = json + + +CTX_SETTINGS = dict(help_option_names=["-h", "--help"]) +DB_OPTION = click.option( + "--db", + required=False, + metavar="", + default=None, + help=( + "Specify Photos database path. " + "Path to Photos library/database can be specified using either --db " + "or directly as PHOTOS_LIBRARY positional argument. " + "If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library " + "to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary" + ), + type=click.Path(exists=True), +) + +DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True)) + +JSON_OPTION = click.option( + "--json", + "json_", + required=False, + is_flag=True, + default=False, + help="Print output in JSON format.", +) + + +def deleted_options(f): + o = click.option + options = [ + o( + "--deleted", + is_flag=True, + help="Include photos from the 'Recently Deleted' folder.", + ), + o( + "--deleted-only", + is_flag=True, + help="Include only photos from the 'Recently Deleted' folder.", + ), + ] + for o in options[::-1]: + f = o(f) + return f + + +def query_options(f): + o = click.option + options = [ + o( + "--keyword", + metavar="KEYWORD", + default=None, + multiple=True, + help="Search for photos with keyword KEYWORD. " + 'If more than one keyword, treated as "OR", e.g. find photos matching any keyword', + ), + o( + "--person", + metavar="PERSON", + default=None, + multiple=True, + help="Search for photos with person PERSON. " + 'If more than one person, treated as "OR", e.g. find photos matching any person', + ), + o( + "--album", + metavar="ALBUM", + default=None, + multiple=True, + help="Search for photos in album ALBUM. " + 'If more than one album, treated as "OR", e.g. find photos matching any album', + ), + o( + "--folder", + metavar="FOLDER", + default=None, + multiple=True, + help="Search for photos in an album in folder FOLDER. " + 'If more than one folder, treated as "OR", e.g. find photos in any FOLDER. ' + "Only searches top level folders (e.g. does not look at subfolders)", + ), + o( + "--uuid", + metavar="UUID", + default=None, + multiple=True, + help="Search for photos with UUID(s).", + ), + o( + "--uuid-from-file", + metavar="FILE", + default=None, + multiple=False, + help="Search for photos with UUID(s) loaded from FILE. " + "Format is a single UUID per line. Lines preceeded with # are ignored.", + type=click.Path(exists=True), + ), + o( + "--title", + metavar="TITLE", + default=None, + multiple=True, + help="Search for TITLE in title of photo.", + ), + o("--no-title", is_flag=True, help="Search for photos with no title."), + o( + "--description", + metavar="DESC", + default=None, + multiple=True, + help="Search for DESC in description of photo.", + ), + o( + "--no-description", + is_flag=True, + help="Search for photos with no description.", + ), + o( + "--place", + metavar="PLACE", + default=None, + multiple=True, + help="Search for PLACE in photo's reverse geolocation info", + ), + o( + "--no-place", + is_flag=True, + help="Search for photos with no associated place name info (no reverse geolocation info)", + ), + o( + "--label", + metavar="LABEL", + multiple=True, + help="Search for photos with image classification label LABEL (Photos 5 only). " + 'If more than one label, treated as "OR", e.g. find photos matching any label', + ), + o( + "--uti", + metavar="UTI", + default=None, + multiple=False, + help="Search for photos whose uniform type identifier (UTI) matches UTI", + ), + o( + "-i", + "--ignore-case", + is_flag=True, + help="Case insensitive search for title, description, place, keyword, person, or album.", + ), + o("--edited", is_flag=True, help="Search for photos that have been edited."), + o( + "--external-edit", + is_flag=True, + help="Search for photos edited in external editor.", + ), + o("--favorite", is_flag=True, help="Search for photos marked favorite."), + o( + "--not-favorite", + is_flag=True, + help="Search for photos not marked favorite.", + ), + o("--hidden", is_flag=True, help="Search for photos marked hidden."), + o("--not-hidden", is_flag=True, help="Search for photos not marked hidden."), + o( + "--shared", + is_flag=True, + help="Search for photos in shared iCloud album (Photos 5 only).", + ), + o( + "--not-shared", + is_flag=True, + help="Search for photos not in shared iCloud album (Photos 5 only).", + ), + o( + "--burst", + is_flag=True, + help="Search for photos that were taken in a burst.", + ), + o( + "--not-burst", + is_flag=True, + help="Search for photos that are not part of a burst.", + ), + o("--live", is_flag=True, help="Search for Apple live photos"), + o( + "--not-live", + is_flag=True, + help="Search for photos that are not Apple live photos.", + ), + o("--portrait", is_flag=True, help="Search for Apple portrait mode photos."), + o( + "--not-portrait", + is_flag=True, + help="Search for photos that are not Apple portrait mode photos.", + ), + o("--screenshot", is_flag=True, help="Search for screenshot photos."), + o( + "--not-screenshot", + is_flag=True, + help="Search for photos that are not screenshot photos.", + ), + o("--slow-mo", is_flag=True, help="Search for slow motion videos."), + o( + "--not-slow-mo", + is_flag=True, + help="Search for photos that are not slow motion videos.", + ), + o("--time-lapse", is_flag=True, help="Search for time lapse videos."), + o( + "--not-time-lapse", + is_flag=True, + help="Search for photos that are not time lapse videos.", + ), + o("--hdr", is_flag=True, help="Search for high dynamic range (HDR) photos."), + o("--not-hdr", is_flag=True, help="Search for photos that are not HDR photos."), + o( + "--selfie", + is_flag=True, + help="Search for selfies (photos taken with front-facing cameras).", + ), + o("--not-selfie", is_flag=True, help="Search for photos that are not selfies."), + o("--panorama", is_flag=True, help="Search for panorama photos."), + o( + "--not-panorama", + is_flag=True, + help="Search for photos that are not panoramas.", + ), + o( + "--has-raw", + is_flag=True, + help="Search for photos with both a jpeg and raw version", + ), + o( + "--only-movies", + is_flag=True, + help="Search only for movies (default searches both images and movies).", + ), + o( + "--only-photos", + is_flag=True, + help="Search only for photos/images (default searches both images and movies).", + ), + o( + "--from-date", + help="Search by start item date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601).", + type=DateTimeISO8601(), + ), + o( + "--to-date", + help="Search by end item date, e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601).", + type=DateTimeISO8601(), + ), + o("--has-comment", is_flag=True, help="Search for photos that have comments."), + o("--no-comment", is_flag=True, help="Search for photos with no comments."), + o("--has-likes", is_flag=True, help="Search for photos that have likes."), + o("--no-likes", is_flag=True, help="Search for photos with no likes."), + o( + "--is-reference", + is_flag=True, + help="Search for photos that were imported as referenced files (not copied into Photos library).", + ), + ] + for o in options[::-1]: + f = o(f) + return f + + +@click.group(context_settings=CTX_SETTINGS) +@DB_OPTION +@JSON_OPTION +@click.option("--debug", required=False, is_flag=True, default=False, hidden=True) +@click.version_option(__version__, "--version", "-v") +@click.pass_context +def cli(ctx, db, json_, debug): + ctx.obj = CLI_Obj(db=db, json=json_, debug=debug) + + +@cli.command(cls=ExportCommand) +@DB_OPTION +@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.") +@query_options +@click.option( + "--missing", + is_flag=True, + help="Export only photos missing from the Photos library; must be used with --download-missing.", +) +@deleted_options +@click.option( + "--update", + is_flag=True, + help="Only export new or updated files. See notes below on export and --update.", +) +@click.option( + "--ignore-signature", + is_flag=True, + help="When used with --update, ignores file signature when updating files. " + "This is useful if you have processed or edited exported photos changing the " + "file signature (size & modification date). In this case, --update would normally " + "re-export the processed files but with --ignore-signature, files which exist " + "in the export directory will not be re-exported.", +) +@click.option( + "--dry-run", + is_flag=True, + help="Dry run (test) the export but don't actually export any files; most useful with --verbose.", +) +@click.option( + "--export-as-hardlink", + is_flag=True, + help="Hardlink files instead of copying them. " + "Cannot be used with --exiftool which creates copies of the files with embedded EXIF data. " + "Note: on APFS volumes, files are cloned when exporting giving many of the same " + "advantages as hardlinks without having to use --export-as-hardlink.", +) +@click.option( + "--touch-file", + is_flag=True, + help="Sets the file's modification time to match photo date.", +) +@click.option( + "--overwrite", + is_flag=True, + help="Overwrite existing files. " + "Default behavior is to add (1), (2), etc to filename if file already exists. " + "Use this with caution as it may create name collisions on export. " + "(e.g. if two files happen to have the same name)", +) +@click.option( + "--export-by-date", + is_flag=True, + help="Automatically create output folders to organize photos by date created " + "(e.g. DEST/2019/12/20/photoname.jpg).", +) +@click.option( + "--skip-edited", + is_flag=True, + help="Do not export edited version of photo if an edited version exists.", +) +@click.option( + "--skip-original-if-edited", + is_flag=True, + help="Do not export original if there is an edited version (exports only the edited version).", +) +@click.option( + "--skip-bursts", + is_flag=True, + help="Do not export all associated burst images in the library if a photo is a burst photo. ", +) +@click.option( + "--skip-live", + is_flag=True, + help="Do not export the associated live video component of a live photo.", +) +@click.option( + "--skip-raw", + is_flag=True, + help="Do not export associated raw images of a RAW+JPEG pair. " + "Note: this does not skip raw photos if the raw photo does not have an associated jpeg image " + "(e.g. the raw file was imported to Photos without a jpeg preview).", +) +@click.option( + "--current-name", + is_flag=True, + help="Use photo's current filename instead of original filename for export. " + "Note: Starting with Photos 5, all photos are renamed upon import. By default, " + "photos are exported with the the original name they had before import.", +) +@click.option( + "--convert-to-jpeg", + is_flag=True, + help="Convert all non-jpeg images (e.g. raw, HEIC, PNG, etc) " + "to JPEG upon export. Only works if your Mac has a GPU.", +) +@click.option( + "--jpeg-quality", + type=click.FloatRange(0.0, 1.0), + help="Value in range 0.0 to 1.0 to use with --convert-to-jpeg. " + "A value of 1.0 specifies best quality, " + "a value of 0.0 specifies maximum compression. " + f"Defaults to {DEFAULT_JPEG_QUALITY}", +) +@click.option( + "--download-missing", + is_flag=True, + help="Attempt to download missing photos from iCloud. The current implementation uses Applescript " + "to interact with Photos to export the photo which will force Photos to download from iCloud if " + "the photo does not exist on disk. This will be slow and will require internet connection. " + "This obviously only works if the Photos library is synched to iCloud. " + "Note: --download-missing does not currently export all burst images; " + "only the primary photo will be exported--associated burst images will be skipped.", +) +@click.option( + "--sidecar", + default=None, + multiple=True, + metavar="FORMAT", + type=click.Choice(["xmp", "json", "exiftool"], case_sensitive=False), + help="Create sidecar for each photo exported; valid FORMAT values: xmp, json, exiftool; " + "--sidecar xmp: create XMP sidecar used by Digikam, Adobe Lightroom, etc. " + "The sidecar file is named in format photoname.ext.xmp " + "The XMP sidecar exports the following tags: Description, Title, Keywords/Tags, " + "Subject (set to Keywords + PersonInImage), PersonInImage, CreateDate, ModifyDate, " + "GPSLongitude, Face Regions (Metadata Working Group and Microsoft Photo)." + f"\n--sidecar json: create JSON sidecar useable by exiftool ({_EXIF_TOOL_URL}) " + "The sidecar file can be used to apply metadata to the file with exiftool, for example: " + '"exiftool -j=photoname.jpg.json photoname.jpg" ' + "The sidecar file is named in format photoname.ext.json; " + "format includes tag groups (equivalent to running 'exiftool -G -j'). " + "\n--sidecar exiftool: create JSON sidecar compatible with output of 'exiftool -j'. " + "Unlike '--sidecar json', '--sidecar exiftool' does not export tag groups. " + "Sidecar filename is in format photoname.ext.json; " + "For a list of tags exported in the JSON and exiftool sidecar, see '--exiftool'.", +) +@click.option( + "--sidecar-drop-ext", + is_flag=True, + help="Drop the photo's extension when naming sidecar files. " + "By default, sidecar files are named in format 'photo_filename.photo_ext.sidecar_ext', " + "e.g. 'IMG_1234.JPG.xmp'. Use '--sidecar-drop-ext' to ignore the photo extension. " + "Resulting sidecar files will have name in format 'IMG_1234.xmp'. " + "Warning: this may result in sidecar filename collisions if there are files of different " + "types but the same name in the output directory, e.g. 'IMG_1234.JPG' and 'IMG_1234.MOV'.", +) +@click.option( + "--exiftool", + is_flag=True, + help="Use exiftool to write metadata directly to exported photos. " + "To use this option, exiftool must be installed and in the path. " + "exiftool may be installed from https://exiftool.org/. " + "Cannot be used with --export-as-hardlink. Writes the following metadata: " + "EXIF:ImageDescription, XMP:Description (see also --description-template); " + "XMP:Title; XMP:TagsList, IPTC:Keywords, XMP:Subject " + "(see also --keyword-template, --person-keyword, --album-keyword); " + "XMP:PersonInImage; EXIF:GPSLatitudeRef; EXIF:GPSLongitudeRef; EXIF:GPSLatitude; EXIF:GPSLongitude; " + "EXIF:GPSPosition; EXIF:DateTimeOriginal; EXIF:OffsetTimeOriginal; " + "EXIF:ModifyDate (see --ignore-date-modified); IPTC:DateCreated; IPTC:TimeCreated; " + "(video files only): QuickTime:CreationDate; QuickTime:CreateDate; QuickTime:ModifyDate (see also --ignore-date-modified); " + "QuickTime:GPSCoordinates; UserData:GPSCoordinates.", +) +@click.option( + "--exiftool-path", + metavar="EXIFTOOL_PATH", + type=click.Path(exists=True), + help="Optionally specify path to exiftool; if not provided, will look for exiftool in $PATH.", +) +@click.option( + "--exiftool-option", + multiple=True, + metavar="OPTION", + help="Optional flag/option to pass to exiftool when using --exiftool. " + "For example, --exiftool-option '-m' to ignore minor warnings. " + "Specify these as you would on the exiftool command line. " + "See exiftool docs at https://exiftool.org/exiftool_pod.html for full list of options. " + "More than one option may be specified by repeating the option, e.g. " + "--exiftool-option '-m' --exiftool-option '-F'. ", +) +@click.option( + "--exiftool-merge-keywords", + is_flag=True, + help="Merge any keywords found in the original file with keywords used for '--exiftool' and '--sidecar'.", +) +@click.option( + "--exiftool-merge-persons", + is_flag=True, + help="Merge any persons found in the original file with persons used for '--exiftool' and '--sidecar'.", +) +@click.option( + "--ignore-date-modified", + is_flag=True, + help="If used with --exiftool or --sidecar, will ignore the photo " + "modification date and set EXIF:ModifyDate to EXIF:DateTimeOriginal; " + "this is consistent with how Photos handles the EXIF:ModifyDate tag.", +) +@click.option( + "--person-keyword", + is_flag=True, + help="Use person in image as keyword/tag when exporting metadata.", +) +@click.option( + "--album-keyword", + is_flag=True, + help="Use album name as keyword/tag when exporting metadata.", +) +@click.option( + "--keyword-template", + metavar="TEMPLATE", + multiple=True, + default=None, + help="For use with --exiftool, --sidecar; specify a template string to use as " + "keyword in the form '{name,DEFAULT}' " + "This is the same format as --directory. For example, if you wanted to add " + "the full path to the folder and album photo is contained in as a keyword when exporting " + 'you could specify --keyword-template "{folder_album}" ' + 'You may specify more than one template, for example --keyword-template "{folder_album}" ' + '--keyword-template "{created.year}" ' + "See Templating System below.", +) +@click.option( + "--description-template", + metavar="TEMPLATE", + multiple=False, + default=None, + help="For use with --exiftool, --sidecar; specify a template string to use as " + "description in the form '{name,DEFAULT}' " + "This is the same format as --directory. For example, if you wanted to append " + "'exported with osxphotos on [today's date]' to the description, you could specify " + '--description-template "{descr} exported with osxphotos on {today.date}" ' + "See Templating System below.", +) +@click.option( + "--finder-tag-template", + metavar="TEMPLATE", + multiple=True, + default=None, + help="Set MacOS Finder tags to TEMPLATE. These tags can be searched in the Finder or Spotlight with " + "'tag:tagname' format. For example, '--finder-tag-template \"{label}\"' to set Finder tags to photo labels. " + "You may specify multiple TEMPLATE values by using '--finder-tag-template' multiple times. " + "See also '--finder-tag-keywords and Extended Attributes below.'.", +) +@click.option( + "--finder-tag-keywords", + is_flag=True, + help="Set MacOS Finder tags to keywords; any keywords specified via '--keyword-template', '--person-keyword', etc. " + "will also be used as Finder tags. See also '--finder-tag-template and Extended Attributes below.'.", +) +@click.option( + "--xattr-template", + nargs=2, + metavar="ATTRIBUTE TEMPLATE", + multiple=True, + help="Set extended attribute ATTRIBUTE to TEMPLATE value. Valid attributes are: " + f"{', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}. " + "For example, to set Finder comment to the photo's title and description: " + '\'--xattr-template findercomment "{title}; {descr}" ' + "See Extended Attributes below for additional details on this option.", +) +@click.option( + "--directory", + metavar="DIRECTORY", + default=None, + help="Optional template for specifying name of output directory in the form '{name,DEFAULT}'. " + "See below for additional details on templating system.", +) +@click.option( + "--filename", + "filename_template", + metavar="FILENAME", + default=None, + help="Optional template for specifying name of output file in the form '{name,DEFAULT}'. " + "File extension will be added automatically--do not include an extension in the FILENAME template. " + "See below for additional details on templating system.", +) +@click.option( + "--jpeg-ext", + multiple=False, + metavar="EXTENSION", + type=click.Choice(["jpeg", "jpg", "JPEG", "JPG"], case_sensitive=True), + help="Specify file extension for JPEG files. Photos uses .jpeg for edited images but many images " + "are imported with .jpg or .JPG which can result in multiple different extensions used for JPEG files " + "upon export. Use --jpg-ext to specify a single extension to use for all exported JPEG images. " + "Valid values are jpeg, jpg, JPEG, JPG; e.g. '--jpg-ext jpg' to use '.jpg' for all JPEGs.", +) +@click.option( + "--strip", + is_flag=True, + help="Optionally strip leading and trailing whitespace from any rendered templates. " + 'For example, if --filename template is "{title,} {original_name}" and image has no ' + "title, resulting file would have a leading space but if used with --strip, this will " + "be removed.", +) +@click.option( + "--edited-suffix", + metavar="SUFFIX", + help="Optional suffix template for naming edited photos. Default name for edited photos is in form " + "'photoname_edited.ext'. For example, with '--edited-suffix _bearbeiten', the edited photo " + f"would be named 'photoname_bearbeiten.ext'. The default suffix is '{DEFAULT_EDITED_SUFFIX}'. " + "Multi-value templates (see Templating System) are not permitted with --edited-suffix.", +) +@click.option( + "--original-suffix", + metavar="SUFFIX", + help="Optional suffix template for naming original photos. Default name for original photos is in form " + "'filename.ext'. For example, with '--original-suffix _original', the original photo " + "would be named 'filename_original.ext'. The default suffix is '' (no suffix). " + "Multi-value templates (see Templating System) are not permitted with --original-suffix.", +) +@click.option( + "--use-photos-export", + is_flag=True, + help="Force the use of AppleScript or PhotoKit to export even if not missing (see also '--download-missing' and '--use-photokit').", +) +@click.option( + "--use-photokit", + is_flag=True, + help="Use with '--download-missing' or '--use-photos-export' to use direct Photos interface instead of AppleScript to export. " + "Highly experimental alpha feature; does not work with iTerm2 (use with Terminal.app). " + "This is faster and more reliable than the default AppleScript interface.", +) +@click.option( + "--report", + metavar="", + help="Write a CSV formatted report of all files that were exported.", + type=click.Path(), +) +@click.option( + "--cleanup", + is_flag=True, + help="Cleanup export directory by deleting any files which were not included in this export set. " + "For example, photos which had previously been exported and were subsequently deleted in Photos.", +) +@click.option( + "--exportdb", + metavar="EXPORTDB_FILE", + default=None, + help=( + "Specify alternate name for database file which stores state information for export and --update. " + f"If --exportdb is not specified, export database will be saved to '{OSXPHOTOS_EXPORT_DB}' " + "in the export directory. Must be specified as filename only, not a path, as export database " + "will be saved in export directory." + ), + type=click.Path(), +) +@click.option( + "--load-config", + required=False, + metavar="", + default=None, + help=( + "Load options from file as written with --save-config. " + "This allows you to save a complex export command to file for later reuse. " + "For example: 'osxphotos export --save-config osxphotos.toml' then " + " 'osxphotos export /path/to/export --load-config osxphotos.toml'. " + "If any other command line options are used in conjunction with --load-config, " + "they will override the corresponding values in the config file." + ), + type=click.Path(exists=True), +) +@click.option( + "--save-config", + required=False, + metavar="", + default=None, + help=("Save options to file for use with --load-config. File format is TOML."), + type=click.Path(), +) +@click.option( + "--beta", is_flag=True, default=False, hidden=True, help="Enable beta options." +) +@DB_ARGUMENT +@click.argument("dest", nargs=1, type=click.Path(exists=True)) +@click.pass_obj +@click.pass_context +def export( + ctx, + cli_obj, + db, + photos_library, + keyword, + person, + album, + folder, + uuid, + uuid_from_file, + title, + no_title, + description, + no_description, + uti, + ignore_case, + edited, + external_edit, + favorite, + not_favorite, + hidden, + not_hidden, + shared, + not_shared, + from_date, + to_date, + verbose, + missing, + update, + ignore_signature, + dry_run, + export_as_hardlink, + touch_file, + overwrite, + export_by_date, + skip_edited, + skip_original_if_edited, + skip_bursts, + skip_live, + skip_raw, + person_keyword, + album_keyword, + keyword_template, + description_template, + finder_tag_template, + finder_tag_keywords, + xattr_template, + current_name, + convert_to_jpeg, + jpeg_quality, + sidecar, + sidecar_drop_ext, + only_photos, + only_movies, + burst, + not_burst, + live, + not_live, + download_missing, + dest, + exiftool, + exiftool_path, + exiftool_option, + exiftool_merge_keywords, + exiftool_merge_persons, + ignore_date_modified, + portrait, + not_portrait, + screenshot, + not_screenshot, + slow_mo, + not_slow_mo, + time_lapse, + not_time_lapse, + hdr, + not_hdr, + selfie, + not_selfie, + panorama, + not_panorama, + has_raw, + directory, + filename_template, + jpeg_ext, + strip, + edited_suffix, + original_suffix, + place, + no_place, + has_comment, + no_comment, + has_likes, + no_likes, + label, + deleted, + deleted_only, + use_photos_export, + use_photokit, + report, + cleanup, + exportdb, + load_config, + save_config, + is_reference, + beta, +): + """Export photos from the Photos database. + Export path DEST is required. + Optionally, query the Photos database using 1 or more search options; + if more than one option is provided, they are treated as "AND" + (e.g. search for photos matching all options). + If no query options are provided, all photos will be exported. + By default, all versions of all photos will be exported including edited + versions, live photo movies, burst photos, and associated raw images. + See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options + to modify this behavior. + """ + + # NOTE: because of the way ConfigOptions works, Click options must not + # set defaults which are not None or False. If defaults need to be set + # do so below after load_config and save_config are handled. + cfg = ConfigOptions( + "export", + locals(), + ignore=["ctx", "cli_obj", "dest", "load_config", "save_config"], + ) + + global VERBOSE + VERBOSE = bool(verbose) + + if load_config: + try: + cfg.load_from_file(load_config) + except ConfigOptionsLoadError as e: + click.echo( + click.style( + f"Error parsing {load_config} config file: {e.message}", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + raise click.Abort() + + # re-set the local vars to the corresponding config value + # this isn't elegant but avoids having to rewrite this function to use cfg.varname for every parameter + db = cfg.db + photos_library = cfg.photos_library + keyword = cfg.keyword + person = cfg.person + album = cfg.album + folder = cfg.folder + uuid = cfg.uuid + uuid_from_file = cfg.uuid_from_file + title = cfg.title + no_title = cfg.no_title + description = cfg.description + no_description = cfg.no_description + uti = cfg.uti + ignore_case = cfg.ignore_case + edited = cfg.edited + external_edit = cfg.external_edit + favorite = cfg.favorite + not_favorite = cfg.not_favorite + hidden = cfg.hidden + not_hidden = cfg.not_hidden + shared = cfg.shared + not_shared = cfg.not_shared + from_date = cfg.from_date + to_date = cfg.to_date + verbose = cfg.verbose + missing = cfg.missing + update = cfg.update + ignore_signature = cfg.ignore_signature + dry_run = cfg.dry_run + export_as_hardlink = cfg.export_as_hardlink + touch_file = cfg.touch_file + overwrite = cfg.overwrite + export_by_date = cfg.export_by_date + skip_edited = cfg.skip_edited + skip_original_if_edited = cfg.skip_original_if_edited + skip_bursts = cfg.skip_bursts + skip_live = cfg.skip_live + skip_raw = cfg.skip_raw + person_keyword = cfg.person_keyword + album_keyword = cfg.album_keyword + keyword_template = cfg.keyword_template + description_template = cfg.description_template + finder_tag_template = cfg.finder_tag_template + finder_tag_keywords = cfg.finder_tag_keywords + xattr_template = cfg.xattr_template + current_name = cfg.current_name + convert_to_jpeg = cfg.convert_to_jpeg + jpeg_quality = cfg.jpeg_quality + sidecar = cfg.sidecar + sidecar_drop_ext = cfg.sidecar_drop_ext + only_photos = cfg.only_photos + only_movies = cfg.only_movies + burst = cfg.burst + not_burst = cfg.not_burst + live = cfg.live + not_live = cfg.not_live + download_missing = cfg.download_missing + exiftool = cfg.exiftool + exiftool_path = cfg.exiftool_path + exiftool_option = cfg.exiftool_option + exiftool_merge_keywords = cfg.exiftool_merge_keywords + exiftool_merge_persons = cfg.exiftool_merge_persons + ignore_date_modified = cfg.ignore_date_modified + portrait = cfg.portrait + not_portrait = cfg.not_portrait + screenshot = cfg.screenshot + not_screenshot = cfg.not_screenshot + slow_mo = cfg.slow_mo + not_slow_mo = cfg.not_slow_mo + time_lapse = cfg.time_lapse + not_time_lapse = cfg.not_time_lapse + hdr = cfg.hdr + not_hdr = cfg.not_hdr + selfie = cfg.selfie + not_selfie = cfg.not_selfie + panorama = cfg.panorama + not_panorama = cfg.not_panorama + has_raw = cfg.has_raw + directory = cfg.directory + filename_template = cfg.filename_template + jpeg_ext = cfg.jpeg_ext + strip = cfg.strip + edited_suffix = cfg.edited_suffix + original_suffix = cfg.original_suffix + place = cfg.place + no_place = cfg.no_place + has_comment = cfg.has_comment + no_comment = cfg.no_comment + has_likes = cfg.has_likes + no_likes = cfg.no_likes + label = cfg.label + deleted = cfg.deleted + deleted_only = cfg.deleted_only + use_photos_export = cfg.use_photos_export + use_photokit = cfg.use_photokit + report = cfg.report + cleanup = cfg.cleanup + exportdb = cfg.exportdb + beta = cfg.beta + + # config file might have changed verbose + VERBOSE = bool(verbose) + verbose_(f"Loaded options from file {load_config}") + + verbose_(f"osxphotos version {__version__}") + + # validate options + exclusive_options = [ + ("favorite", "not_favorite"), + ("hidden", "not_hidden"), + ("title", "no_title"), + ("description", "no_description"), + ("only_photos", "only_movies"), + ("burst", "not_burst"), + ("live", "not_live"), + ("portrait", "not_portrait"), + ("screenshot", "not_screenshot"), + ("slow_mo", "not_slow_mo"), + ("time_lapse", "not_time_lapse"), + ("hdr", "not_hdr"), + ("selfie", "not_selfie"), + ("panorama", "not_panorama"), + ("export_by_date", "directory"), + ("export_as_hardlink", "exiftool"), + ("place", "no_place"), + ("deleted", "deleted_only"), + ("skip_edited", "skip_original_if_edited"), + ("export_as_hardlink", "convert_to_jpeg"), + ("export_as_hardlink", "download_missing"), + ("shared", "not_shared"), + ("has_comment", "no_comment"), + ("has_likes", "no_likes"), + ] + dependent_options = [ + ("missing", ("download_missing", "use_photos_export")), + ("jpeg_quality", ("convert_to_jpeg")), + ("ignore_signature", ("update")), + ("exiftool_option", ("exiftool")), + ("exiftool_merge_keywords", ("exiftool", "sidecar")), + ("exiftool_merge_persons", ("exiftool", "sidecar")), + ] + try: + cfg.validate(exclusive=exclusive_options, dependent=dependent_options, cli=True) + except ConfigOptionsInvalidError as e: + click.echo( + click.style( + f"Incompatible export options: {e.message}", fg=CLI_COLOR_ERROR + ), + err=True, + ) + raise click.Abort() + + if all(x in [s.lower() for s in sidecar] for x in ["json", "exiftool"]): + click.echo( + click.style( + "Cannot use --sidecar json with --sidecar exiftool due to name collisions", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + raise click.Abort() + + if xattr_template: + for attr, _ in xattr_template: + if attr not in EXTENDED_ATTRIBUTE_NAMES: + click.echo( + click.style( + f"Invalid attribute '{attr}' for --xattr-template; " + f"valid values are {', '.join(EXTENDED_ATTRIBUTE_NAMES_QUOTED)}", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + raise click.Abort() + + if save_config: + verbose_(f"Saving options to file {save_config}") + cfg.write_to_file(save_config) + + # set defaults for options that need them + jpeg_quality = DEFAULT_JPEG_QUALITY if jpeg_quality is None else jpeg_quality + edited_suffix = DEFAULT_EDITED_SUFFIX if edited_suffix is None else edited_suffix + original_suffix = ( + DEFAULT_ORIGINAL_SUFFIX if original_suffix is None else original_suffix + ) + + if not os.path.isdir(dest): + click.echo( + click.style(f"DEST {dest} must be valid path", fg=CLI_COLOR_ERROR), err=True + ) + raise click.Abort() + + dest = str(pathlib.Path(dest).resolve()) + + if report and os.path.isdir(report): + click.echo( + click.style( + f"report is a directory, must be file name", fg=CLI_COLOR_ERROR + ), + err=True, + ) + raise click.Abort() + + # if use_photokit and not check_photokit_authorization(): + # click.echo( + # "Requesting access to use your Photos library. Click 'OK' on the dialog box to grant access." + # ) + # request_photokit_authorization() + # click.confirm("Have you granted access?") + # if not check_photokit_authorization(): + # click.echo( + # "Failed to get access to the Photos library which is needed with `--use-photokit`." + # ) + # return + + # initialize export flags + # by default, will export all versions of photos unless skip flag is set + (export_edited, export_bursts, export_live, export_raw) = [ + not x for x in [skip_edited, skip_bursts, skip_live, skip_raw] + ] + + # verify exiftool installed and in path if path not provided and exiftool will be used + # NOTE: this won't catch use of {exiftool:} in a template + # but those will raise error during template eval if exiftool path not set + if ( + any([exiftool, exiftool_merge_keywords, exiftool_merge_persons]) + and not exiftool_path + ): + try: + exiftool_path = get_exiftool_path() + except FileNotFoundError: + click.echo( + click.style( + "Could not find exiftool. Please download and install" + " from https://exiftool.org/", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + ctx.exit(2) + + if any([exiftool, exiftool_merge_keywords, exiftool_merge_persons]): + verbose_(f"exiftool path: {exiftool_path}") + + isphoto = ismovie = True # default searches for everything + if only_movies: + isphoto = False + if only_photos: + ismovie = False + + # load UUIDs if necessary and append to any uuids passed with --uuid + if uuid_from_file: + uuid_list = list(uuid) # Click option is a tuple + uuid_list.extend(load_uuid_from_file(uuid_from_file)) + uuid = tuple(uuid_list) + + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + db = get_photos_db(*photos_library, db, cli_db) + if db is None: + click.echo(cli.commands["export"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + # sanity check exportdb + if exportdb and exportdb != OSXPHOTOS_EXPORT_DB: + if "/" in exportdb: + click.echo( + click.style( + f"Error: --exportdb must be specified as filename not path; " + + f"export database will saved in export directory '{dest}'.", + fg=CLI_COLOR_ERROR, + ) + ) + raise click.Abort() + elif pathlib.Path(pathlib.Path(dest) / OSXPHOTOS_EXPORT_DB).exists(): + click.echo( + click.style( + f"Warning: export database is '{exportdb}' but found '{OSXPHOTOS_EXPORT_DB}' in {dest}; using '{exportdb}'", + fg=CLI_COLOR_WARNING, + ) + ) + + # open export database and assign copy/link/unlink functions + export_db_path = os.path.join(dest, exportdb or OSXPHOTOS_EXPORT_DB) + + # check that export isn't in the parent or child of a previously exported library + other_db_files = find_files_in_branch(dest, OSXPHOTOS_EXPORT_DB) + if other_db_files: + click.echo( + click.style( + "WARNING: found other export database files in this destination directory branch. " + + "This likely means you are attempting to export files into a directory " + + "that is either the parent or a child directory of a previous export. " + + "Proceeding may cause your exported files to be overwritten.", + fg=CLI_COLOR_WARNING, + ), + err=True, + ) + click.echo( + f"You are exporting to {dest}, found {OSXPHOTOS_EXPORT_DB} files in:" + ) + for other_db in other_db_files: + click.echo(f"{other_db}") + click.confirm("Do you want to continue?", abort=True) + + if dry_run: + export_db = ExportDBInMemory(export_db_path) + fileutil = FileUtilNoOp + else: + export_db = ExportDB(export_db_path) + fileutil = FileUtil + + if verbose_: + if export_db.was_created: + verbose_(f"Created export database {export_db_path}") + else: + verbose_(f"Using export database {export_db_path}") + upgraded = export_db.was_upgraded + if upgraded: + verbose_( + f"Upgraded export database {export_db_path} from version {upgraded[0]} to {upgraded[1]}" + ) + + photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_, exiftool=exiftool_path) + + # enable beta features if requested + photosdb._beta = beta + + photos = _query( + photosdb=photosdb, + keyword=keyword, + person=person, + album=album, + folder=folder, + uuid=uuid, + title=title, + no_title=no_title, + description=description, + no_description=no_description, + ignore_case=ignore_case, + edited=edited, + external_edit=external_edit, + favorite=favorite, + not_favorite=not_favorite, + hidden=hidden, + not_hidden=not_hidden, + missing=missing, + not_missing=None, + shared=shared, + not_shared=not_shared, + isphoto=isphoto, + ismovie=ismovie, + uti=uti, + burst=burst, + not_burst=not_burst, + live=live, + not_live=not_live, + cloudasset=False, + not_cloudasset=False, + incloud=False, + not_incloud=False, + from_date=from_date, + to_date=to_date, + portrait=portrait, + not_portrait=not_portrait, + screenshot=screenshot, + not_screenshot=not_screenshot, + slow_mo=slow_mo, + not_slow_mo=not_slow_mo, + time_lapse=time_lapse, + not_time_lapse=not_time_lapse, + hdr=hdr, + not_hdr=not_hdr, + selfie=selfie, + not_selfie=not_selfie, + panorama=panorama, + not_panorama=not_panorama, + has_raw=has_raw, + place=place, + no_place=no_place, + label=label, + deleted=deleted, + deleted_only=deleted_only, + has_comment=has_comment, + no_comment=no_comment, + has_likes=has_likes, + no_likes=no_likes, + is_reference=is_reference, + ) + + if photos: + if export_bursts: + # add the burst_photos to the export set + photos_burst = [p for p in photos if p.burst] + for burst in photos_burst: + burst_set = [p for p in burst.burst_photos if not p.ismissing] + photos.extend(burst_set) + + num_photos = len(photos) + # TODO: photos or photo appears several times, pull into a separate function + photo_str = "photos" if num_photos > 1 else "photo" + click.echo(f"Exporting {num_photos} {photo_str} to {dest}...") + start_time = time.perf_counter() + # though the command line option is current_name, internally all processing + # logic uses original_name which is the boolean inverse of current_name + # because the original code used --original-name as an option + original_name = not current_name + + results = ExportResults() + # send progress bar output to /dev/null if verbose to hide the progress bar + fp = open(os.devnull, "w") if verbose else None + with click.progressbar(photos, file=fp) as bar: + for p in bar: + export_results = export_photo( + photo=p, + dest=dest, + verbose=verbose, + export_by_date=export_by_date, + sidecar=sidecar, + sidecar_drop_ext=sidecar_drop_ext, + update=update, + ignore_signature=ignore_signature, + export_as_hardlink=export_as_hardlink, + overwrite=overwrite, + export_edited=export_edited, + skip_original_if_edited=skip_original_if_edited, + original_name=original_name, + export_live=export_live, + download_missing=download_missing, + exiftool=exiftool, + exiftool_merge_keywords=exiftool_merge_keywords, + exiftool_merge_persons=exiftool_merge_persons, + directory=directory, + filename_template=filename_template, + export_raw=export_raw, + album_keyword=album_keyword, + person_keyword=person_keyword, + keyword_template=keyword_template, + description_template=description_template, + export_db=export_db, + fileutil=fileutil, + dry_run=dry_run, + touch_file=touch_file, + edited_suffix=edited_suffix, + original_suffix=original_suffix, + use_photos_export=use_photos_export, + convert_to_jpeg=convert_to_jpeg, + jpeg_quality=jpeg_quality, + ignore_date_modified=ignore_date_modified, + use_photokit=use_photokit, + exiftool_option=exiftool_option, + strip=strip, + jpeg_ext=jpeg_ext, + ) + results += export_results + + # all photo files (not including sidecars) that are part of this export set + # used below for applying Finder tags, etc. + photo_files = set( + export_results.exported + + export_results.new + + export_results.updated + + export_results.exif_updated + + export_results.converted_to_jpeg + + export_results.skipped + ) + + if finder_tag_keywords or finder_tag_template: + tags_written, tags_skipped = write_finder_tags( + p, + photo_files, + keywords=finder_tag_keywords, + keyword_template=keyword_template, + album_keyword=album_keyword, + person_keyword=person_keyword, + exiftool_merge_keywords=exiftool_merge_keywords, + finder_tag_template=finder_tag_template, + strip=strip, + ) + results.xattr_written.extend(tags_written) + results.xattr_skipped.extend(tags_skipped) + + if xattr_template: + xattr_written, xattr_skipped = write_extended_attributes( + p, photo_files, xattr_template, strip=strip + ) + results.xattr_written.extend(xattr_written) + results.xattr_skipped.extend(xattr_skipped) + + if fp is not None: + fp.close() + + if cleanup: + all_files = ( + results.exported + + results.skipped + + results.exif_updated + + results.touched + + results.converted_to_jpeg + + results.sidecar_json_written + + results.sidecar_json_skipped + + results.sidecar_exiftool_written + + results.sidecar_exiftool_skipped + + results.sidecar_xmp_written + + results.sidecar_xmp_skipped + # include missing so a file that was already in export directory + # but was missing on --update doesn't get deleted + # (better to have old version than none) + + results.missing + # include files that have error in case they exist from previous export + + [r[0] for r in results.error] + + [str(pathlib.Path(export_db_path).resolve())] + ) + click.echo(f"Cleaning up {dest}") + (cleaned_files, cleaned_dirs) = cleanup_files(dest, all_files, fileutil) + file_str = "files" if cleaned_files != 1 else "file" + dir_str = "directories" if cleaned_dirs != 1 else "directory" + click.echo(f"Deleted: {cleaned_files} {file_str}, {cleaned_dirs} {dir_str}") + + if report: + verbose_(f"Writing export report to {report}") + write_export_report(report, results) + + photo_str_total = "photos" if len(photos) != 1 else "photo" + if update: + summary = ( + f"Processed: {len(photos)} {photo_str_total}, " + f"exported: {len(results.new)}, " + f"updated: {len(results.updated)}, " + f"skipped: {len(results.skipped)}, " + f"updated EXIF data: {len(results.exif_updated)}, " + ) + else: + summary = ( + f"Processed: {len(photos)} {photo_str_total}, " + f"exported: {len(results.exported)}, " + ) + summary += f"missing: {len(results.missing)}, " + summary += f"error: {len(results.error)}" + if touch_file: + summary += f", touched date: {len(results.touched)}" + click.echo(summary) + stop_time = time.perf_counter() + click.echo(f"Elapsed time: {(stop_time-start_time):.3f} seconds") + else: + click.echo("Did not find any photos to export") + + export_db.close() + + +@cli.command() +@click.argument("topic", default=None, required=False, nargs=1) +@click.pass_context +def help(ctx, topic, **kw): + """ Print help; for help on commands: help . """ + if topic is None: + click.echo(ctx.parent.get_help()) + elif topic in cli.commands: + ctx.info_name = topic + click.echo_via_pager(cli.commands[topic].get_help(ctx)) + else: + click.echo(f"Invalid command: {topic}", err=True) + click.echo(ctx.parent.get_help()) + + +@cli.command() +@DB_OPTION +@JSON_OPTION +@query_options +@deleted_options +@click.option("--missing", is_flag=True, help="Search for photos missing from disk.") +@click.option( + "--not-missing", + is_flag=True, + help="Search for photos present on disk (e.g. not missing).", +) +@click.option( + "--cloudasset", + is_flag=True, + help="Search for photos that are part of an iCloud library", +) +@click.option( + "--not-cloudasset", + is_flag=True, + help="Search for photos that are not part of an iCloud library", +) +@click.option( + "--incloud", + is_flag=True, + help="Search for photos that are in iCloud (have been synched)", +) +@click.option( + "--not-incloud", + is_flag=True, + help="Search for photos that are not in iCloud (have not been synched)", +) +@DB_ARGUMENT +@click.pass_obj +@click.pass_context +def query( + ctx, + cli_obj, + db, + photos_library, + keyword, + person, + album, + folder, + uuid, + uuid_from_file, + title, + no_title, + description, + no_description, + ignore_case, + json_, + edited, + external_edit, + favorite, + not_favorite, + hidden, + not_hidden, + missing, + not_missing, + shared, + not_shared, + only_movies, + only_photos, + uti, + burst, + not_burst, + live, + not_live, + cloudasset, + not_cloudasset, + incloud, + not_incloud, + from_date, + to_date, + portrait, + not_portrait, + screenshot, + not_screenshot, + slow_mo, + not_slow_mo, + time_lapse, + not_time_lapse, + hdr, + not_hdr, + selfie, + not_selfie, + panorama, + not_panorama, + has_raw, + place, + no_place, + label, + deleted, + deleted_only, + has_comment, + no_comment, + has_likes, + no_likes, + is_reference, +): + """Query the Photos database using 1 or more search options; + if more than one option is provided, they are treated as "AND" + (e.g. search for photos matching all options). + """ + + # if no query terms, show help and return + # sanity check input args + nonexclusive = [ + keyword, + person, + album, + folder, + uuid, + uuid_from_file, + edited, + external_edit, + uti, + has_raw, + from_date, + to_date, + label, + is_reference, + ] + exclusive = [ + (favorite, not_favorite), + (hidden, not_hidden), + (missing, not_missing), + (any(title), no_title), + (any(description), no_description), + (only_photos, only_movies), + (burst, not_burst), + (live, not_live), + (cloudasset, not_cloudasset), + (incloud, not_incloud), + (portrait, not_portrait), + (screenshot, not_screenshot), + (slow_mo, not_slow_mo), + (time_lapse, not_time_lapse), + (hdr, not_hdr), + (selfie, not_selfie), + (panorama, not_panorama), + (any(place), no_place), + (deleted, deleted_only), + (shared, not_shared), + (has_comment, no_comment), + (has_likes, no_likes), + ] + # print help if no non-exclusive term or a double exclusive term is given + if any(all(bb) for bb in exclusive) or not any( + nonexclusive + [b ^ n for b, n in exclusive] + ): + click.echo("Incompatible query options", err=True) + click.echo(cli.commands["query"].get_help(ctx), err=True) + return + + # actually have something to query + isphoto = ismovie = True # default searches for everything + if only_movies: + isphoto = False + if only_photos: + ismovie = False + + # load UUIDs if necessary and append to any uuids passed with --uuid + if uuid_from_file: + uuid_list = list(uuid) # Click option is a tuple + uuid_list.extend(load_uuid_from_file(uuid_from_file)) + uuid = tuple(uuid_list) + + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + db = get_photos_db(*photos_library, db, cli_db) + if db is None: + click.echo(cli.commands["query"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_) + photos = _query( + photosdb=photosdb, + keyword=keyword, + person=person, + album=album, + folder=folder, + uuid=uuid, + title=title, + no_title=no_title, + description=description, + no_description=no_description, + ignore_case=ignore_case, + edited=edited, + external_edit=external_edit, + favorite=favorite, + not_favorite=not_favorite, + hidden=hidden, + not_hidden=not_hidden, + missing=missing, + not_missing=not_missing, + shared=shared, + not_shared=not_shared, + isphoto=isphoto, + ismovie=ismovie, + uti=uti, + burst=burst, + not_burst=not_burst, + live=live, + not_live=not_live, + cloudasset=cloudasset, + not_cloudasset=not_cloudasset, + incloud=incloud, + not_incloud=not_incloud, + from_date=from_date, + to_date=to_date, + portrait=portrait, + not_portrait=not_portrait, + screenshot=screenshot, + not_screenshot=not_screenshot, + slow_mo=slow_mo, + not_slow_mo=not_slow_mo, + time_lapse=time_lapse, + not_time_lapse=not_time_lapse, + hdr=hdr, + not_hdr=not_hdr, + selfie=selfie, + not_selfie=not_selfie, + panorama=panorama, + not_panorama=not_panorama, + has_raw=has_raw, + place=place, + no_place=no_place, + label=label, + deleted=deleted, + deleted_only=deleted_only, + has_comment=has_comment, + no_comment=no_comment, + has_likes=has_likes, + no_likes=no_likes, + is_reference=is_reference, + ) + + # below needed for to make CliRunner work for testing + cli_json = cli_obj.json if cli_obj is not None else None + print_photo_info(photos, cli_json or json_) + + +def print_photo_info(photos, json=False): + dump = [] + if json: + for p in photos: + dump.append(p.json()) + click.echo(f"[{', '.join(dump)}]") + else: + # dump as CSV + csv_writer = csv.writer( + sys.stdout, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL + ) + # add headers + dump.append( + [ + "uuid", + "filename", + "original_filename", + "date", + "description", + "title", + "keywords", + "albums", + "persons", + "path", + "ismissing", + "hasadjustments", + "external_edit", + "favorite", + "hidden", + "shared", + "latitude", + "longitude", + "path_edited", + "isphoto", + "ismovie", + "uti", + "burst", + "live_photo", + "path_live_photo", + "iscloudasset", + "incloud", + "date_modified", + "portrait", + "screenshot", + "slow_mo", + "time_lapse", + "hdr", + "selfie", + "panorama", + "has_raw", + "uti_raw", + "path_raw", + "intrash", + ] + ) + for p in photos: + date_modified_iso = p.date_modified.isoformat() if p.date_modified else None + dump.append( + [ + p.uuid, + p.filename, + p.original_filename, + p.date.isoformat(), + p.description, + p.title, + ", ".join(p.keywords), + ", ".join(p.albums), + ", ".join(p.persons), + p.path, + p.ismissing, + p.hasadjustments, + p.external_edit, + p.favorite, + p.hidden, + p.shared, + p._latitude, + p._longitude, + p.path_edited, + p.isphoto, + p.ismovie, + p.uti, + p.burst, + p.live_photo, + p.path_live_photo, + p.iscloudasset, + p.incloud, + date_modified_iso, + p.portrait, + p.screenshot, + p.slow_mo, + p.time_lapse, + p.hdr, + p.selfie, + p.panorama, + p.has_raw, + p.uti_raw, + p.path_raw, + p.intrash, + ] + ) + for row in dump: + csv_writer.writerow(row) + + +def _query( + photosdb, + keyword=None, + person=None, + album=None, + folder=None, + uuid=None, + title=None, + no_title=None, + description=None, + no_description=None, + ignore_case=None, + edited=None, + external_edit=None, + favorite=None, + not_favorite=None, + hidden=None, + not_hidden=None, + missing=None, + not_missing=None, + shared=None, + not_shared=None, + isphoto=None, + ismovie=None, + uti=None, + burst=None, + not_burst=None, + live=None, + not_live=None, + cloudasset=None, + not_cloudasset=None, + incloud=None, + not_incloud=None, + from_date=None, + to_date=None, + portrait=None, + not_portrait=None, + screenshot=None, + not_screenshot=None, + slow_mo=None, + not_slow_mo=None, + time_lapse=None, + not_time_lapse=None, + hdr=None, + not_hdr=None, + selfie=None, + not_selfie=None, + panorama=None, + not_panorama=None, + has_raw=None, + place=None, + no_place=None, + label=None, + deleted=False, + deleted_only=False, + has_comment=False, + no_comment=False, + has_likes=False, + no_likes=False, + is_reference=False, +): + """Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands + + Args: + photosdb: PhotosDB object + """ + + if deleted or deleted_only: + photos = photosdb.photos( + uuid=uuid, + images=isphoto, + movies=ismovie, + from_date=from_date, + to_date=to_date, + intrash=True, + ) + else: + photos = [] + if not deleted_only: + photos += photosdb.photos( + uuid=uuid, + images=isphoto, + movies=ismovie, + from_date=from_date, + to_date=to_date, + ) + + person = normalize_unicode(person) + keyword = normalize_unicode(keyword) + album = normalize_unicode(album) + folder = normalize_unicode(folder) + title = normalize_unicode(title) + description = normalize_unicode(description) + place = normalize_unicode(place) + label = normalize_unicode(label) + + if album: + photos = get_photos_by_attribute(photos, "albums", album, ignore_case) + + if keyword: + photos = get_photos_by_attribute(photos, "keywords", keyword, ignore_case) + + if person: + photos = get_photos_by_attribute(photos, "persons", person, ignore_case) + + if label: + photos = get_photos_by_attribute(photos, "labels", label, ignore_case) + + if folder: + # search for photos in an album in folder + # finds photos that have albums whose top level folder matches folder + photo_list = [] + for f in folder: + photo_list.extend( + [ + p + for p in photos + if p.album_info + and f in [a.folder_names[0] for a in p.album_info if a.folder_names] + ] + ) + photos = photo_list + + if title: + # search title field for text + # if more than one, find photos with all title values in title + if ignore_case: + # case-insensitive + for t in title: + t = t.lower() + photos = [p for p in photos if p.title and t in p.title.lower()] + else: + for t in title: + photos = [p for p in photos if p.title and t in p.title] + elif no_title: + photos = [p for p in photos if not p.title] + + if description: + # search description field for text + # if more than one, find photos with all description values in description + if ignore_case: + # case-insensitive + for d in description: + d = d.lower() + photos = [ + p for p in photos if p.description and d in p.description.lower() + ] + else: + for d in description: + photos = [p for p in photos if p.description and d in p.description] + elif no_description: + photos = [p for p in photos if not p.description] + + if place: + # search place.names for text matching place + # if more than one place, find photos with all place values in description + if ignore_case: + # case-insensitive + for place_name in place: + place_name = place_name.lower() + photos = [ + p + for p in photos + if p.place + and any( + pname + for pname in p.place.names + if any( + pvalue for pvalue in pname if place_name in pvalue.lower() + ) + ) + ] + else: + for place_name in place: + photos = [ + p + for p in photos + if p.place + and any( + pname + for pname in p.place.names + if any(pvalue for pvalue in pname if place_name in pvalue) + ) + ] + elif no_place: + photos = [p for p in photos if not p.place] + + if edited: + photos = [p for p in photos if p.hasadjustments] + + if external_edit: + photos = [p for p in photos if p.external_edit] + + if favorite: + photos = [p for p in photos if p.favorite] + elif not_favorite: + photos = [p for p in photos if not p.favorite] + + if hidden: + photos = [p for p in photos if p.hidden] + elif not_hidden: + photos = [p for p in photos if not p.hidden] + + if missing: + photos = [p for p in photos if not p.path] + elif not_missing: + photos = [p for p in photos if p.path] + + if shared: + photos = [p for p in photos if p.shared] + elif not_shared: + photos = [p for p in photos if not p.shared] + + if shared: + photos = [p for p in photos if p.shared] + elif not_shared: + photos = [p for p in photos if not p.shared] + + if uti: + photos = [p for p in photos if uti in p.uti_original] + + if burst: + photos = [p for p in photos if p.burst] + elif not_burst: + photos = [p for p in photos if not p.burst] + + if live: + photos = [p for p in photos if p.live_photo] + elif not_live: + photos = [p for p in photos if not p.live_photo] + + if portrait: + photos = [p for p in photos if p.portrait] + elif not_portrait: + photos = [p for p in photos if not p.portrait] + + if screenshot: + photos = [p for p in photos if p.screenshot] + elif not_screenshot: + photos = [p for p in photos if not p.screenshot] + + if slow_mo: + photos = [p for p in photos if p.slow_mo] + elif not_slow_mo: + photos = [p for p in photos if not p.slow_mo] + + if time_lapse: + photos = [p for p in photos if p.time_lapse] + elif not_time_lapse: + photos = [p for p in photos if not p.time_lapse] + + if hdr: + photos = [p for p in photos if p.hdr] + elif not_hdr: + photos = [p for p in photos if not p.hdr] + + if selfie: + photos = [p for p in photos if p.selfie] + elif not_selfie: + photos = [p for p in photos if not p.selfie] + + if panorama: + photos = [p for p in photos if p.panorama] + elif not_panorama: + photos = [p for p in photos if not p.panorama] + + if cloudasset: + photos = [p for p in photos if p.iscloudasset] + elif not_cloudasset: + photos = [p for p in photos if not p.iscloudasset] + + if incloud: + photos = [p for p in photos if p.incloud] + elif not_incloud: + photos = [p for p in photos if not p.incloud] + + if has_raw: + photos = [p for p in photos if p.has_raw] + + if has_comment: + photos = [p for p in photos if p.comments] + elif no_comment: + photos = [p for p in photos if not p.comments] + + if has_likes: + photos = [p for p in photos if p.likes] + elif no_likes: + photos = [p for p in photos if not p.likes] + + if is_reference: + photos = [p for p in photos if p.isreference] + + return photos + + +def get_photos_by_attribute(photos, attribute, values, ignore_case): + """Search for photos based on values being in PhotoInfo.attribute + + Args: + photos: a list of PhotoInfo objects + attribute: str, name of PhotoInfo attribute to search (e.g. keywords, persons, etc) + values: list of values to search in property + ignore_case: ignore case when searching + + Returns: + list of PhotoInfo objects matching search criteria + """ + photos_search = [] + if ignore_case: + # case-insensitive + for x in values: + x = x.lower() + photos_search.extend( + p + for p in photos + if x in [attr.lower() for attr in getattr(p, attribute)] + ) + else: + for x in values: + photos_search.extend(p for p in photos if x in getattr(p, attribute)) + return photos_search + + +def export_photo( + photo=None, + dest=None, + verbose=None, + export_by_date=None, + sidecar=None, + sidecar_drop_ext=False, + update=None, + ignore_signature=None, + export_as_hardlink=None, + overwrite=None, + export_edited=None, + skip_original_if_edited=None, + original_name=None, + export_live=None, + download_missing=None, + exiftool=None, + exiftool_merge_keywords=False, + exiftool_merge_persons=False, + directory=None, + filename_template=None, + export_raw=None, + album_keyword=None, + person_keyword=None, + keyword_template=None, + description_template=None, + export_db=None, + fileutil=FileUtil, + dry_run=None, + touch_file=None, + edited_suffix="_edited", + original_suffix="", + use_photos_export=False, + convert_to_jpeg=False, + jpeg_quality=1.0, + ignore_date_modified=False, + use_photokit=False, + exiftool_option=None, + strip=False, + jpeg_ext=None, +): + """Helper function for export that does the actual export + + Args: + photo: PhotoInfo object + dest: destination path as string + verbose: boolean; print verbose output + export_by_date: boolean; create export folder in form dest/YYYY/MM/DD + sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export + sidecar_drop_ext: boolean; if True, drops photo extension from sidecar name + export_as_hardlink: boolean; hardlink files instead of copying them + overwrite: boolean; overwrite dest file if it already exists + original_name: boolean; use original filename instead of current filename + export_live: boolean; also export live video component if photo is a live photo + live video will have same name as photo but with .mov extension + download_missing: attempt download of missing iCloud photos + exiftool: use exiftool to write EXIF metadata directly to exported photo + directory: template used to determine output directory + filename_template: template use to determine output file + export_raw: boolean; if True exports raw image associate with the photo + export_edited: boolean; if True exports edited version of photo if there is one + skip_original_if_edited: boolean; if True does not export original if photo has been edited + album_keyword: boolean; if True, exports album names as keywords in metadata + person_keyword: boolean; if True, exports person names as keywords in metadata + keyword_template: list of strings; if provided use rendered template strings as keywords + description_template: string; optional template string that will be rendered for use as photo description + export_db: export database instance compatible with ExportDB_ABC + fileutil: file util class compatible with FileUtilABC + dry_run: boolean; if True, doesn't actually export or update any files + touch_file: boolean; sets file's modification time to match photo date + use_photos_export: boolean; if True forces the use of AppleScript to export even if photo not missing + convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg + jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression. + ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set + exiftool_option: optional list flags (e.g. ["-m", "-F"]) to pass to exiftool + exiftool_merge_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool) + exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool) + jpeg_ext: if not None, specify the extension to use for all JPEG images on export + + Returns: + list of path(s) of exported photo or None if photo was missing + + Raises: + ValueError on invalid filename_template + """ + global VERBOSE + VERBOSE = bool(verbose) + + results = ExportResults() + + export_original = not (skip_original_if_edited and photo.hasadjustments) + + # can't export edited if photo doesn't have edited versions + export_edited = export_edited if photo.hasadjustments else False + + # slow_mo photos will always have hasadjustments=True even if not edited + if photo.hasadjustments and photo.path_edited is None: + if photo.slow_mo: + export_original = True + export_edited = False + elif not download_missing: + # requested edited version but it's missing, download original + export_original = True + export_edited = False + verbose_( + f"Edited file for {photo.original_filename} is missing, exporting original" + ) + + # check for missing photos before downloading + missing_original = False + missing_edited = False + if download_missing: + if ( + (photo.ismissing or photo.path is None) + and not photo.iscloudasset + and not photo.incloud + ): + missing_original = True + if ( + photo.hasadjustments + and photo.path_edited is None + and not photo.iscloudasset + and not photo.incloud + ): + missing_edited = True + else: + if photo.ismissing or photo.path is None: + missing_original = True + if photo.hasadjustments and photo.path_edited is None: + missing_edited = True + + filenames = get_filenames_from_template( + photo, filename_template, original_name, strip=strip + ) + for filename in filenames: + rendered_suffix = "" + if original_suffix: + try: + rendered_suffix, unmatched = photo.render_template( + original_suffix, filename=True, strip=strip + ) + except ValueError: + raise click.BadOptionUsage( + "original_suffix", + f"Invalid template for --original-suffix '{original_suffix}'", + ) + if not rendered_suffix or unmatched: + raise click.BadOptionUsage( + "original_suffix", + f"Invalid template for --original-suffix '{original_suffix}': results={rendered_suffix} unmatched={unmatched}", + ) + if len(rendered_suffix) > 1: + raise click.BadOptionUsage( + "original_suffix", + f"Invalid template for --original-suffix: may not use multi-valued templates: '{original_suffix}': results={rendered_suffix}", + ) + rendered_suffix = rendered_suffix[0] + + original_filename = pathlib.Path(filename) + file_ext = ( + "." + jpeg_ext + if jpeg_ext and (photo.uti == "public.jpeg" or convert_to_jpeg) + else ".jpeg" + if convert_to_jpeg and photo.uti != "public.jpeg" + else original_filename.suffix + ) + original_filename = ( + original_filename.parent + / f"{original_filename.stem}{rendered_suffix}{file_ext}" + ) + original_filename = str(original_filename) + + verbose_( + f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename}" + ) + + dest_paths = get_dirnames_from_template( + photo, directory, export_by_date, dest, dry_run, strip=strip + ) + + sidecar = [s.lower() for s in sidecar] + sidecar_flags = 0 + if "json" in sidecar: + sidecar_flags |= SIDECAR_JSON + if "xmp" in sidecar: + sidecar_flags |= SIDECAR_XMP + if "exiftool" in sidecar: + sidecar_flags |= SIDECAR_EXIFTOOL + + # if download_missing and the photo is missing or path doesn't exist, + # try to download with Photos + use_photos_export = use_photos_export or ( + download_missing + and ( + photo.ismissing + or photo.path is None + or (export_edited and photo.path_edited is None) + ) + ) + + # export the photo to each path in dest_paths + for dest_path in dest_paths: + # TODO: if --skip-original-if-edited, it's possible edited version is on disk but + # original is missing, in which case we should download the edited version + if export_original: + if missing_original: + space = " " if not verbose else "" + verbose_( + f"{space}Skipping missing photo {photo.original_filename} ({photo.uuid})" + ) + results.missing.append( + str(pathlib.Path(dest_path) / original_filename) + ) + elif photo.intrash and (not photo.path or use_photos_export): + # skip deleted files if they're missing or using use_photos_export + # as AppleScript/PhotoKit cannot export deleted photos + space = " " if not verbose else "" + verbose_( + f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})" + ) + results.missing.append( + str(pathlib.Path(dest_path) / original_filename) + ) + else: + try: + export_results = photo.export2( + dest_path, + original_filename, + sidecar=sidecar_flags, + sidecar_drop_ext=sidecar_drop_ext, + live_photo=export_live, + raw_photo=export_raw, + export_as_hardlink=export_as_hardlink, + overwrite=overwrite, + use_photos_export=use_photos_export, + exiftool=exiftool, + merge_exif_keywords=exiftool_merge_keywords, + merge_exif_persons=exiftool_merge_persons, + use_albums_as_keywords=album_keyword, + use_persons_as_keywords=person_keyword, + keyword_template=keyword_template, + description_template=description_template, + update=update, + ignore_signature=ignore_signature, + export_db=export_db, + fileutil=fileutil, + dry_run=dry_run, + touch_file=touch_file, + convert_to_jpeg=convert_to_jpeg, + jpeg_quality=jpeg_quality, + ignore_date_modified=ignore_date_modified, + use_photokit=use_photokit, + verbose=verbose_, + exiftool_flags=exiftool_option, + jpeg_ext=jpeg_ext, + ) + results += export_results + for warning_ in export_results.exiftool_warning: + verbose_( + f"exiftool warning for file {warning_[0]}: {warning_[1]}" + ) + for error_ in export_results.exiftool_error: + click.echo( + click.style( + f"exiftool error for file {error_[0]}: {error_[1]}", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + for error_ in export_results.error: + click.echo( + click.style( + f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + + except Exception as e: + click.echo( + click.style( + f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {original_filename}: {e}", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + results.error.append( + (str(pathlib.Path(dest) / original_filename), e) + ) + else: + verbose_(f"Skipping original version of {photo.original_filename}") + + # if export-edited, also export the edited version + # verify the photo has adjustments and valid path to avoid raising an exception + if export_edited and photo.hasadjustments: + edited_filename = pathlib.Path(filename) + edited_ext = ( + "." + jpeg_ext + if jpeg_ext and photo.uti_edited == "public.jpeg" + else "." + get_preferred_uti_extension(photo.uti_edited) + if photo.uti_edited + else pathlib.Path(photo.path_edited).suffix + if photo.path_edited + else pathlib.Path(photo.filename).suffix + ) + # Big Sur uses .heic for some edited photos so need to check + # if extension isn't jpeg/jpg and using --convert-to-jpeg + if convert_to_jpeg and edited_ext.lower() not in [".jpg", ".jpeg"]: + edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg" + + if edited_suffix: + try: + rendered_suffix, unmatched = photo.render_template( + edited_suffix, filename=True, strip=strip + ) + except ValueError: + raise click.BadOptionUsage( + "edited_suffix", + f"Invalid template for --edited-suffix '{edited_suffix}'", + ) + if not rendered_suffix or unmatched: + raise click.BadOptionUsage( + "edited_suffix", + f"Invalid template for --edited-suffix '{edited_suffix}': results={rendered_suffix} unmatched={unmatched}", + ) + if len(rendered_suffix) > 1: + raise click.BadOptionUsage( + "edited_suffix", + f"Invalid template for --edited-suffix: may not use multi-valued templates: '{edited_suffix}': results={rendered_suffix}", + ) + rendered_suffix = rendered_suffix[0] + + edited_filename = ( + f"{edited_filename.stem}{rendered_suffix}{edited_ext}" + ) + else: + edited_filename = f"{edited_filename.stem}{edited_ext}" + + verbose_( + f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}" + ) + if missing_edited: + space = " " if not verbose else "" + verbose_( + f"{space}Skipping missing edited photo for {edited_filename}" + ) + results.missing.append( + str(pathlib.Path(dest_path) / edited_filename) + ) + elif photo.intrash and (not photo.path_edited or use_photos_export): + # skip deleted files if they're missing or using use_photos_export + # as AppleScript/PhotoKit cannot export deleted photos + space = " " if not verbose else "" + verbose_( + f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})" + ) + results.missing.append( + str(pathlib.Path(dest_path) / edited_filename) + ) + + else: + try: + export_results_edited = photo.export2( + dest_path, + edited_filename, + sidecar=sidecar_flags, + sidecar_drop_ext=sidecar_drop_ext, + export_as_hardlink=export_as_hardlink, + overwrite=overwrite, + edited=True, + use_photos_export=use_photos_export, + exiftool=exiftool, + merge_exif_keywords=exiftool_merge_keywords, + merge_exif_persons=exiftool_merge_persons, + use_albums_as_keywords=album_keyword, + use_persons_as_keywords=person_keyword, + keyword_template=keyword_template, + description_template=description_template, + update=update, + ignore_signature=ignore_signature, + export_db=export_db, + fileutil=fileutil, + dry_run=dry_run, + touch_file=touch_file, + convert_to_jpeg=convert_to_jpeg, + jpeg_quality=jpeg_quality, + ignore_date_modified=ignore_date_modified, + use_photokit=use_photokit, + verbose=verbose_, + exiftool_flags=exiftool_option, + jpeg_ext=jpeg_ext, + ) + results += export_results_edited + for warning_ in export_results_edited.exiftool_warning: + verbose_( + f"exiftool warning for file {warning_[0]}: {warning_[1]}" + ) + for error_ in export_results_edited.exiftool_error: + click.echo( + click.style( + f"exiftool error for file {error_[0]}: {error_[1]}", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + for error_ in export_results_edited.error: + click.echo( + click.style( + f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + except Exception as e: + click.echo( + click.style( + f"Error exporting edited photo ({photo.uuid}: {photo.original_filename}) {filename} as {edited_filename}: {e}", + fg=CLI_COLOR_ERROR, + ), + err=True, + ) + results.error.append( + (str(pathlib.Path(dest) / edited_filename), e) + ) + + if verbose: + if update: + for new in results.new: + verbose_(f"Exported new file {new}") + for updated in results.updated: + verbose_(f"Exported updated file {updated}") + for skipped in results.skipped: + verbose_(f"Skipped up to date file {skipped}") + else: + for exported in results.exported: + verbose_(f"Exported {exported}") + for touched in results.touched: + verbose_(f"Touched date on file {touched}") + + return results + + +def get_filenames_from_template(photo, filename_template, original_name, strip=False): + """get list of export filenames for a photo + + Args: + photo: a PhotoInfo instance + filename_template: a PhotoTemplate template string, may be None + original_name: boolean; if True, use photo's original filename instead of current filename + + Returns: + list of filenames + + Raises: + click.BadOptionUsage if template is invalid + """ + if filename_template: + photo_ext = pathlib.Path(photo.original_filename).suffix + try: + filenames, unmatched = photo.render_template( + filename_template, path_sep="_", filename=True, strip=strip + ) + except ValueError: + raise click.BadOptionUsage( + "filename_template", f"Invalid template '{filename_template}'" + ) + if not filenames or unmatched: + raise click.BadOptionUsage( + "filename_template", + f"Invalid template '{filename_template}': results={filenames} unmatched={unmatched}", + ) + filenames = [f"{file_}{photo_ext}" for file_ in filenames] + else: + filenames = ( + [photo.original_filename] + if (original_name and (photo.original_filename is not None)) + else [photo.filename] + ) + + filenames = [sanitize_filename(filename) for filename in filenames] + return filenames + + +def get_dirnames_from_template( + photo, directory, export_by_date, dest, dry_run, strip=False +): + """get list of directories to export a photo into, creates directories if they don't exist + + Args: + photo: a PhotoInstance object + directory: a PhotoTemplate template string, may be None + export_by_date: boolean; if True, creates output directories in form YYYY-MM-DD + dest: top-level destination directory + dry_run: boolean; if True, runs in dry-run mode and does not create output directories + + Returns: + list of export directories + + Raises: + click.BadOptionUsage if template is invalid + """ + + if export_by_date: + date_created = DateTimeFormatter(photo.date) + dest_path = os.path.join( + dest, date_created.year, date_created.mm, date_created.dd + ) + if not (dry_run or os.path.isdir(dest_path)): + os.makedirs(dest_path) + dest_paths = [dest_path] + elif directory: + # got a directory template, render it and check results are valid + try: + dirnames, unmatched = photo.render_template( + directory, dirname=True, strip=strip + ) + except ValueError: + raise click.BadOptionUsage("directory", f"Invalid template '{directory}'") + if not dirnames or unmatched: + raise click.BadOptionUsage( + "directory", + f"Invalid template '{directory}': results={dirnames} unmatched={unmatched}", + ) + + dest_paths = [] + for dirname in dirnames: + dirname = sanitize_filepath(dirname) + dest_path = os.path.join(dest, dirname) + if not is_valid_filepath(dest_path): + raise ValueError(f"Invalid file path: '{dest_path}'") + if not dry_run and not os.path.isdir(dest_path): + os.makedirs(dest_path) + dest_paths.append(dest_path) + else: + dest_paths = [dest] + return dest_paths + + +def find_files_in_branch(pathname, filename): + """Search a directory branch to find file(s) named filename + The branch searched includes all folders below pathname and + the parent tree of pathname but not pathname itself. + + e.g. find filename in children folders and parent folders + + Args: + pathname: str, full path of directory to search + filename: str, filename to search for + + Returns: list of full paths to any matching files + """ + + pathname = pathlib.Path(pathname).resolve() + files = [] + + # walk down the tree + for root, _, filenames in os.walk(pathname): + # for directory in directories: + # print(os.path.join(root, directory)) + for fname in filenames: + if fname == filename and pathlib.Path(root) != pathname: + files.append(os.path.join(root, fname)) + + # walk up the tree + path = pathlib.Path(pathname) + for root in path.parents: + filenames = os.listdir(root) + for fname in filenames: + filepath = os.path.join(root, fname) + if fname == filename and os.path.isfile(filepath): + files.append(os.path.join(root, fname)) + + return files + + +def load_uuid_from_file(filename): + """Load UUIDs from file. Does not validate UUIDs. + Format is 1 UUID per line, any line beginning with # is ignored. + Whitespace is stripped. + + Arguments: + filename: file name of the file containing UUIDs + + Returns: + list of UUIDs or empty list of no UUIDs in file + + Raises: + FileNotFoundError if file does not exist + """ + + if not pathlib.Path(filename).is_file(): + raise FileNotFoundError(f"Could not find file {filename}") + + uuid = [] + with open(filename, "r") as uuid_file: + for line in uuid_file: + line = line.strip() + if len(line) and line[0] != "#": + uuid.append(line) + return uuid + + +def write_export_report(report_file, results): + + """write CSV report with results from export + + Args: + report_file: path to report file + results: ExportResults object + """ + + # Collect results for reporting + # TODO: pull this in a separate write_report function + all_results = { + result: { + "filename": result, + "exported": 0, + "new": 0, + "updated": 0, + "skipped": 0, + "exif_updated": 0, + "touched": 0, + "converted_to_jpeg": 0, + "sidecar_xmp": 0, + "sidecar_json": 0, + "sidecar_exiftool": 0, + "missing": 0, + "error": "", + "exiftool_warning": "", + "exiftool_error": "", + "extended_attributes_written": 0, + "extended_attributes_skipped": 0, + } + for result in results.all_files() + } + + for result in results.exported: + all_results[result]["exported"] = 1 + + for result in results.new: + all_results[result]["new"] = 1 + + for result in results.updated: + all_results[result]["updated"] = 1 + + for result in results.skipped: + all_results[result]["skipped"] = 1 + + for result in results.exif_updated: + all_results[result]["exif_updated"] = 1 + + for result in results.touched: + all_results[result]["touched"] = 1 + + for result in results.converted_to_jpeg: + all_results[result]["converted_to_jpeg"] = 1 + + for result in results.sidecar_xmp_written: + all_results[result]["sidecar_xmp"] = 1 + all_results[result]["exported"] = 1 + + for result in results.sidecar_xmp_skipped: + all_results[result]["sidecar_xmp"] = 1 + all_results[result]["skipped"] = 1 + + for result in results.sidecar_json_written: + all_results[result]["sidecar_json"] = 1 + all_results[result]["exported"] = 1 + + for result in results.sidecar_json_skipped: + all_results[result]["sidecar_json"] = 1 + all_results[result]["skipped"] = 1 + + for result in results.sidecar_exiftool_written: + all_results[result]["sidecar_exiftool"] = 1 + all_results[result]["exported"] = 1 + + for result in results.sidecar_exiftool_skipped: + all_results[result]["sidecar_exiftool"] = 1 + all_results[result]["skipped"] = 1 + + for result in results.missing: + all_results[result]["missing"] = 1 + + for result in results.error: + all_results[result[0]]["error"] = result[1] + + for result in results.exiftool_warning: + all_results[result[0]]["exiftool_warning"] = result[1] + + for result in results.exiftool_error: + all_results[result[0]]["exiftool_error"] = result[1] + + for result in results.xattr_written: + all_results[result]["extended_attributes_written"] = 1 + + for result in results.xattr_skipped: + all_results[result]["extended_attributes_skipped"] = 1 + + report_columns = [ + "filename", + "exported", + "new", + "updated", + "skipped", + "exif_updated", + "touched", + "converted_to_jpeg", + "sidecar_xmp", + "sidecar_json", + "sidecar_exiftool", + "missing", + "error", + "exiftool_warning", + "exiftool_error", + "extended_attributes_written", + "extended_attributes_skipped", + ] + + try: + with open(report_file, "w") as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=report_columns) + writer.writeheader() + for data in [result for result in all_results.values()]: + writer.writerow(data) + except IOError: + click.echo( + click.style("Could not open output file for writing", fg=CLI_COLOR_ERROR), + err=True, + ) + raise click.Abort() + + +def cleanup_files(dest_path, files_to_keep, fileutil): + """cleanup dest_path by deleting and files and empty directories + not in files_to_keep + + Args: + dest_path: path to directory to clean + files_to_keep: list of full file paths to keep (not delete) + fileutile: FileUtil object + + Returns: + tuple of (number of files deleted, number of directories deleted) + """ + keepers = {filename.lower(): 1 for filename in files_to_keep} + + deleted_files = 0 + for p in pathlib.Path(dest_path).rglob("*"): + path = str(p).lower() + if p.is_file() and path not in keepers: + verbose_(f"Deleting {p}") + fileutil.unlink(p) + deleted_files += 1 + + # delete empty directories + deleted_dirs = 0 + for p in pathlib.Path(dest_path).rglob("*"): + path = str(p).lower() + # if directory and directory is empty + if p.is_dir() and not next(p.iterdir(), False): + verbose_(f"Deleting empty directory {p}") + fileutil.rmdir(p) + deleted_dirs += 1 + + return (deleted_files, deleted_dirs) + + +def write_finder_tags( + photo, + files, + keywords=False, + keyword_template=None, + album_keyword=None, + person_keyword=None, + exiftool_merge_keywords=None, + finder_tag_template=None, + strip=False, +): + """Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written + + Args: + photo: a PhotoInfo object + files: list of file paths to write Finder tags to + keywords: if True, sets Finder tags to all keywords including any evaluated from keyword_template, album_keyword, person_keyword, exiftool_merge_keywords + keyword_template: list of keyword templates to evaluate for determining keywords + album_keyword: if True, use album names as keywords + person_keyword: if True, use person in image as keywords + exiftool_merge_keywords: if True, include any keywords in the exif data of the source image as keywords + finder_tag_template: list of templates to evaluate for determining Finder tags + + Returns: + (list of file paths that were updated with new Finder tags, list of file paths skipped because Finder tags didn't need updating) + """ + + tags = [] + written = [] + skipped = [] + if keywords: + # match whatever keywords would've been used in --exiftool or --sidecar + exif = photo._exiftool_dict( + use_albums_as_keywords=album_keyword, + use_persons_as_keywords=person_keyword, + keyword_template=keyword_template, + merge_exif_keywords=exiftool_merge_keywords, + ) + try: + if exif["IPTC:Keywords"]: + tags.extend(exif["IPTC:Keywords"]) + except KeyError: + pass + + if finder_tag_template: + rendered_tags = [] + for template_str in finder_tag_template: + try: + rendered, unmatched = photo.render_template( + template_str, + none_str=_OSXPHOTOS_NONE_SENTINEL, + path_sep="/", + strip=strip, + ) + except ValueError: + raise click.BadOptionUsage( + "finder_tag_template", + f"Invalid template for --finder-tag-template': {template_str}", + ) + + if unmatched: + click.echo( + click.style( + f"Warning: unmatched template substitution for template: {template_str} {unmatched}", + fg=CLI_COLOR_WARNING, + ), + err=True, + ) + rendered_tags.extend(rendered) + + # filter out any template values that didn't match by looking for sentinel + rendered_tags = [ + tag for tag in rendered_tags if _OSXPHOTOS_NONE_SENTINEL not in tag + ] + tags.extend(rendered_tags) + + tags = [osxmetadata.Tag(tag) for tag in set(tags)] + for f in files: + md = osxmetadata.OSXMetaData(f) + if sorted(md.tags) != sorted(tags): + verbose_(f"Writing Finder tags to {f}") + md.tags = tags + written.append(f) + else: + verbose_(f"Skipping Finder tags for {f}: nothing to do") + skipped.append(f) + + return (written, skipped) + + +def write_extended_attributes(photo, files, xattr_template, strip=False): + """ Writes extended attributes to exported files + + Args: + photo: a PhotoInfo object + xattr_template: list of tuples: (attribute name, attribute template) + + Returns: + tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating) + """ + + attributes = {} + for xattr, template_str in xattr_template: + try: + rendered, unmatched = photo.render_template( + template_str, + none_str=_OSXPHOTOS_NONE_SENTINEL, + path_sep="/", + strip=strip, + ) + except ValueError: + raise click.BadOptionUsage( + "xattr_template", + f"Invalid template for --xattr-template': {template_str}", + ) + if unmatched: + click.echo( + click.style( + f"Warning: unmatched template substitution for template: {template_str} {unmatched}", + fg=CLI_COLOR_WARNING, + ), + err=True, + ) + # filter out any template values that didn't match by looking for sentinel + rendered = [ + value for value in rendered if _OSXPHOTOS_NONE_SENTINEL not in value + ] + try: + attributes[xattr].extend(rendered) + except KeyError: + attributes[xattr] = rendered + + written = set() + skipped = set() + for f in files: + md = osxmetadata.OSXMetaData(f) + for attr, value in attributes.items(): + islist = osxmetadata.ATTRIBUTES[attr].list + if value: + value = ", ".join(value) if not islist else sorted(value) + file_value = md.get_attribute(attr) + + if file_value and islist: + file_value = sorted(file_value) + + if (not file_value and not value) or file_value == value: + # if both not set or both equal, nothing to do + # get_attribute returns None if not set and value will be [] if not set so can't directly compare + verbose_(f"Skipping extended attribute {attr} for {f}: nothing to do") + skipped.add(f) + else: + verbose_(f"Writing extended attribute {attr} to {f}") + md.set_attribute(attr, value) + written.add(f) + + return list(written), [f for f in skipped if f not in written] + + +@cli.command(hidden=True) +@DB_OPTION +@DB_ARGUMENT +@click.option( + "--dump", + metavar="ATTR", + help="Name of PhotosDB attribute to print; " + + "can also use albums, persons, keywords, photos to dump related attributes.", + multiple=True, +) +@click.option( + "--uuid", + metavar="UUID", + help="Use with '--dump photos' to dump only certain UUIDs", + multiple=True, +) +@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.") +@click.pass_obj +@click.pass_context +def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose): + """ Print out debug info """ + + global VERBOSE + VERBOSE = bool(verbose) + + db = get_photos_db(*photos_library, db, cli_obj.db) + if db is None: + click.echo(cli.commands["debug-dump"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + start_t = time.perf_counter() + print(f"Opening database: {db}") + photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_) + stop_t = time.perf_counter() + print(f"Done; took {(stop_t-start_t):.2f} seconds") + + for attr in dump: + if attr == "albums": + print("_dbalbums_album:") + pprint.pprint(photosdb._dbalbums_album) + print("_dbalbums_uuid:") + pprint.pprint(photosdb._dbalbums_uuid) + print("_dbalbum_details:") + pprint.pprint(photosdb._dbalbum_details) + print("_dbalbum_folders:") + pprint.pprint(photosdb._dbalbum_folders) + print("_dbfolder_details:") + pprint.pprint(photosdb._dbfolder_details) + elif attr == "keywords": + print("_dbkeywords_keyword:") + pprint.pprint(photosdb._dbkeywords_keyword) + print("_dbkeywords_uuid:") + pprint.pprint(photosdb._dbkeywords_uuid) + elif attr == "persons": + print("_dbfaces_uuid:") + pprint.pprint(photosdb._dbfaces_uuid) + print("_dbfaces_pk:") + pprint.pprint(photosdb._dbfaces_pk) + print("_dbpersons_pk:") + pprint.pprint(photosdb._dbpersons_pk) + print("_dbpersons_fullname:") + pprint.pprint(photosdb._dbpersons_fullname) + elif attr == "photos": + if uuid: + for uuid_ in uuid: + print(f"_dbphotos['{uuid_}']:") + try: + pprint.pprint(photosdb._dbphotos[uuid_]) + except KeyError: + print(f"Did not find uuid {uuid_} in _dbphotos") + else: + print("_dbphotos:") + pprint.pprint(photosdb._dbphotos) + else: + try: + val = getattr(photosdb, attr) + print(f"{attr}:") + pprint.pprint(val) + except Exception: + print(f"Did not find attribute {attr} in PhotosDB") + + +@cli.command() +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT +@click.pass_obj +@click.pass_context +def keywords(ctx, cli_obj, db, json_, photos_library): + """ Print out keywords found in the Photos library. """ + + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + db = get_photos_db(*photos_library, db, cli_db) + if db is None: + click.echo(cli.commands["keywords"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + photosdb = osxphotos.PhotosDB(dbfile=db) + keywords = {"keywords": photosdb.keywords_as_dict} + if json_ or cli_obj.json: + click.echo(json.dumps(keywords, ensure_ascii=False)) + else: + click.echo(yaml.dump(keywords, sort_keys=False, allow_unicode=True)) + + +@cli.command() +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT +@click.pass_obj +@click.pass_context +def albums(ctx, cli_obj, db, json_, photos_library): + """ Print out albums found in the Photos library. """ + + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + db = get_photos_db(*photos_library, db, cli_db) + if db is None: + click.echo(cli.commands["albums"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + photosdb = osxphotos.PhotosDB(dbfile=db) + albums = {"albums": photosdb.albums_as_dict} + if photosdb.db_version > _PHOTOS_4_VERSION: + albums["shared albums"] = photosdb.albums_shared_as_dict + + if json_ or cli_obj.json: + click.echo(json.dumps(albums, ensure_ascii=False)) + else: + click.echo(yaml.dump(albums, sort_keys=False, allow_unicode=True)) + + +@cli.command() +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT +@click.pass_obj +@click.pass_context +def persons(ctx, cli_obj, db, json_, photos_library): + """ Print out persons (faces) found in the Photos library. """ + + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + db = get_photos_db(*photos_library, db, cli_db) + if db is None: + click.echo(cli.commands["persons"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + photosdb = osxphotos.PhotosDB(dbfile=db) + persons = {"persons": photosdb.persons_as_dict} + if json_ or cli_obj.json: + click.echo(json.dumps(persons, ensure_ascii=False)) + else: + click.echo(yaml.dump(persons, sort_keys=False, allow_unicode=True)) + + +@cli.command() +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT +@click.pass_obj +@click.pass_context +def labels(ctx, cli_obj, db, json_, photos_library): + """ Print out image classification labels found in the Photos library. """ + + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + db = get_photos_db(*photos_library, db, cli_db) + if db is None: + click.echo(cli.commands["labels"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + photosdb = osxphotos.PhotosDB(dbfile=db) + labels = {"labels": photosdb.labels_as_dict} + if json_ or cli_obj.json: + click.echo(json.dumps(labels, ensure_ascii=False)) + else: + click.echo(yaml.dump(labels, sort_keys=False, allow_unicode=True)) + + +@cli.command() +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT +@click.pass_obj +@click.pass_context +def info(ctx, cli_obj, db, json_, photos_library): + """ Print out descriptive info of the Photos library database. """ + + db = get_photos_db(*photos_library, db, cli_obj.db) + if db is None: + click.echo(cli.commands["info"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + photosdb = osxphotos.PhotosDB(dbfile=db) + info = {"database_path": photosdb.db_path, "database_version": photosdb.db_version} + photos = photosdb.photos(movies=False) + not_shared_photos = [p for p in photos if not p.shared] + info["photo_count"] = len(not_shared_photos) + + hidden = [p for p in photos if p.hidden] + info["hidden_photo_count"] = len(hidden) + + movies = photosdb.photos(images=False, movies=True) + not_shared_movies = [p for p in movies if not p.shared] + info["movie_count"] = len(not_shared_movies) + + if photosdb.db_version > _PHOTOS_4_VERSION: + shared_photos = [p for p in photos if p.shared] + info["shared_photo_count"] = len(shared_photos) + + shared_movies = [p for p in movies if p.shared] + info["shared_movie_count"] = len(shared_movies) + + keywords = photosdb.keywords_as_dict + info["keywords_count"] = len(keywords) + info["keywords"] = keywords + + albums = photosdb.albums_as_dict + info["albums_count"] = len(albums) + info["albums"] = albums + + if photosdb.db_version > _PHOTOS_4_VERSION: + albums_shared = photosdb.albums_shared_as_dict + info["shared_albums_count"] = len(albums_shared) + info["shared_albums"] = albums_shared + + persons = photosdb.persons_as_dict + + info["persons_count"] = len(persons) + info["persons"] = persons + + if cli_obj.json or json_: + click.echo(json.dumps(info, ensure_ascii=False)) + else: + click.echo(yaml.dump(info, sort_keys=False, allow_unicode=True)) + + +@cli.command() +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT +@click.pass_obj +@click.pass_context +def places(ctx, cli_obj, db, json_, photos_library): + """ Print out places found in the Photos library. """ + + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + db = get_photos_db(*photos_library, db, cli_db) + if db is None: + click.echo(cli.commands["places"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + photosdb = osxphotos.PhotosDB(dbfile=db) + place_names = {} + for photo in photosdb.photos(movies=True): + if photo.place: + try: + place_names[photo.place.name] += 1 + except Exception: + place_names[photo.place.name] = 1 + else: + try: + place_names[_UNKNOWN_PLACE] += 1 + except Exception: + place_names[_UNKNOWN_PLACE] = 1 + + # sort by place count + places = { + "places": { + name: place_names[name] + for name in sorted( + place_names.keys(), key=lambda key: place_names[key], reverse=True + ) + } + } + + # below needed for to make CliRunner work for testing + cli_json = cli_obj.json if cli_obj is not None else None + if json_ or cli_json: + click.echo(json.dumps(places, ensure_ascii=False)) + else: + click.echo(yaml.dump(places, sort_keys=False, allow_unicode=True)) + + +@cli.command() +@DB_OPTION +@JSON_OPTION +@deleted_options +@DB_ARGUMENT +@click.pass_obj +@click.pass_context +def dump(ctx, cli_obj, db, json_, deleted, deleted_only, photos_library): + """ Print list of all photos & associated info from the Photos library. """ + + db = get_photos_db(*photos_library, db, cli_obj.db) + if db is None: + click.echo(cli.commands["dump"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) + _list_libraries() + return + + # check exclusive options + if deleted and deleted_only: + click.echo("Incompatible dump options", err=True) + click.echo(cli.commands["dump"].get_help(ctx), err=True) + return + + photosdb = osxphotos.PhotosDB(dbfile=db) + if deleted or deleted_only: + photos = photosdb.photos(movies=True, intrash=True) + else: + photos = [] + if not deleted_only: + photos += photosdb.photos(movies=True) + + print_photo_info(photos, json_ or cli_obj.json) + + +@cli.command(name="list") +@JSON_OPTION +@click.pass_obj +@click.pass_context +def list_libraries(ctx, cli_obj, json_): + """ Print list of Photos libraries found on the system. """ + + # implemented in _list_libraries so it can be called by other CLI functions + # without errors due to passing ctx and cli_obj + _list_libraries(json_=json_ or cli_obj.json, error=False) + + +def _list_libraries(json_=False, error=True): + """Print list of Photos libraries found on the system. + If json_ == True, print output as JSON (default = False)""" + + photo_libs = osxphotos.utils.list_photo_libraries() + sys_lib = osxphotos.utils.get_system_library_path() + last_lib = osxphotos.utils.get_last_library_path() + + if json_: + libs = { + "photo_libraries": photo_libs, + "system_library": sys_lib, + "last_library": last_lib, + } + click.echo(json.dumps(libs, ensure_ascii=False)) + else: + last_lib_flag = sys_lib_flag = False + + for lib in photo_libs: + if lib == sys_lib: + click.echo(f"(*)\t{lib}", err=error) + sys_lib_flag = True + elif lib == last_lib: + click.echo(f"(#)\t{lib}", err=error) + last_lib_flag = True + else: + click.echo(f"\t{lib}", err=error) + + if sys_lib_flag or last_lib_flag: + click.echo("\n", err=error) + if sys_lib_flag: + click.echo("(*)\tSystem Photos Library", err=error) + if last_lib_flag: + click.echo("(#)\tLast opened Photos Library", err=error) + + +@cli.command(name="about") +@click.pass_obj +@click.pass_context +def about(ctx, cli_obj): + """ Print information about osxphotos including license. """ + license = """ +MIT License + +Copyright (c) 2019-2021 Rhet Turnbull + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + click.echo(f"osxphotos, version {__version__}") + click.echo("") + click.echo(f"Source code available at: {OSXPHOTOS_URL}") + click.echo(license) diff --git a/osxphotos/cli_help.py b/osxphotos/cli_help.py new file mode 100644 index 00000000..759d4b7c --- /dev/null +++ b/osxphotos/cli_help.py @@ -0,0 +1,286 @@ +"""Help text helper class for osxphotos CLI """ + +import click +import osxmetadata + +from ._constants import ( + EXTENDED_ATTRIBUTE_NAMES, + EXTENDED_ATTRIBUTE_NAMES_QUOTED, + OSXPHOTOS_EXPORT_DB, +) +from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED + + +class ExportCommand(click.Command): + """ Custom click.Command that overrides get_help() to show additional help info for export """ + + def get_help(self, ctx): + help_text = super().get_help(ctx) + formatter = click.HelpFormatter() + + # passed to click.HelpFormatter.write_dl for formatting + + formatter.write("\n\n") + formatter.write_text("** Export **") + formatter.write_text( + "When exporting photos, osxphotos creates a database in the top-level " + + f"export folder called '{OSXPHOTOS_EXPORT_DB}'. This database preserves state information " + + "used for determining which files need to be updated when run with --update. It is recommended " + + "that if you later move the export folder tree you also move the database file." + ) + formatter.write("\n") + formatter.write_text( + "The --update option will only copy new or updated files from the library " + + "to the export folder. If a file is changed in the export folder (for example, you edited the " + + "exported image), osxphotos will detect this as a difference and re-export the original image " + + "from the library thus overwriting the changes. If using --update, the exported library " + + "should be treated as a backup, not a working copy where you intend to make changes. " + + "If you do edit or process the exported files and do not want them to be overwritten with" + + "subsequent --update, use --ignore-signature which will match filename but not file signature when " + + "exporting." + ) + formatter.write("\n") + formatter.write_text( + "Note: The number of files reported for export and the number actually exported " + + "may differ due to live photos, associated raw images, and edited photos which are reported " + + "in the total photos exported." + ) + formatter.write("\n") + formatter.write_text( + "Implementation note: To determine which files need to be updated, " + + f"osxphotos stores file signature information in the '{OSXPHOTOS_EXPORT_DB}' database. " + + "The signature includes size, modification time, and filename. In order to minimize " + + "run time, --update does not do a full comparison (diff) of the files nor does it compare " + + "hashes of the files. In normal usage, this is sufficient for updating the library. " + + "You can always run export without the --update option to re-export the entire library thus " + + f"rebuilding the '{OSXPHOTOS_EXPORT_DB}' database." + ) + formatter.write("\n\n") + formatter.write_text("** Extended Attributes **") + formatter.write("\n") + formatter.write_text( + """ +Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write +additional metadata to extended attributes in the file. These options will only work +if the destination filesystem supports extended attributes (most do). +For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template' +or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'. +For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly +find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar. +Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute. +Unlike EXIF metadata, extended attributes do not modify the actual file. Most cloud storage services +do not synch extended attributes. Dropbox does sync them and any changes to a file's extended attributes +will cause Dropbox to re-sync the files. + +The following attributes may be used with '--xattr-template': + + """ + ) + formatter.write_dl( + [ + ( + attr, + f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})", + ) + for attr in EXTENDED_ATTRIBUTE_NAMES + ] + ) + formatter.write("\n") + formatter.write_text( + "For additional information on extended attributes see: https://developer.apple.com/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_keys" + ) + formatter.write("\n\n") + formatter.write_text("** Templating System **") + formatter.write("\n") + formatter.write_text( + """ +Several options, such as --directory, allow you to specify a template which +will be rendered to substitute template fields with values from the photo. +For example, '{created.month}' would be replaced with the month name of the +photo creation date. e.g. 'November'. + +Some options supporting templates may be repeated e.g., --keyword-template +'{label}' --keyword-template '{media_type}' to add both labels and media +types to the keywords. + +The general format for a template is '{TEMPLATE_FIELD,DEFAULT}'. The full template format is: +'{DELIM+TEMPLATE_FIELD(PATH_SEP)[OLD,NEW]?VALUE_IF_TRUE,DEFAULT}' + +With a few exceptions (like '{created.strftime}') everything but the TEMPLATE_FIELD +is optional. + +- 'DELIM+' Multi-value template fields such as '{keyword}' may be expanded 'in place' +with an optional delimiter using the template form '{DELIM+TEMPLATE_FIELD}'. +For example, a photo with keywords 'foo' and 'bar': + +'{keyword}' renders to 'foo' and 'bar' + +'{,+keyword}' renders to: 'foo,bar' + +'{; +keyword}' renders to: 'foo; bar' + +'{+keyword}' renders to 'foobar' + +- 'TEMPLATE_FIELD' The name of the template field, for example 'keyword' + +- '(PATH_SEP)' Some template fields such as '{folder_album}' are "path-like" in +that they join multiple elements into a single path-like string. For example, +if photo is in album Album1 in folder Folder1, '{folder_album}' results in +'Folder1/Album1'. This is so these template fields may be used as paths in +--directory. If you intend to use such a field as a string, e.g. in the +filename, you may specify a different path separator using the form: +'{TEMPLATE_FIELD(PATH_SEP)}'. For example, using the example above, +'{folder_album(-)}' would result in 'Folder1-Album1' and '{folder_album()}' +would result in 'Folder1Album1'. + +- '[OLD,NEW]' Use the [OLD,NEW] option to replace text "OLD" in the template value +with text "NEW". For example, if you have album names with '/' in the album name you +could replace '/' with "-" using the template '{album[/,-]}'. This would replace +any occurence of "/" in the album name with "-"; album "Vacation/2019" would thus +become "Vacation-2019". You may specify more than one pair of OLD,NEW values by +listing them delimited by '|'. For example: '{album[/,-|:,-]}' to replace both +'/' and ':' by '-'. You can also use the [OLD,NEW] syntax to delete a character by +omitting the NEW value as in '{album[/,]}'. + +- '?' Some template fields such as 'hdr' are boolean and resolve to True or False. +These take the form: '{TEMPLATE_FIELD?VALUE_IF_TRUE,VALUE_IF_FALSE}', e.g. +{hdr?is_hdr,not_hdr} which would result in 'is_hdr' if photo is an HDR image +and 'not_hdr' otherwise. + +- ',DEFAULT' The ',' and DEFAULT value are optional. If TEMPLATE_FIELD results +in a null (empty) value, the template will result in default value of '_'. +You may specify an alternate default value by appending ',DEFAULT' after +template_field. Example: '{title,no_title}' would result in 'no_title' if the photo +had no title. Example: '{created.year}/{place.address,NO_ADDRESS}' but there was +no address associated with the photo, the resulting output would be: +'2020/NO_ADDRESS/photoname.jpg'. If specified, the default value may not +contain a brace symbol ('{' or '}'). + +Again, if you do not specify a default value and the template substitution has no +value, '_' (underscore) will be used as the default value. For example, in the +above example, this would result in '2020/_/photoname.jpg' if address was +null. + +You may specify a null default (e.g. "" or empty string) by omitting the value +after the comma, e.g. {title,} which would render to "" if title had no value thus +effectively deleting the template from the resulting string. + +You may include other text in the template string outside the {} +and use more than one template field in a single string, +e.g. '{created.year} - {created.month}' (e.g. '2020 - November'). + +Some templates may resolve to more than one value. For example, a photo can +have multiple keywords so '{keyword}' can result in multiple values. If used +in a filename or directory, these templates may result in more than one copy +of the photo being exported. For example, if photo has keywords "foo" and +"bar", --directory '{keyword}' will result in copies of the photo being +exported to 'foo/image_name.jpeg' and 'bar/image_name.jpeg'. + +Some template fields such as '{media_type}' use the 'DEFAULT' value to allow +customization of the output. For example, '{media_type}' resolves to the +special media type of the photo such as 'panorama' or 'selfie'. You may use +the 'DEFAULT' value to override these in form: +'{media_type,video=vidéo;time_lapse=vidéo_accélérée}'. In this example, if +photo is a time_lapse photo, 'media_type' would resolve to 'vidéo_accélérée' +instead of 'time_lapse' and video would resolve to 'vidéo' if photo is an +ordinary video. + +With the --directory and --filename options you may specify a template for the +export directory or filename, respectively. The directory will be appended to +the export path specified in the export DEST argument to export. For example, +if template is '{created.year}/{created.month}', and export destination DEST +is '/Users/maria/Pictures/export', the actual export directory for a photo +would be '/Users/maria/Pictures/export/2020/March' if the photo was created in +March 2020. + +The templating system may also be used with the --keyword-template option to +set keywords on export (with --exiftool or --sidecar), for example, to set a +new keyword in format 'folder/subfolder/album' to preserve the folder/album +structure, you can use --keyword-template "{folder_album}" + +In the template, valid template substitutions will be replaced by the +corresponding value from the table below. Invalid substitutions will result +in an error. + +If you want the actual text of the template substition to appear in the +rendered name, use double braces, e.g. '{{' or '}}', thus using +'{created.year}/{{name}}' for --directory would result in output of +2020/{name}/photoname.jpg +""" + ) + formatter.write("\n") + formatter.write_text( + "With the --directory and --filename options you may specify a template for the " + + "export directory or filename, respectively. " + + "The directory will be appended to the export path specified " + + "in the export DEST argument to export. For example, if template is " + + "'{created.year}/{created.month}', and export destination DEST is " + + "'/Users/maria/Pictures/export', " + + "the actual export directory for a photo would be '/Users/maria/Pictures/export/2020/March' " + + "if the photo was created in March 2020. " + ) + formatter.write("\n") + formatter.write_text( + "The templating system may also be used with the --keyword-template option " + + "to set keywords on export (with --exiftool or --sidecar), " + + "for example, to set a new keyword in format 'folder/subfolder/album' to " + + 'preserve the folder/album structure, you can use --keyword-template "{folder_album}"' + ) + formatter.write("\n") + formatter.write_text( + "In the template, valid template substitutions will be replaced by " + + "the corresponding value from the table below. Invalid substitutions will result in a " + + "an error and the script will abort." + ) + formatter.write("\n") + formatter.write_text( + "If you want the actual text of the template substition to appear " + + "in the rendered name, use double braces, e.g. '{{' or '}}', thus " + + "using '{created.year}/{{name}}' for --directory " + + "would result in output of 2020/{name}/photoname.jpg" + ) + formatter.write("\n") + formatter.write_text( + "You may specify an optional default value to use if the substitution does not contain a value " + + "(e.g. the value is null) " + + "by specifying the default value after a ',' in the template string: " + + "for example, if template is '{created.year}/{place.address,NO_ADDRESS}' " + + "but there was no address associated with the photo, the resulting output would be: " + + "'2020/NO_ADDRESS/photoname.jpg'. " + + "If specified, the default value may not contain a brace symbol ('{' or '}')." + ) + formatter.write("\n") + formatter.write_text( + "If you do not specify a default value and the template substitution " + + "has no value, '_' (underscore) will be used as the default value. For example, in the " + + "above example, this would result in '2020/_/photoname.jpg' if address was null." + ) + formatter.write("\n") + formatter.write_text( + 'You may specify a null default (e.g. "" or empty string) by omitting the value after ' + + 'the comma, e.g. {title,} which would render to "" if title had no value.' + ) + formatter.write("\n") + templ_tuples = [("Substitution", "Description")] + templ_tuples.extend((k, v) for k, v in TEMPLATE_SUBSTITUTIONS.items()) + formatter.write_dl(templ_tuples) + + formatter.write("\n") + formatter.write_text( + "The following substitutions may result in multiple values. Thus " + + "if specified for --directory these could result in multiple copies of a photo being " + + "being exported, one to each directory. For example: " + + "--directory '{created.year}/{album}' could result in the same photo being exported " + + "to each of the following directories if the photos were created in 2019 " + + "and were in albums 'Vacation' and 'Family': " + + "2019/Vacation, 2019/Family" + ) + formatter.write("\n") + templ_tuples = [("Substitution", "Description")] + templ_tuples.extend( + (k, v) for k, v in TEMPLATE_SUBSTITUTIONS_MULTI_VALUED.items() + ) + + formatter.write_dl(templ_tuples) + help_text += formatter.getvalue() + return help_text diff --git a/tests/search_info_test_data_10_15_7.json b/tests/search_info_test_data_10_15_7.json index d0fb85fc..8c29d22d 100644 --- a/tests/search_info_test_data_10_15_7.json +++ b/tests/search_info_test_data_10_15_7.json @@ -1 +1 @@ -{"UUID_SEARCH_INFO": {"C8EAF50A-D891-4E0C-8086-C417E1284153": {"labels": ["Food", "Butter"], "place_names": ["Durham Bulls Athletic Park"], "streets": ["Blackwell St"], "neighborhoods": ["American Tobacco District", "Downtown Durham"], "city": "Durham", "locality_names": ["Durham"], "state": "North Carolina", "state_abbreviation": "NC", "country": "United States", "bodies_of_water": [], "month": "October", "year": "2018", "holidays": [], "activities": ["Entertainment", "Travel", "Dining", "Dinner", "Trip"], "season": "Fall", "venues": ["Luna Rotisserie and Empanadas", "The Pinhook", "Copa", "Pie Pusher's"], "venue_types": [], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["Sunset Sunrise", "Sky", "Desert", "Outdoor", "Land"], "place_names": ["Royal Palms State Beach"], "streets": [], "neighborhoods": ["San Pedro"], "city": "Los Angeles", "locality_names": [], "state": "California", "state_abbreviation": "", "country": "United States", "bodies_of_water": ["Catalina Channel"], "month": "November", "year": "2017", "holidays": [], "activities": ["Beach Activity", "Activity"], "season": "Fall", "venues": [], "venue_types": [], "media_types": ["Live Photos"]}, "2C151013-5BBA-4D00-B70F-1C9420418B86": {"labels": ["People", "Land", "Forest", "Vegetation", "Outdoor", "Furniture", "Bench", "Water", "Water Body"], "place_names": [], "streets": [], "neighborhoods": [], "city": "", "locality_names": [], "state": "", "state_abbreviation": "", "country": "", "bodies_of_water": [], "month": "December", "year": "2014", "holidays": ["Christmas Day"], "activities": ["Celebration", "Holiday"], "season": "Winter", "venues": [], "venue_types": [], "media_types": []}}, "UUID_SEARCH_INFO_NORMALIZED": {"C8EAF50A-D891-4E0C-8086-C417E1284153": {"labels": ["food", "butter"], "place_names": ["durham bulls athletic park"], "streets": ["blackwell st"], "neighborhoods": ["american tobacco district", "downtown durham"], "city": "durham", "locality_names": ["durham"], "state": "north carolina", "state_abbreviation": "nc", "country": "united states", "bodies_of_water": [], "month": "october", "year": "2018", "holidays": [], "activities": ["entertainment", "travel", "dining", "dinner", "trip"], "season": "fall", "venues": ["luna rotisserie and empanadas", "the pinhook", "copa", "pie pusher's"], "venue_types": [], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["sunset sunrise", "sky", "desert", "outdoor", "land"], "place_names": ["royal palms state beach"], "streets": [], "neighborhoods": ["san pedro"], "city": "los angeles", "locality_names": [], "state": "california", "state_abbreviation": "", "country": "united states", "bodies_of_water": ["catalina channel"], "month": "november", "year": "2017", "holidays": [], "activities": ["beach activity", "activity"], "season": "fall", "venues": [], "venue_types": [], "media_types": ["live photos"]}, "2C151013-5BBA-4D00-B70F-1C9420418B86": {"labels": ["people", "land", "forest", "vegetation", "outdoor", "furniture", "bench", "water", "water body"], "place_names": [], "streets": [], "neighborhoods": [], "city": "", "locality_names": [], "state": "", "state_abbreviation": "", "country": "", "bodies_of_water": [], "month": "december", "year": "2014", "holidays": ["christmas day"], "activities": ["celebration", "holiday"], "season": "winter", "venues": [], "venue_types": [], "media_types": []}}, "UUID_SEARCH_INFO_ALL": {"C8EAF50A-D891-4E0C-8086-C417E1284153": ["Food", "Butter", "Durham Bulls Athletic Park", "Blackwell St", "American Tobacco District", "Downtown Durham", "Durham", "Entertainment", "Travel", "Dining", "Dinner", "Trip", "Luna Rotisserie and Empanadas", "The Pinhook", "Copa", "Pie Pusher's", "Durham", "North Carolina", "NC", "United States", "October", "2018", "Fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["Sunset Sunrise", "Sky", "Desert", "Outdoor", "Land", "Royal Palms State Beach", "San Pedro", "Catalina Channel", "Beach Activity", "Activity", "Live Photos", "Los Angeles", "California", "United States", "November", "2017", "Fall"], "2C151013-5BBA-4D00-B70F-1C9420418B86": ["People", "Land", "Forest", "Vegetation", "Outdoor", "Furniture", "Bench", "Water", "Water Body", "Christmas Day", "Celebration", "Holiday", "December", "2014", "Winter"]}, "UUID_SEARCH_INFO_ALL_NORMALIZED": {"C8EAF50A-D891-4E0C-8086-C417E1284153": ["food", "butter", "durham bulls athletic park", "blackwell st", "american tobacco district", "downtown durham", "durham", "entertainment", "travel", "dining", "dinner", "trip", "luna rotisserie and empanadas", "the pinhook", "copa", "pie pusher's", "durham", "north carolina", "nc", "united states", "october", "2018", "fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["sunset sunrise", "sky", "desert", "outdoor", "land", "royal palms state beach", "san pedro", "catalina channel", "beach activity", "activity", "live photos", "los angeles", "california", "united states", "november", "2017", "fall"], "2C151013-5BBA-4D00-B70F-1C9420418B86": ["people", "land", "forest", "vegetation", "outdoor", "furniture", "bench", "water", "water body", "christmas day", "celebration", "holiday", "december", "2014", "winter"]}} +{"UUID_SEARCH_INFO": {"C8EAF50A-D891-4E0C-8086-C417E1284153": {"labels": ["Food", "Butter"], "place_names": ["Durham Bulls Athletic Park"], "streets": ["Blackwell St"], "neighborhoods": ["American Tobacco District", "Downtown Durham"], "city": "Durham", "locality_names": ["Durham"], "state": "North Carolina", "state_abbreviation": "NC", "country": "United States", "bodies_of_water": [], "month": "October", "year": "2018", "holidays": [], "activities": ["Entertainment", "Travel", "Dining", "Dinner", "Trip"], "season": "Fall", "venues": ["Luna Rotisserie and Empanadas", "The Pinhook", "Copa", "Pie Pusher's"], "venue_types": [], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["Desert", "Land", "Sky", "Sunset Sunrise", "Outdoor"], "place_names": ["Royal Palms State Beach"], "streets": [], "neighborhoods": ["San Pedro"], "city": "Los Angeles", "locality_names": [], "state": "California", "state_abbreviation": "", "country": "United States", "bodies_of_water": ["Catalina Channel"], "month": "November", "year": "2017", "holidays": [], "activities": ["Beach Activity", "Activity"], "season": "Fall", "venues": [], "venue_types": [], "media_types": ["Live Photos"]}, "2C151013-5BBA-4D00-B70F-1C9420418B86": {"labels": ["People", "Bench", "Land", "Vegetation", "Forest", "Outdoor", "Water", "Water Body", "Furniture"], "place_names": [], "streets": [], "neighborhoods": [], "city": "", "locality_names": [], "state": "", "state_abbreviation": "", "country": "", "bodies_of_water": [], "month": "December", "year": "2014", "holidays": ["Christmas Day"], "activities": ["Celebration", "Holiday"], "season": "Winter", "venues": [], "venue_types": [], "media_types": []}}, "UUID_SEARCH_INFO_NORMALIZED": {"C8EAF50A-D891-4E0C-8086-C417E1284153": {"labels": ["food", "butter"], "place_names": ["durham bulls athletic park"], "streets": ["blackwell st"], "neighborhoods": ["american tobacco district", "downtown durham"], "city": "durham", "locality_names": ["durham"], "state": "north carolina", "state_abbreviation": "nc", "country": "united states", "bodies_of_water": [], "month": "october", "year": "2018", "holidays": [], "activities": ["entertainment", "travel", "dining", "dinner", "trip"], "season": "fall", "venues": ["luna rotisserie and empanadas", "the pinhook", "copa", "pie pusher's"], "venue_types": [], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["desert", "land", "sky", "sunset sunrise", "outdoor"], "place_names": ["royal palms state beach"], "streets": [], "neighborhoods": ["san pedro"], "city": "los angeles", "locality_names": [], "state": "california", "state_abbreviation": "", "country": "united states", "bodies_of_water": ["catalina channel"], "month": "november", "year": "2017", "holidays": [], "activities": ["beach activity", "activity"], "season": "fall", "venues": [], "venue_types": [], "media_types": ["live photos"]}, "2C151013-5BBA-4D00-B70F-1C9420418B86": {"labels": ["people", "bench", "land", "vegetation", "forest", "outdoor", "water", "water body", "furniture"], "place_names": [], "streets": [], "neighborhoods": [], "city": "", "locality_names": [], "state": "", "state_abbreviation": "", "country": "", "bodies_of_water": [], "month": "december", "year": "2014", "holidays": ["christmas day"], "activities": ["celebration", "holiday"], "season": "winter", "venues": [], "venue_types": [], "media_types": []}}, "UUID_SEARCH_INFO_ALL": {"C8EAF50A-D891-4E0C-8086-C417E1284153": ["Food", "Butter", "Durham Bulls Athletic Park", "Blackwell St", "American Tobacco District", "Downtown Durham", "Durham", "Entertainment", "Travel", "Dining", "Dinner", "Trip", "Luna Rotisserie and Empanadas", "The Pinhook", "Copa", "Pie Pusher's", "Durham", "North Carolina", "NC", "United States", "October", "2018", "Fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["Desert", "Land", "Sky", "Sunset Sunrise", "Outdoor", "Royal Palms State Beach", "San Pedro", "Catalina Channel", "Beach Activity", "Activity", "Live Photos", "Los Angeles", "California", "United States", "November", "2017", "Fall"], "2C151013-5BBA-4D00-B70F-1C9420418B86": ["People", "Bench", "Land", "Vegetation", "Forest", "Outdoor", "Water", "Water Body", "Furniture", "Christmas Day", "Celebration", "Holiday", "December", "2014", "Winter"]}, "UUID_SEARCH_INFO_ALL_NORMALIZED": {"C8EAF50A-D891-4E0C-8086-C417E1284153": ["food", "butter", "durham bulls athletic park", "blackwell st", "american tobacco district", "downtown durham", "durham", "entertainment", "travel", "dining", "dinner", "trip", "luna rotisserie and empanadas", "the pinhook", "copa", "pie pusher's", "durham", "north carolina", "nc", "united states", "october", "2018", "fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["desert", "land", "sky", "sunset sunrise", "outdoor", "royal palms state beach", "san pedro", "catalina channel", "beach activity", "activity", "live photos", "los angeles", "california", "united states", "november", "2017", "fall"], "2C151013-5BBA-4D00-B70F-1C9420418B86": ["people", "bench", "land", "vegetation", "forest", "outdoor", "water", "water body", "furniture", "christmas day", "celebration", "holiday", "december", "2014", "winter"]}} diff --git a/tests/sidecars/15uNd7%8RguTEgNPKHfTWw.xmp b/tests/sidecars/15uNd7%8RguTEgNPKHfTWw.xmp index 24a62f0d..68f7ff87 100644 --- a/tests/sidecars/15uNd7%8RguTEgNPKHfTWw.xmp +++ b/tests/sidecars/15uNd7%8RguTEgNPKHfTWw.xmp @@ -1,5 +1,5 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +