diff --git a/README.md b/README.md
index 6e27dc3b..e9db3179 100644
--- a/README.md
+++ b/README.md
@@ -1050,6 +1050,36 @@ Options:
run with --cleanup first if you're not
certain.
+ --add-exported-to-album ALBUM Add all exported photos to album ALBUM in
+ Photos. Album ALBUM will be created if it
+ doesn't exist. All exported photos will be
+ added to this album. This only works if the
+ Photos library being exported is the last-
+ opened (default) library in Photos. This
+ feature is currently experimental. I don't
+ know how well it will work on large export
+ sets.
+
+ --add-skipped-to-album ALBUM Add all skipped photos to album ALBUM in
+ Photos. Album ALBUM will be created if it
+ doesn't exist. All skipped photos will be
+ added to this album. This only works if the
+ Photos library being exported is the last-
+ opened (default) library in Photos. This
+ feature is currently experimental. I don't
+ know how well it will work on large export
+ sets.
+
+ --add-missing-to-album ALBUM Add all missing photos to album ALBUM in
+ Photos. Album ALBUM will be created if it
+ doesn't exist. All missing photos will be
+ added to this album. This only works if the
+ Photos library being exported is the last-
+ opened (default) library in Photos. This
+ feature is currently experimental. I don't
+ know how well it will work on large export
+ sets.
+
--exportdb EXPORTDB_FILE Specify alternate name for database file which
stores state information for export and
--update. If --exportdb is not specified,
diff --git a/docs/.buildinfo b/docs/.buildinfo
index fb905c3a..ff2165c0 100644
--- a/docs/.buildinfo
+++ b/docs/.buildinfo
@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
-config: 09f774bbf0a11a7f854d5b240879b3b4
+config: 5342827b9d06cfc608d1a286ed0f5c3f
tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/docs/_modules/index.html b/docs/_modules/index.html
index 5147faf8..3e2a3976 100644
--- a/docs/_modules/index.html
+++ b/docs/_modules/index.html
@@ -5,7 +5,7 @@
- Overview: module code — osxphotos 0.42.13 documentation
+ Overview: module code — osxphotos 0.42.14 documentation
diff --git a/docs/_modules/osxphotos/photosdb/photosdb.html b/docs/_modules/osxphotos/photosdb/photosdb.html
index 259a891c..c947cfb4 100644
--- a/docs/_modules/osxphotos/photosdb/photosdb.html
+++ b/docs/_modules/osxphotos/photosdb/photosdb.html
@@ -5,7 +5,7 @@
- osxphotos.photosdb.photosdb — osxphotos 0.42.11 documentation
+ osxphotos.photosdb.photosdb — osxphotos 0.42.14 documentation
diff --git a/docs/_sources/tutorial.md.txt b/docs/_sources/tutorial.md.txt
index e4ca1b69..fe2421ed 100644
--- a/docs/_sources/tutorial.md.txt
+++ b/docs/_sources/tutorial.md.txt
@@ -55,7 +55,7 @@ By default, osxphotos will use the original filename of the photo when exporting
`osxphotos export /path/to/export --filename "{title}"`
-The above command will export photos using the title. Note that you don't need to specify the extension as part of the `--filename` template as osxphotos will automatically add the correct fie extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:
+The above command will export photos using the title. Note that you don't need to specify the extension as part of the `--filename` template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:
```txt
osxphotos export /path/to/export --filename "{title,{original_name}}"
@@ -315,6 +315,40 @@ Then the next to you run osxphotos, you can simply do this:
The configuration file is a plain text file in [TOML](https://toml.io/en/) format so the `.toml` extension is standard but you can name the file anything you like.
+### An example from an actual osxphotos user
+
+Here's a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
+
+ I usually import my iPhone’s photo roll on a more or less regular basis, and it
+ includes photos and videos. As a result, the size ot my Photos library may rise
+ very quickly. Nevertheless, I will tag and geolocate everything as Photos has a
+ quite good keyword management system.
+
+ After a while, I want to take most of the videos out of the library and move them
+ to a separate "videos" folder on a different folder / volume. As I might want to
+ use them in Final Cut Pro, and since Final Cut is able to import Finder tags into
+ its internal library tagging system, I will use osxphotos to do just this.
+
+ Picking the videos can be left to Photos, using a smart folder for instance. Then
+ just add a keyword to all videos to be processed. Here I chose "Quik" as I wanted
+ to spot all videos created on my iPhone using the Quik application (now part of
+ GoPro).
+
+ I want to retrieve my keywords only and make sure they populate the Finder tags, as
+ well as export all the persons identified in the videos by Photos. I also want to
+ merge any keywords or persons already in the video metadata with the exported
+ metadata.
+
+ Keeping Photo’s edited titles and descriptions and putting both in the Finder
+ comments field in a readable manner is also enabled.
+
+ And I want to keep the file’s creation date (using `--touch-file`).
+
+ Finally, use `--strip` to remove any leading or trailing whitespace from processed
+ template fields.
+
+`osxphotos export ~/Desktop/folder for exported videos/ --keyword Quik --only-movies --db /path to my.photoslibrary --touch-file --finder-tag-keywords --person-keyword --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}" --exiftool-merge-keywords --exiftool-merge-persons --exiftool --strip`
+
### Conclusion
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the `--directory` option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
\ No newline at end of file
diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js
index 7105d46b..38fecd0b 100644
--- a/docs/_static/documentation_options.js
+++ b/docs/_static/documentation_options.js
@@ -1,6 +1,6 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
- VERSION: '0.42.13',
+ VERSION: '0.42.14',
LANGUAGE: 'None',
COLLAPSE_INDEX: false,
BUILDER: 'html',
diff --git a/docs/cli.html b/docs/cli.html
index 646e3c21..5a9e588d 100644
--- a/docs/cli.html
+++ b/docs/cli.html
@@ -5,7 +5,7 @@
- osxphotos command line interface (CLI) — osxphotos 0.42.13 documentation
+ osxphotos command line interface (CLI) — osxphotos 0.42.14 documentation
@@ -813,6 +813,24 @@ to modify this behavior.
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. WARNING: –cleanup will delete any files in the export directory that were not exported by osxphotos, for example, your own scripts or other files. Be sure this is what you intend before using –cleanup. Use –dry-run with –cleanup first if you’re not certain.
Add all exported photos to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All exported photos will be added to this album. This only works if the Photos library being exported is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large export sets.
Add all skipped photos to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All skipped photos will be added to this album. This only works if the Photos library being exported is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large export sets.
Add all missing photos to album ALBUM in Photos. Album ALBUM will be created if it doesn’t exist. All missing photos will be added to this album. This only works if the Photos library being exported is the last-opened (default) library in Photos. This feature is currently experimental. I don’t know how well it will work on large export sets.
By default, osxphotos will use the original filename of the photo when exporting. That is, the filename the photo had when it was taken or imported into Photos. This is often something like IMG_1234.JPG or DSC05678.dng. osxphotos allows you to specify a custom filename template using the --filename option in the same way as --directory allows you to specify a custom directory name. For example, Photos allows you specify a title or caption for a photo and you can use this in place of the original filename:
osxphotosexport/path/to/export--filename"{title}"
-
The above command will export photos using the title. Note that you don’t need to specify the extension as part of the --filename template as osxphotos will automatically add the correct fie extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:
+
The above command will export photos using the title. Note that you don’t need to specify the extension as part of the --filename template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead:
Here’s a comprehensive use case from an actual osxphotos user that integrates many of the concepts discussed in this tutorial (thank-you Philippe for contributing this!):
+
I usually import my iPhone’s photo roll on a more or less regular basis, and it
+includes photos and videos. As a result, the size ot my Photos library may rise
+very quickly. Nevertheless, I will tag and geolocate everything as Photos has a
+quite good keyword management system.
+
+After a while, I want to take most of the videos out of the library and move them
+to a separate "videos" folder on a different folder / volume. As I might want to
+use them in Final Cut Pro, and since Final Cut is able to import Finder tags into
+its internal library tagging system, I will use osxphotos to do just this.
+
+Picking the videos can be left to Photos, using a smart folder for instance. Then
+just add a keyword to all videos to be processed. Here I chose "Quik" as I wanted
+to spot all videos created on my iPhone using the Quik application (now part of
+GoPro).
+
+I want to retrieve my keywords only and make sure they populate the Finder tags, as
+well as export all the persons identified in the videos by Photos. I also want to
+merge any keywords or persons already in the video metadata with the exported
+metadata.
+
+Keeping Photo’s edited titles and descriptions and putting both in the Finder
+comments field in a readable manner is also enabled.
+
+And I want to keep the file’s creation date (using `--touch-file`).
+
+Finally, use `--strip` to remove any leading or trailing whitespace from processed
+template fields.
+
osxphotos is very flexible. If you merely want to backup your Photos library, then spending a few minutes to understand the --directory option is likely all you need and you can be up and running in minutes. However, if you have a more complex workflow, osxphotos likely provides options to implement your workflow. This tutorial does not attempt to cover every option offered by osxphotos but hopefully it provides a good understanding of what kinds of things are possible and where to explore if you want to learn more.
diff --git a/osxphotos/_version.py b/osxphotos/_version.py
index ab6c25e9..8ad822ca 100644
--- a/osxphotos/_version.py
+++ b/osxphotos/_version.py
@@ -1,3 +1,3 @@
""" version info """
-__version__ = "0.42.13"
+__version__ = "0.42.14"
diff --git a/osxphotos/cli.py b/osxphotos/cli.py
index 3e9b3488..641abe8a 100644
--- a/osxphotos/cli.py
+++ b/osxphotos/cli.py
@@ -14,6 +14,7 @@ import unicodedata
import bitmath
import click
import osxmetadata
+import photoscript
import yaml
import osxphotos
@@ -53,6 +54,7 @@ from .photoinfo import ExportResults
from .photokit import check_photokit_authorization, request_photokit_authorization
from .queryoptions import QueryOptions
from .utils import get_preferred_uti_extension
+from .photosalbum import PhotosAlbum
# global variable to control verbose output
# set via --verbose/-V
@@ -878,6 +880,30 @@ def cli(ctx, db, json_, debug):
"for example, your own scripts or other files. Be sure this is what you intend before using "
"--cleanup. Use --dry-run with --cleanup first if you're not certain.",
)
+@click.option(
+ "--add-exported-to-album",
+ metavar="ALBUM",
+ help="Add all exported photos to album ALBUM in Photos. Album ALBUM will be created "
+ "if it doesn't exist. All exported photos will be added to this album. "
+ "This only works if the Photos library being exported is the last-opened (default) library in Photos. "
+ "This feature is currently experimental. I don't know how well it will work on large export sets.",
+)
+@click.option(
+ "--add-skipped-to-album",
+ metavar="ALBUM",
+ help="Add all skipped photos to album ALBUM in Photos. Album ALBUM will be created "
+ "if it doesn't exist. All skipped photos will be added to this album. "
+ "This only works if the Photos library being exported is the last-opened (default) library in Photos. "
+ "This feature is currently experimental. I don't know how well it will work on large export sets.",
+)
+@click.option(
+ "--add-missing-to-album",
+ metavar="ALBUM",
+ help="Add all missing photos to album ALBUM in Photos. Album ALBUM will be created "
+ "if it doesn't exist. All missing photos will be added to this album. "
+ "This only works if the Photos library being exported is the last-opened (default) library in Photos. "
+ "This feature is currently experimental. I don't know how well it will work on large export sets.",
+)
@click.option(
"--exportdb",
metavar="EXPORTDB_FILE",
@@ -1027,6 +1053,9 @@ def export(
use_photokit,
report,
cleanup,
+ add_exported_to_album,
+ add_skipped_to_album,
+ add_missing_to_album,
exportdb,
load_config,
save_config,
@@ -1180,6 +1209,9 @@ def export(
use_photokit = cfg.use_photokit
report = cfg.report
cleanup = cfg.cleanup
+ add_exported_to_album = cfg.add_exported_to_album
+ add_skipped_to_album = cfg.add_skipped_to_album
+ add_missing_to_album = cfg.add_missing_to_album
exportdb = cfg.exportdb
beta = cfg.beta
only_new = cfg.only_new
@@ -1524,6 +1556,24 @@ def export(
original_name = not current_name
results = ExportResults()
+
+ # set up for --add-export-to-album if needed
+ album_export = (
+ PhotosAlbum(add_exported_to_album, verbose=verbose_)
+ if add_exported_to_album
+ else None
+ )
+ album_skipped = (
+ PhotosAlbum(add_skipped_to_album, verbose=verbose_)
+ if add_skipped_to_album
+ else None
+ )
+ album_missing = (
+ PhotosAlbum(add_missing_to_album, verbose=verbose_)
+ if add_missing_to_album
+ else None
+ )
+
# 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:
@@ -1571,6 +1621,46 @@ def export(
replace_keywords=replace_keywords,
retry=retry,
)
+
+ if album_export and export_results.exported:
+ try:
+ album_export.add(p)
+ export_results.exported_album = [
+ (filename, album_export.name)
+ for filename in export_results.exported
+ ]
+ except Exception as e:
+ click.echo(
+ f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_export.name}: {e}",
+ err=True,
+ )
+
+ if album_skipped and export_results.skipped:
+ try:
+ album_skipped.add(p)
+ export_results.skipped_album = [
+ (filename, album_skipped.name)
+ for filename in export_results.skipped
+ ]
+ except Exception as e:
+ click.echo(
+ f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_skipped.name}: {e}",
+ err=True,
+ )
+
+ if album_missing and export_results.missing:
+ try:
+ album_missing.add(p)
+ export_results.missing_album = [
+ (filename, album_missing.name)
+ for filename in export_results.missing
+ ]
+ except Exception as e:
+ click.echo(
+ f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_missing.name}: {e}",
+ err=True,
+ )
+
results += export_results
# all photo files (not including sidecars) that are part of this export set
@@ -2784,7 +2874,6 @@ def write_export_report(report_file, results):
"""
# Collect results for reporting
- # TODO: pull this in a separate write_report function
all_results = {
result: {
"filename": result,
@@ -2806,6 +2895,7 @@ def write_export_report(report_file, results):
"extended_attributes_skipped": 0,
"cleanup_deleted_file": 0,
"cleanup_deleted_directory": 0,
+ "exported_album": "",
}
for result in results.all_files()
+ results.deleted_files
@@ -2881,6 +2971,9 @@ def write_export_report(report_file, results):
for result in results.deleted_directories:
all_results[result]["cleanup_deleted_directory"] = 1
+ for result, album in results.exported_album:
+ all_results[result]["exported_album"] = album
+
report_columns = [
"filename",
"exported",
@@ -2901,6 +2994,7 @@ def write_export_report(report_file, results):
"extended_attributes_skipped",
"cleanup_deleted_file",
"cleanup_deleted_directory",
+ "exported_album",
]
try:
diff --git a/osxphotos/photoinfo/_photoinfo_export.py b/osxphotos/photoinfo/_photoinfo_export.py
index 1c75cf6b..8843c54c 100644
--- a/osxphotos/photoinfo/_photoinfo_export.py
+++ b/osxphotos/photoinfo/_photoinfo_export.py
@@ -89,6 +89,9 @@ class ExportResults:
xattr_skipped=None,
deleted_files=None,
deleted_directories=None,
+ exported_album=None,
+ skipped_album=None,
+ missing_album=None,
):
self.exported = exported or []
self.new = new or []
@@ -111,6 +114,9 @@ class ExportResults:
self.xattr_skipped = xattr_skipped or []
self.deleted_files = deleted_files or []
self.deleted_directories = deleted_directories or []
+ self.exported_album = exported_album or []
+ self.skipped_album = skipped_album or []
+ self.missing_album = missing_album or []
def all_files(self):
""" return all filenames contained in results """
@@ -157,6 +163,10 @@ class ExportResults:
self.exiftool_error += other.exiftool_error
self.deleted_files += other.deleted_files
self.deleted_directories += other.deleted_directories
+ self.exported_album += other.exported_album
+ self.skipped_album += other.skipped_album
+ self.missing_album += other.missing_album
+
return self
def __str__(self):
@@ -181,6 +191,9 @@ class ExportResults:
+ f",exiftool_error={self.exiftool_error}"
+ f",deleted_files={self.deleted_files}"
+ f",deleted_directories={self.deleted_directories}"
+ + f",exported_album={self.exported_album}"
+ + f",skipped_album={self.skipped_album}"
+ + f",missing_album={self.missing_album}"
+ ")"
)
@@ -621,7 +634,11 @@ def export2(
)
edited_name = pathlib.Path(self.path_edited).name
edited_suffix = pathlib.Path(edited_name).suffix
- fname = pathlib.Path(self.original_filename).stem + edited_identifier + edited_suffix
+ fname = (
+ pathlib.Path(self.original_filename).stem
+ + edited_identifier
+ + edited_suffix
+ )
else:
fname = self.original_filename
@@ -1654,7 +1671,7 @@ def _exiftool_dict(
exif["QuickTime:ModifyDate"] = datetime_tz_to_utc(
self.date_modified
).strftime("%Y:%m:%d %H:%M:%S")
-
+
# remove any new lines in any fields
for field, val in exif.items():
if type(val) == str:
diff --git a/osxphotos/photosalbum.py b/osxphotos/photosalbum.py
new file mode 100644
index 00000000..9eafb41a
--- /dev/null
+++ b/osxphotos/photosalbum.py
@@ -0,0 +1,74 @@
+""" PhotosAlbum class to create an album in default Photos library and add photos to it """
+
+from typing import Optional
+import photoscript
+from .photoinfo import PhotoInfo
+from .utils import noop
+
+
+class PhotosAlbum:
+ def __init__(self, name: str, verbose: Optional[callable] = None):
+ self.name = name
+ self.verbose = verbose or noop
+ self.library = photoscript.PhotosLibrary()
+
+ album = self.library.album(name)
+ if album is None:
+ self.verbose(f"Creating Photos album '{self.name}'")
+ album = self.library.create_album(name)
+ self.album = album
+
+ def add(self, photo: PhotoInfo):
+ photo_ = photoscript.Photo(photo.uuid)
+ self.album.add([photo_])
+ self.verbose(
+ f"Added {photo.original_filename} ({photo.uuid}) to album {self.name}"
+ )
+
+ def photos(self):
+ return self.album.photos()
+
+
+# def add_photo_to_album(photo, album_pairs, results):
+# # todo: class PhotoAlbum
+# # keeps a name, maintains state
+# """ add photo to album(s) as defined in album_pairs
+
+# Args:
+# photo: PhotoInfo object
+# album_pairs: list of tuples with [(album name, results_list)]
+# results: ExportResults object
+
+# Returns:
+# updated ExportResults object
+# """
+# for album, result_list in album_pairs:
+# try:
+# if album_export is None:
+# # first time fetching the album, see if it exists already
+# album_export = photos_library.album(
+# add_exported_to_album
+# )
+# if album_export is None:
+# # album doesn't exist, so create it
+# verbose_(
+# f"Creating Photos album '{add_exported_to_album}'"
+# )
+# album_export = photos_library.create_album(
+# add_exported_to_album
+# )
+# exported_photo = photoscript.Photo(p.uuid)
+# album_export.add([exported_photo])
+# verbose_(
+# f"Added {p.original_filename} ({p.uuid}) to album {add_exported_to_album}"
+# )
+# exported_album = [
+# (filename, add_exported_to_album)
+# for filename in export_results.exported
+# ]
+# export_results.exported_album = exported_album
+# if
+# except Exception as e:
+# click.echo(
+# f"Error adding photo {p.original_filename} ({p.uuid}) to album {add_exported_to_album}"
+# )
diff --git a/requirements.txt b/requirements.txt
index 8079a8e7..7e3826d7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -48,7 +48,7 @@ parso==0.6.2
pathspec==0.7.0
pathvalidate==2.2.1
pexpect==4.8.0
-photoscript==0.1.0
+photoscript==0.1.2
pickleshare==0.7.5
Pillow==8.1.1
pkginfo==1.5.0.1
diff --git a/setup.py b/setup.py
index 99d29b09..d3887324 100755
--- a/setup.py
+++ b/setup.py
@@ -81,7 +81,7 @@ setup(
"pathvalidate==2.2.1",
"dataclasses==0.7;python_version<'3.7'",
"wurlitzer>=2.0.1",
- "photoscript>=0.1.0",
+ "photoscript>=0.1.2",
"toml>=0.10.0",
"osxmetadata>=0.99.13",
"textx==2.3.0",
diff --git a/tests/conftest.py b/tests/conftest.py
index 6df4903b..fdd6d478 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,9 +1,116 @@
""" pytest test configuration """
+import os
+import pathlib
+
import pytest
+from applescript import AppleScript
+from photoscript.utils import ditto
from osxphotos.exiftool import _ExifToolProc
+
+def get_os_version():
+ import platform
+
+ # returns tuple containing OS version
+ # e.g. 10.13.6 = (10, 13, 6)
+ version = platform.mac_ver()[0].split(".")
+ if len(version) == 2:
+ (ver, major) = version
+ minor = "0"
+ elif len(version) == 3:
+ (ver, major, minor) = version
+ else:
+ raise (
+ ValueError(
+ f"Could not parse version string: {platform.mac_ver()} {version}"
+ )
+ )
+ return (ver, major, minor)
+
+
+OS_VER = get_os_version()[1]
+if OS_VER == "15":
+ TEST_LIBRARY = "tests/Test-10.15.7.photoslibrary"
+else:
+ TEST_LIBRARY = None
+ pytest.exit("This test suite currently only runs on MacOS Catalina ")
+
+
@pytest.fixture(autouse=True)
def reset_singletons():
""" Need to clean up any ExifTool singletons between tests """
- _ExifToolProc.instance = None
\ No newline at end of file
+ _ExifToolProc.instance = None
+
+
+def pytest_addoption(parser):
+ parser.addoption(
+ "--addalbum",
+ action="store_true",
+ default=False,
+ help="run --add-exported-to-album tests",
+ )
+
+
+def pytest_configure(config):
+ config.addinivalue_line(
+ "markers", "addalbum: mark test as requiring --addalbum to run"
+ )
+
+
+def pytest_collection_modifyitems(config, items):
+ if config.getoption("--addalbum"):
+ # --addalbum given in cli: do not skip addalbum tests (these require interactive test)
+ return
+ skip_addalbum = pytest.mark.skip(reason="need --addalbum option to run")
+ for item in items:
+ if "addalbum" in item.keywords:
+ item.add_marker(skip_addalbum)
+
+
+def copy_photos_library(photos_library=TEST_LIBRARY, delay=0):
+ """ copy the test library and open Photos, returns path to copied library """
+ script = AppleScript(
+ """
+ tell application "Photos"
+ quit
+ end tell
+ """
+ )
+ script.run()
+ src = pathlib.Path(os.getcwd()) / photos_library
+ picture_folder = (
+ pathlib.Path(os.environ["PHOTOSCRIPT_PICTURES_FOLDER"])
+ if "PHOTOSCRIPT_PICTURES_FOLDER" in os.environ
+ else pathlib.Path("~/Pictures")
+ )
+ picture_folder = picture_folder.expanduser()
+ if not picture_folder.is_dir():
+ pytest.exit(f"Invalid picture folder: '{picture_folder}'")
+ dest = picture_folder / photos_library
+ ditto(src, dest)
+ script = AppleScript(
+ f"""
+ set tries to 0
+ repeat while tries < 5
+ try
+ tell application "Photos"
+ activate
+ delay 3
+ open POSIX file "{dest}"
+ delay {delay}
+ end tell
+ set tries to 5
+ on error
+ set tries to tries + 1
+ end try
+ end repeat
+ """
+ )
+ script.run()
+ return dest
+
+
+@pytest.fixture
+def addalbum_library():
+ copy_photos_library(delay=10)
diff --git a/tests/search_info_test_data_10_15_7.json b/tests/search_info_test_data_10_15_7.json
index 2803bf11..93f1ddf4 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": ["Dinner", "Travel", "Entertainment", "Dining", "Trip"], "season": "Fall", "venues": ["Copa", "Luna Rotisserie and Empanadas", "The Pinhook", "Pie Pushers"], "venue_types": [], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["Sunset Sunrise", "Sky", "Outdoor", "Land", "Desert"], "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": ["Water Body", "Forest", "Furniture", "Bench", "Water", "People", "Vegetation", "Outdoor", "Land"], "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": ["dinner", "travel", "entertainment", "dining", "trip"], "season": "fall", "venues": ["copa", "luna rotisserie and empanadas", "the pinhook", "pie pushers"], "venue_types": [], "media_types": []}, "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": {"labels": ["sunset sunrise", "sky", "outdoor", "land", "desert"], "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": ["water body", "forest", "furniture", "bench", "water", "people", "vegetation", "outdoor", "land"], "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", "Dinner", "Travel", "Entertainment", "Dining", "Trip", "Copa", "Luna Rotisserie and Empanadas", "The Pinhook", "Pie Pushers", "Durham", "North Carolina", "NC", "United States", "October", "2018", "Fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["Sunset Sunrise", "Sky", "Outdoor", "Land", "Desert", "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": ["Water Body", "Forest", "Furniture", "Bench", "Water", "People", "Vegetation", "Outdoor", "Land", "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", "dinner", "travel", "entertainment", "dining", "trip", "copa", "luna rotisserie and empanadas", "the pinhook", "pie pushers", "durham", "north carolina", "nc", "united states", "october", "2018", "fall"], "71DFB4C3-E868-4BE4-906E-D96BD8692D7E": ["sunset sunrise", "sky", "outdoor", "land", "desert", "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": ["water body", "forest", "furniture", "bench", "water", "people", "vegetation", "outdoor", "land", "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", "Pie Pushers", "The Pinhook", "Copa"], "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": ["Land", "Water Body", "Forest", "Water", "People", "Plant", "Outdoor", "Vegetation", "Bench", "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", "pie pushers", "the pinhook", "copa"], "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": ["land", "water body", "forest", "water", "people", "plant", "outdoor", "vegetation", "bench", "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", "Pie Pushers", "The Pinhook", "Copa", "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": ["Land", "Water Body", "Forest", "Water", "People", "Plant", "Outdoor", "Vegetation", "Bench", "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", "pie pushers", "the pinhook", "copa", "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": ["land", "water body", "forest", "water", "people", "plant", "outdoor", "vegetation", "bench", "furniture", "christmas day", "celebration", "holiday", "december", "2014", "winter"]}}
diff --git a/tests/test_cli_add_to_album.py b/tests/test_cli_add_to_album.py
new file mode 100644
index 00000000..a2ef5380
--- /dev/null
+++ b/tests/test_cli_add_to_album.py
@@ -0,0 +1,92 @@
+""" Test --add-exported-to-album """
+
+import pytest
+import os
+from click.testing import CliRunner
+import photoscript
+
+UUID_EXPORT = {"3DD2C897-F19E-4CA6-8C22-B027D5A71907": {"filename": "IMG_4547.jpg"}}
+UUID_MISSING = {
+ "8E1D7BC9-9321-44F9-8CFB-4083F6B9232A": {"filename": "IMG_2000.JPGssss"}
+}
+
+
+@pytest.mark.addalbum
+def test_export_add_to_album(addalbum_library):
+ from osxphotos.cli import export
+
+ runner = CliRunner()
+ cwd = os.getcwd()
+ with runner.isolated_filesystem():
+ EXPORT_ALBUM = "OSXPhotos Export"
+ SKIP_ALBUM = "OSXPhotos Skipped"
+ MISSING_ALBUM = "OSXPhotos Missing"
+
+ uuid_opt = [f"--uuid={uuid}" for uuid in UUID_EXPORT]
+ uuid_opt += [f"--uuid={uuid}" for uuid in UUID_MISSING]
+
+ result = runner.invoke(
+ export,
+ [
+ ".",
+ "-V",
+ "--add-exported-to-album",
+ EXPORT_ALBUM,
+ "--add-skipped-to-album",
+ SKIP_ALBUM,
+ *uuid_opt,
+ ],
+ )
+ assert result.exit_code == 0
+ assert f"Creating Photos album '{EXPORT_ALBUM}'" in result.output
+ assert f"Creating Photos album '{SKIP_ALBUM}'" in result.output
+
+ photoslib = photoscript.PhotosLibrary()
+ album = photoslib.album(EXPORT_ALBUM)
+ assert album is not None
+
+ assert len(album) == len(UUID_EXPORT)
+ got_uuids = [p.uuid for p in album.photos()]
+ assert sorted(got_uuids) == sorted(list(UUID_EXPORT.keys()))
+
+ skip_album = photoslib.album(SKIP_ALBUM)
+ assert skip_album is not None
+ assert len(skip_album) == 0
+
+ result = runner.invoke(
+ export,
+ [
+ ".",
+ "-V",
+ "--add-exported-to-album",
+ EXPORT_ALBUM,
+ "--add-skipped-to-album",
+ SKIP_ALBUM,
+ "--add-missing-to-album",
+ MISSING_ALBUM,
+ "--update",
+ *uuid_opt,
+ ],
+ )
+ assert result.exit_code == 0
+ assert f"Creating Photos album '{EXPORT_ALBUM}'" not in result.output
+ assert f"Creating Photos album '{SKIP_ALBUM}'" not in result.output
+ assert f"Creating Photos album '{MISSING_ALBUM}'" in result.output
+
+ photoslib = photoscript.PhotosLibrary()
+ export_album = photoslib.album(EXPORT_ALBUM)
+ assert export_album is not None
+ assert len(export_album) == len(UUID_EXPORT)
+
+ skip_album = photoslib.album(SKIP_ALBUM)
+ assert skip_album is not None
+ assert len(skip_album) == len(UUID_EXPORT)
+ got_uuids = [p.uuid for p in skip_album.photos()]
+ assert sorted(got_uuids) == sorted(list(UUID_EXPORT.keys()))
+
+ missing_album = photoslib.album(MISSING_ALBUM)
+ assert missing_album is not None
+ assert len(missing_album) == len(UUID_MISSING)
+ got_uuids = [p.uuid for p in missing_album.photos()]
+ assert sorted(got_uuids) == sorted(list(UUID_MISSING.keys()))
+
diff --git a/tests/test_exportresults.py b/tests/test_exportresults.py
index 58e113b9..3b900ddc 100644
--- a/tests/test_exportresults.py
+++ b/tests/test_exportresults.py
@@ -47,6 +47,9 @@ def test_exportresults_init():
assert results.exiftool_error == []
assert results.deleted_files == []
assert results.deleted_directories == []
+ assert results.exported_album == []
+ assert results.skipped_album == []
+ assert results.missing_album == []
def test_exportresults_iadd():
@@ -110,6 +113,6 @@ def test_str():
results = ExportResults()
assert (
str(results)
- == "ExportResults(exported=[],new=[],updated=[],skipped=[],exif_updated=[],touched=[],converted_to_jpeg=[],sidecar_json_written=[],sidecar_json_skipped=[],sidecar_exiftool_written=[],sidecar_exiftool_skipped=[],sidecar_xmp_written=[],sidecar_xmp_skipped=[],missing=[],error=[],exiftool_warning=[],exiftool_error=[],deleted_files=[],deleted_directories=[])"
+ == "ExportResults(exported=[],new=[],updated=[],skipped=[],exif_updated=[],touched=[],converted_to_jpeg=[],sidecar_json_written=[],sidecar_json_skipped=[],sidecar_exiftool_written=[],sidecar_exiftool_skipped=[],sidecar_xmp_written=[],sidecar_xmp_skipped=[],missing=[],error=[],exiftool_warning=[],exiftool_error=[],deleted_files=[],deleted_directories=[],exported_album=[],skipped_album=[],missing_album=[])"
)