Added --filename to CLI, closes #89

This commit is contained in:
Rhet Turnbull
2020-05-31 11:25:34 -07:00
parent d47fd46a21
commit 6c84827ec7
5 changed files with 370 additions and 171 deletions

View File

@@ -282,6 +282,12 @@ Options:
output directory in the form output directory in the form
'{name,DEFAULT}'. See below for additional '{name,DEFAULT}'. See below for additional
details on templating system. details on templating system.
--filename FILENAME 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.
--edited-suffix SUFFIX Optional suffix for naming edited photos. --edited-suffix SUFFIX Optional suffix for naming edited photos.
Default name for edited photos is in form Default name for edited photos is in form
'photoname_edited.ext'. For example, with ' 'photoname_edited.ext'. For example, with '
@@ -326,13 +332,16 @@ option to re-export the entire library thus rebuilding the
** Templating System ** ** Templating System **
With the --directory option you may specify a template for the export With the --directory and --filename options you may specify a template for the
directory. This directory will be appended to the export path specified in export directory or filename, respectively. The directory will be appended to
the export DEST argument to export. For example, if template is the export path specified in the export DEST argument to export. For example,
'{created.year}/{created.month}', and export desitnation DEST is if template is '{created.year}/{created.month}', and export desitnation DEST
'/Users/maria/Pictures/export', the actual export directory for a photo would is '/Users/maria/Pictures/export', the actual export directory for a photo
be '/Users/maria/Pictures/export/2020/March' if the photo was created in March would be '/Users/maria/Pictures/export/2020/March' if the photo was created in
2020. March 2020. Some template substitutions may result in more than one value, for
example '{album}' if photo is in more than one album or '{keyword}' if photo
has more than one keyword. In this case, more than one copy of the photo will
be exported, each in a separate directory or with a different filename.
The templating system may also be used with the --keyword-template option to 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 set keywords on export (with --exiftool or --sidecar), for example, to set a
@@ -358,9 +367,7 @@ contain a brace symbol ('{' or '}').
If you do not specify a default value and the template substitution has no 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 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 above example, this would result in '2020/_/photoname.jpg' if address was null.
I plan to eventually extend the templating system to the exported filename so
you can specify the filename using a template.
Substitution Description Substitution Description
{name} Current filename of the photo {name} Current filename of the photo

View File

@@ -15,23 +15,20 @@ import yaml
from pathvalidate import ( from pathvalidate import (
is_valid_filename, is_valid_filename,
is_valid_filepath, is_valid_filepath,
sanitize_filepath,
sanitize_filename, sanitize_filename,
sanitize_filepath,
) )
import osxphotos import osxphotos
from ._constants import _EXIF_TOOL_URL, _PHOTOS_4_VERSION, _UNKNOWN_PLACE from ._constants import _EXIF_TOOL_URL, _PHOTOS_4_VERSION, _UNKNOWN_PLACE
from .datetime_formatter import DateTimeFormatter from ._export_db import ExportDB, ExportDBInMemory
from ._version import __version__ from ._version import __version__
from .datetime_formatter import DateTimeFormatter
from .exiftool import get_exiftool_path from .exiftool import get_exiftool_path
from .fileutil import FileUtil, FileUtilNoOp from .fileutil import FileUtil, FileUtilNoOp
from .photoinfo import ExportResults from .photoinfo import ExportResults
from .phototemplate import ( from .phototemplate import TEMPLATE_SUBSTITUTIONS, TEMPLATE_SUBSTITUTIONS_MULTI_VALUED
TEMPLATE_SUBSTITUTIONS,
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED,
)
from ._export_db import ExportDB, ExportDBInMemory
# global variable to control verbose output # global variable to control verbose output
# set via --verbose/-V # set via --verbose/-V
@@ -134,13 +131,18 @@ class ExportCommand(click.Command):
formatter.write_text("** Templating System **") formatter.write_text("** Templating System **")
formatter.write("\n") formatter.write("\n")
formatter.write_text( formatter.write_text(
"With the --directory option you may specify a template for the " "With the --directory and --filename options you may specify a template for the "
+ "export directory. This directory will be appended to the export path specified " + "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 " + "in the export DEST argument to export. For example, if template is "
+ "'{created.year}/{created.month}', and export desitnation DEST is " + "'{created.year}/{created.month}', and export desitnation DEST is "
+ "'/Users/maria/Pictures/export', " + "'/Users/maria/Pictures/export', "
+ "the actual export directory for a photo would be '/Users/maria/Pictures/export/2020/March' " + "the actual export directory for a photo would be '/Users/maria/Pictures/export/2020/March' "
+ "if the photo was created in March 2020. " + "if the photo was created in March 2020. "
+ "Some template substitutions may result in more than one value, for example '{album}' if "
+ "photo is in more than one album or '{keyword}' if photo has more than one keyword. "
+ "In this case, more than one copy of the photo will be exported, each in a separate directory "
+ "or with a different filename."
) )
formatter.write("\n") formatter.write("\n")
formatter.write_text( formatter.write_text(
@@ -176,11 +178,7 @@ class ExportCommand(click.Command):
formatter.write_text( formatter.write_text(
"If you do not specify a default value and the template substitution " "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 " + "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" + "above example, this would result in '2020/_/photoname.jpg' if address was null."
)
formatter.write_text(
"I plan to eventually extend the templating system "
+ "to the exported filename so you can specify the filename using a template."
) )
formatter.write("\n") formatter.write("\n")
@@ -1038,13 +1036,22 @@ def query(
help="Optional template for specifying name of output directory in the form '{name,DEFAULT}'. " help="Optional template for specifying name of output directory in the form '{name,DEFAULT}'. "
"See below for additional details on templating system.", "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( @click.option(
"--edited-suffix", "--edited-suffix",
metavar="SUFFIX", metavar="SUFFIX",
default="_edited", default="_edited",
help="Optional suffix for naming edited photos. Default name for edited photos is in form " help="Optional suffix for naming edited photos. Default name for edited photos is in form "
"'photoname_edited.ext'. For example, with '--edited-suffix _bearbeiten', the edited photo " "'photoname_edited.ext'. For example, with '--edited-suffix _bearbeiten', the edited photo "
"would be named 'photoname_bearbeiten.ext'. The default suffix is '_edited'." "would be named 'photoname_bearbeiten.ext'. The default suffix is '_edited'.",
) )
@click.option( @click.option(
"--no-extended-attributes", "--no-extended-attributes",
@@ -1124,6 +1131,7 @@ def export(
not_panorama, not_panorama,
has_raw, has_raw,
directory, directory,
filename_template,
edited_suffix, edited_suffix,
place, place,
no_place, no_place,
@@ -1288,6 +1296,7 @@ def export(
photos.extend(burst_set) photos.extend(burst_set)
num_photos = len(photos) 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" photo_str = "photos" if num_photos > 1 else "photo"
click.echo(f"Exporting {num_photos} {photo_str} to {dest}...") click.echo(f"Exporting {num_photos} {photo_str} to {dest}...")
start_time = time.perf_counter() start_time = time.perf_counter()
@@ -1310,6 +1319,7 @@ def export(
download_missing=download_missing, download_missing=download_missing,
exiftool=exiftool, exiftool=exiftool,
directory=directory, directory=directory,
filename_template=filename_template,
no_extended_attributes=no_extended_attributes, no_extended_attributes=no_extended_attributes,
export_raw=export_raw, export_raw=export_raw,
album_keyword=album_keyword, album_keyword=album_keyword,
@@ -1317,7 +1327,7 @@ def export(
keyword_template=keyword_template, keyword_template=keyword_template,
export_db=export_db, export_db=export_db,
fileutil=fileutil, fileutil=fileutil,
dry_run = dry_run, dry_run=dry_run,
edited_suffix=edited_suffix, edited_suffix=edited_suffix,
) )
results_exported.extend(results.exported) results_exported.extend(results.exported)
@@ -1342,6 +1352,7 @@ def export(
download_missing=download_missing, download_missing=download_missing,
exiftool=exiftool, exiftool=exiftool,
directory=directory, directory=directory,
filename_template=filename_template,
no_extended_attributes=no_extended_attributes, no_extended_attributes=no_extended_attributes,
export_raw=export_raw, export_raw=export_raw,
album_keyword=album_keyword, album_keyword=album_keyword,
@@ -1350,7 +1361,7 @@ def export(
export_db=export_db, export_db=export_db,
fileutil=fileutil, fileutil=fileutil,
dry_run=dry_run, dry_run=dry_run,
edited_suffix=edited_suffix edited_suffix=edited_suffix,
) )
results_exported.extend(results.exported) results_exported.extend(results.exported)
results_new.extend(results.new) results_new.extend(results.new)
@@ -1762,6 +1773,7 @@ def export_photo(
download_missing=None, download_missing=None,
exiftool=None, exiftool=None,
directory=None, directory=None,
filename_template=None,
no_extended_attributes=None, no_extended_attributes=None,
export_raw=None, export_raw=None,
album_keyword=None, album_keyword=None,
@@ -1770,9 +1782,11 @@ def export_photo(
export_db=None, export_db=None,
fileutil=FileUtil, fileutil=FileUtil,
dry_run=None, dry_run=None,
edited_suffix="_edited" edited_suffix="_edited",
): ):
""" Helper function for export that does the actual export """ Helper function for export that does the actual export
Args:
photo: PhotoInfo object photo: PhotoInfo object
dest: destination path as string dest: destination path as string
verbose_: boolean; print verbose output verbose_: boolean; print verbose output
@@ -1786,6 +1800,7 @@ def export_photo(
download_missing: attempt download of missing iCloud photos download_missing: attempt download of missing iCloud photos
exiftool: use exiftool to write EXIF metadata directly to exported photo exiftool: use exiftool to write EXIF metadata directly to exported photo
directory: template used to determine output directory directory: template used to determine output directory
filename_template: template use to determine output file
no_extended_attributes: boolean; if True, exports photo without preserving extended attributes no_extended_attributes: boolean; if True, exports photo without preserving extended attributes
export_raw: boolean; if True exports RAW image associate with the photo export_raw: boolean; if True exports RAW image associate with the photo
album_keyword: boolean; if True, exports album names as keywords in metadata album_keyword: boolean; if True, exports album names as keywords in metadata
@@ -1794,15 +1809,16 @@ def export_photo(
export_db: export database instance compatible with ExportDB_ABC export_db: export database instance compatible with ExportDB_ABC
fileutil: file util class compatible with FileUtilABC fileutil: file util class compatible with FileUtilABC
dry_run: boolean; if True, doesn't actually export or update any files dry_run: boolean; if True, doesn't actually export or update any files
returns list of path(s) of exported photo or None if photo was missing
Returns:
list of path(s) of exported photo or None if photo was missing
Raises:
ValueError on invalid filename_template
""" """
global VERBOSE global VERBOSE
VERBOSE = True if verbose_ else False VERBOSE = True if verbose_ else False
# Can export to multiple paths
# Start with single path [dest] but direcotry and export_by_date will modify dest_paths
dest_paths = [dest]
if not download_missing: if not download_missing:
if photo.ismissing: if photo.ismissing:
space = " " if not verbose_ else "" space = " " if not verbose_ else ""
@@ -1821,149 +1837,125 @@ def export_photo(
) )
return ExportResults([], [], [], [], []) return ExportResults([], [], [], [], [])
filename = None filenames = get_filenames_from_template(photo, filename_template, original_name)
if original_name: for filename in filenames:
filename = photo.original_filename verbose(f"Exporting {photo.filename} as {filename}")
else:
filename = photo.filename
verbose(f"Exporting {photo.filename} as {filename}") dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run
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 and not 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
dirnames, unmatched = photo.render_template(directory)
if unmatched:
raise click.BadOptionUsage(
"directory",
f"Invalid substitution in template '{directory}': {unmatched}",
)
dest_paths = []
for dirname in dirnames:
dirname = sanitize_filepath(dirname, platform="auto")
dest_path = os.path.join(dest, dirname)
if not is_valid_filepath(dest_path, platform="auto"):
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)
sidecar = [s.lower() for s in sidecar]
sidecar_json = sidecar_xmp = False
if "json" in sidecar:
sidecar_json = True
if "xmp" in sidecar:
sidecar_xmp = True
# if download_missing and the photo is missing or path doesn't exist,
# try to download with Photos
use_photos_export = download_missing and (
photo.ismissing or not os.path.exists(photo.path)
)
# export the photo to each path in dest_paths
results_exported = []
results_new = []
results_updated = []
results_skipped = []
results_exif_updated = []
for dest_path in dest_paths:
export_results = photo.export2(
dest_path,
filename,
sidecar_json=sidecar_json,
sidecar_xmp=sidecar_xmp,
live_photo=export_live,
raw_photo=export_raw,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
use_photos_export=use_photos_export,
exiftool=exiftool,
no_xattr=no_extended_attributes,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
update=update,
export_db=export_db,
fileutil=fileutil,
dry_run = dry_run,
) )
results_exported.extend(export_results.exported) sidecar = [s.lower() for s in sidecar]
results_new.extend(export_results.new) sidecar_json = sidecar_xmp = False
results_updated.extend(export_results.updated) if "json" in sidecar:
results_skipped.extend(export_results.skipped) sidecar_json = True
results_exif_updated.extend(export_results.exif_updated) if "xmp" in sidecar:
sidecar_xmp = True
if verbose_: # if download_missing and the photo is missing or path doesn't exist,
for exported in export_results.exported: # try to download with Photos
verbose(f"Exported {exported}") use_photos_export = download_missing and (
for new in export_results.new: photo.ismissing or not os.path.exists(photo.path)
verbose(f"Exported new file {new}") )
for updated in export_results.updated:
verbose(f"Exported updated file {updated}")
for skipped in export_results.skipped:
verbose(f"Skipped up to date file {skipped}")
# if export-edited, also export the edited version # export the photo to each path in dest_paths
# verify the photo has adjustments and valid path to avoid raising an exception results_exported = []
if export_edited and photo.hasadjustments: results_new = []
# if download_missing and the photo is missing or path doesn't exist, results_updated = []
# try to download with Photos results_skipped = []
use_photos_export = download_missing and photo.path_edited is None results_exif_updated = []
if not download_missing and photo.path_edited is None: for dest_path in dest_paths:
verbose(f"Skipping missing edited photo for {filename}") export_results = photo.export2(
else: dest_path,
edited_name = pathlib.Path(filename) filename,
# check for correct edited suffix sidecar_json=sidecar_json,
if photo.path_edited is not None: sidecar_xmp=sidecar_xmp,
edited_ext = pathlib.Path(photo.path_edited).suffix live_photo=export_live,
raw_photo=export_raw,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
use_photos_export=use_photos_export,
exiftool=exiftool,
no_xattr=no_extended_attributes,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
update=update,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
)
results_exported.extend(export_results.exported)
results_new.extend(export_results.new)
results_updated.extend(export_results.updated)
results_skipped.extend(export_results.skipped)
results_exif_updated.extend(export_results.exif_updated)
if verbose_:
for exported in export_results.exported:
verbose(f"Exported {exported}")
for new in export_results.new:
verbose(f"Exported new file {new}")
for updated in export_results.updated:
verbose(f"Exported updated file {updated}")
for skipped in export_results.skipped:
verbose(f"Skipped up to date file {skipped}")
# 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:
# if download_missing and the photo is missing or path doesn't exist,
# try to download with Photos
use_photos_export = download_missing and photo.path_edited is None
if not download_missing and photo.path_edited is None:
verbose(f"Skipping missing edited photo for {filename}")
else: else:
# use filename suffix which might be wrong, edited_name = pathlib.Path(filename)
# will be corrected by use_photos_export # check for correct edited suffix
edited_ext = pathlib.Path(photo.filename).suffix if photo.path_edited is not None:
edited_name = f"{edited_name.stem}{edited_suffix}{edited_ext}" edited_ext = pathlib.Path(photo.path_edited).suffix
verbose(f"Exporting edited version of {filename} as {edited_name}") else:
export_results_edited = photo.export2( # use filename suffix which might be wrong,
dest_path, # will be corrected by use_photos_export
edited_name, edited_ext = pathlib.Path(photo.filename).suffix
sidecar_json=sidecar_json, edited_name = f"{edited_name.stem}{edited_suffix}{edited_ext}"
sidecar_xmp=sidecar_xmp, verbose(f"Exporting edited version of {filename} as {edited_name}")
export_as_hardlink=export_as_hardlink, export_results_edited = photo.export2(
overwrite=overwrite, dest_path,
edited=True, edited_name,
use_photos_export=use_photos_export, sidecar_json=sidecar_json,
exiftool=exiftool, sidecar_xmp=sidecar_xmp,
no_xattr=no_extended_attributes, export_as_hardlink=export_as_hardlink,
use_albums_as_keywords=album_keyword, overwrite=overwrite,
use_persons_as_keywords=person_keyword, edited=True,
keyword_template=keyword_template, use_photos_export=use_photos_export,
update=update, exiftool=exiftool,
export_db=export_db, no_xattr=no_extended_attributes,
fileutil=fileutil, use_albums_as_keywords=album_keyword,
dry_run = dry_run, use_persons_as_keywords=person_keyword,
) keyword_template=keyword_template,
update=update,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
)
results_exported.extend(export_results_edited.exported) results_exported.extend(export_results_edited.exported)
results_new.extend(export_results_edited.new) results_new.extend(export_results_edited.new)
results_updated.extend(export_results_edited.updated) results_updated.extend(export_results_edited.updated)
results_skipped.extend(export_results_edited.skipped) results_skipped.extend(export_results_edited.skipped)
results_exif_updated.extend(export_results_edited.exif_updated) results_exif_updated.extend(export_results_edited.exif_updated)
if verbose_: if verbose_:
for exported in export_results_edited.exported: for exported in export_results_edited.exported:
verbose(f"Exported {exported}") verbose(f"Exported {exported}")
for new in export_results_edited.new: for new in export_results_edited.new:
verbose(f"Exported new file {new}") verbose(f"Exported new file {new}")
for updated in export_results_edited.updated: for updated in export_results_edited.updated:
verbose(f"Exported updated file {updated}") verbose(f"Exported updated file {updated}")
for skipped in export_results_edited.skipped: for skipped in export_results_edited.skipped:
verbose(f"Skipped up to date file {skipped}") verbose(f"Skipped up to date file {skipped}")
return ExportResults( return ExportResults(
results_exported, results_exported,
@@ -1974,5 +1966,83 @@ def export_photo(
) )
def get_filenames_from_template(photo, filename_template, original_name):
""" 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
filenames, unmatched = photo.render_template(filename_template, path_sep="_")
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:
if original_name:
filenames = [photo.original_filename]
else:
filenames = [photo.filename]
return filenames
def get_dirnames_from_template(photo, directory, export_by_date, dest, dry_run):
""" 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 and not 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
dirnames, unmatched = photo.render_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, platform="auto")
dest_path = os.path.join(dest, dirname)
if not is_valid_filepath(dest_path, platform="auto"):
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
if __name__ == "__main__": if __name__ == "__main__":
cli() # pylint: disable=no-value-for-parameter cli() # pylint: disable=no-value-for-parameter

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.29.8" __version__ = "0.29.9"

View File

@@ -29,6 +29,7 @@ from ..placeinfo import PlaceInfo4, PlaceInfo5
from ..phototemplate import PhotoTemplate from ..phototemplate import PhotoTemplate
from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension
class PhotoInfo: class PhotoInfo:
""" """
Info about a specific photo, contains all the details about the photo Info about a specific photo, contains all the details about the photo

View File

@@ -1,4 +1,5 @@
import os import os
import pytest import pytest
from click.testing import CliRunner from click.testing import CliRunner
@@ -10,6 +11,7 @@ RAW_PHOTOS_DB = "tests/Test-RAW-10.15.1.photoslibrary"
PLACES_PHOTOS_DB = "tests/Test-Places-Catalina-10_15_1.photoslibrary" PLACES_PHOTOS_DB = "tests/Test-Places-Catalina-10_15_1.photoslibrary"
PLACES_PHOTOS_DB_13 = "tests/Test-Places-High-Sierra-10.13.6.photoslibrary" PLACES_PHOTOS_DB_13 = "tests/Test-Places-High-Sierra-10.13.6.photoslibrary"
PHOTOS_DB_15_4 = "tests/Test-10.15.4.photoslibrary" PHOTOS_DB_15_4 = "tests/Test-10.15.4.photoslibrary"
PHOTOS_DB_15_5 = "tests/Test-10.15.5.photoslibrary"
PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary" PHOTOS_DB_14_6 = "tests/Test-10.14.6.photoslibrary"
CLI_OUTPUT_NO_SUBCOMMAND = [ CLI_OUTPUT_NO_SUBCOMMAND = [
@@ -132,6 +134,38 @@ CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES3 = [
"2018/{foo}/Pumkins1.jpg", "2018/{foo}/Pumkins1.jpg",
] ]
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1 = [
"2019-wedding.jpg",
"2019-wedding_edited.jpeg",
"2019-Tulips.jpg",
"2018-St James Park.jpg",
"2018-St James Park_edited.jpeg",
"2018-Pumpkins3.jpg",
"2018-Pumkins2.jpg",
"2018-Pumkins1.jpg",
]
CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2 = [
"Folder1_SubFolder2_AlbumInFolder-IMG_4547.jpg",
"Folder1_SubFolder2_AlbumInFolder-wedding.jpg",
"Folder1_SubFolder2_AlbumInFolder-wedding_edited.jpeg",
"Folder2_Raw-DSC03584.dng",
"Folder2_Raw-IMG_1994.cr2",
"Folder2_Raw-IMG_1994.JPG",
"Folder2_Raw-IMG_1997.cr2",
"Folder2_Raw-IMG_1997.JPG",
"None-St James Park.jpg",
"None-St James Park_edited.jpeg",
"None-Tulips.jpg",
"None-Tulips_edited.jpeg",
"Pumpkin Farm-Pumkins1.jpg",
"Pumpkin Farm-Pumkins2.jpg",
"Pumpkin Farm-Pumpkins3.jpg",
"Test Album-Pumkins1.jpg",
"Test Album-Pumkins2.jpg",
]
CLI_EXPORT_UUID = "D79B8D77-BFFC-460B-9312-034F2877D35B" CLI_EXPORT_UUID = "D79B8D77-BFFC-460B-9312-034F2877D35B"
CLI_EXPORT_UUID_FILENAME = "Pumkins2.jpg" CLI_EXPORT_UUID_FILENAME = "Pumkins2.jpg"
@@ -706,7 +740,7 @@ def test_export_directory_template_2():
def test_export_directory_template_3(): def test_export_directory_template_3():
# test export using directory template with unmatched substituion value # test export using directory template with unmatched substitution value
import glob import glob
import os import os
import os.path import os.path
@@ -728,7 +762,7 @@ def test_export_directory_template_3():
], ],
) )
assert result.exit_code == 2 assert result.exit_code == 2
assert "Error: Invalid substitution in template" in result.output assert "Error: Invalid template" in result.output
def test_export_directory_template_album_1(): def test_export_directory_template_album_1():
@@ -825,6 +859,93 @@ def test_export_directory_template_locale():
assert os.path.isfile(os.path.join(workdir, filepath)) assert os.path.isfile(os.path.join(workdir, filepath))
def test_export_filename_template_1():
""" export photos using filename template """
import glob
import locale
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
locale.setlocale(locale.LC_ALL, "en_US")
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"-V",
"--filename",
"{created.year}-{original_name}",
],
)
assert result.exit_code == 0
workdir = os.getcwd()
files = glob.glob("*.*")
assert sorted(files) == sorted(CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES1)
def test_export_filename_template_2():
""" export photos using filename template with folder_album and path_sep """
import glob
import locale
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
locale.setlocale(locale.LC_ALL, "en_US")
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, PHOTOS_DB_15_5),
".",
"-V",
"--filename",
"{folder_album,None}-{original_name}",
],
)
assert result.exit_code == 0
files = glob.glob("*.*")
assert sorted(files) == sorted(CLI_EXPORTED_FILENAME_TEMPLATE_FILENAMES2)
def test_export_filename_template_3():
""" test --filename with invalid template """
import glob
import os
import os.path
import osxphotos
from osxphotos.__main__ import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"-V",
"--directory",
"{foo}-{original_filename}",
],
)
assert result.exit_code == 2
assert "Error: Invalid template" in result.output
def test_places(): def test_places():
import json import json
import os import os