Initial implementation of configoptions for --save-config, --load-config
This commit is contained in:
@@ -19,9 +19,13 @@ from ._constants import (
|
|||||||
_EXIF_TOOL_URL,
|
_EXIF_TOOL_URL,
|
||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
_UNKNOWN_PLACE,
|
_UNKNOWN_PLACE,
|
||||||
|
DEFAULT_JPEG_QUALITY,
|
||||||
|
DEFAULT_EDITED_SUFFIX,
|
||||||
|
DEFAULT_ORIGINAL_SUFFIX,
|
||||||
UNICODE_FORMAT,
|
UNICODE_FORMAT,
|
||||||
)
|
)
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
|
from .configoptions import ExportOptions, InvalidOptions
|
||||||
from .datetime_formatter import DateTimeFormatter
|
from .datetime_formatter import DateTimeFormatter
|
||||||
from .exiftool import get_exiftool_path
|
from .exiftool import get_exiftool_path
|
||||||
from .export_db import ExportDB, ExportDBInMemory
|
from .export_db import ExportDB, ExportDBInMemory
|
||||||
@@ -39,7 +43,7 @@ VERBOSE = False
|
|||||||
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
|
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
|
||||||
|
|
||||||
|
|
||||||
def verbose(*args, **kwargs):
|
def verbose_(*args, **kwargs):
|
||||||
""" print output if verbose flag set """
|
""" print output if verbose flag set """
|
||||||
if VERBOSE:
|
if VERBOSE:
|
||||||
click.echo(*args, **kwargs)
|
click.echo(*args, **kwargs)
|
||||||
@@ -587,14 +591,14 @@ def cli(ctx, db, json_, debug):
|
|||||||
help="Use with '--dump photos' to dump only certain UUIDs",
|
help="Use with '--dump photos' to dump only certain UUIDs",
|
||||||
multiple=True,
|
multiple=True,
|
||||||
)
|
)
|
||||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose_):
|
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose):
|
||||||
""" Print out debug info """
|
""" Print out debug info """
|
||||||
|
|
||||||
global VERBOSE
|
global VERBOSE
|
||||||
VERBOSE = bool(verbose_)
|
VERBOSE = bool(verbose)
|
||||||
|
|
||||||
db = get_photos_db(*photos_library, db, cli_obj.db)
|
db = get_photos_db(*photos_library, db, cli_obj.db)
|
||||||
if db is None:
|
if db is None:
|
||||||
@@ -605,7 +609,7 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose_):
|
|||||||
|
|
||||||
start_t = time.perf_counter()
|
start_t = time.perf_counter()
|
||||||
print(f"Opening database: {db}")
|
print(f"Opening database: {db}")
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
|
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_)
|
||||||
stop_t = time.perf_counter()
|
stop_t = time.perf_counter()
|
||||||
print(f"Done; took {(stop_t-start_t):.2f} seconds")
|
print(f"Done; took {(stop_t-start_t):.2f} seconds")
|
||||||
|
|
||||||
@@ -1197,7 +1201,7 @@ def query(
|
|||||||
|
|
||||||
@cli.command(cls=ExportCommand)
|
@cli.command(cls=ExportCommand)
|
||||||
@DB_OPTION
|
@DB_OPTION
|
||||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||||
@query_options
|
@query_options
|
||||||
@click.option(
|
@click.option(
|
||||||
"--missing",
|
"--missing",
|
||||||
@@ -1283,11 +1287,10 @@ def query(
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--jpeg-quality",
|
"--jpeg-quality",
|
||||||
type=click.FloatRange(0.0, 1.0),
|
type=click.FloatRange(0.0, 1.0),
|
||||||
default=1.0,
|
|
||||||
help="Value in range 0.0 to 1.0 to use with --convert-to-jpeg. "
|
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 1.0 specifies best quality, "
|
||||||
"a value of 0.0 specifies maximum compression. "
|
"a value of 0.0 specifies maximum compression. "
|
||||||
"Defaults to 1.0.",
|
f"Defaults to {DEFAULT_JPEG_QUALITY}",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--download-missing",
|
"--download-missing",
|
||||||
@@ -1395,15 +1398,13 @@ def query(
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--edited-suffix",
|
"--edited-suffix",
|
||||||
metavar="SUFFIX",
|
metavar="SUFFIX",
|
||||||
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'.",
|
f"would be named 'photoname_bearbeiten.ext'. The default suffix is '{DEFAULT_EDITED_SUFFIX}'.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--original-suffix",
|
"--original-suffix",
|
||||||
metavar="SUFFIX",
|
metavar="SUFFIX",
|
||||||
default="",
|
|
||||||
help="Optional suffix for naming original photos. Default name for original photos is in form "
|
help="Optional suffix for naming original photos. Default name for original photos is in form "
|
||||||
"'filename.ext'. For example, with '--original-suffix _original', the original photo "
|
"'filename.ext'. For example, with '--original-suffix _original', the original photo "
|
||||||
"would be named 'filename_original.ext'. The default suffix is '' (no suffix).",
|
"would be named 'filename_original.ext'. The default suffix is '' (no suffix).",
|
||||||
@@ -1411,7 +1412,6 @@ def query(
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--no-extended-attributes",
|
"--no-extended-attributes",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=False,
|
|
||||||
help="Don't copy extended attributes when exporting. You only need this if exporting "
|
help="Don't copy extended attributes when exporting. You only need this if exporting "
|
||||||
"to a filesystem that doesn't support Mac OS extended attributes. Only use this if you get "
|
"to a filesystem that doesn't support Mac OS extended attributes. Only use this if you get "
|
||||||
"an error while exporting.",
|
"an error while exporting.",
|
||||||
@@ -1419,20 +1419,18 @@ def query(
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--use-photos-export",
|
"--use-photos-export",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=False,
|
|
||||||
help="Force the use of AppleScript or PhotoKit to export even if not missing (see also '--download-missing' and '--use-photokit').",
|
help="Force the use of AppleScript or PhotoKit to export even if not missing (see also '--download-missing' and '--use-photokit').",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--use-photokit",
|
"--use-photokit",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
default=False,
|
|
||||||
help="Use with '--download-missing' or '--use-photos-export' to use direct Photos interface instead of AppleScript to export. "
|
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). "
|
"Highly experimental alpha feature; does not work with iTerm2 (use with Terminal.app). "
|
||||||
"This is faster and more reliable than the default AppleScript interface.",
|
"This is faster and more reliable than the default AppleScript interface.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--report",
|
"--report",
|
||||||
metavar="REPORTNAME.CSV",
|
metavar="<path to export report>",
|
||||||
help="Write a CSV formatted report of all files that were exported.",
|
help="Write a CSV formatted report of all files that were exported.",
|
||||||
type=click.Path(),
|
type=click.Path(),
|
||||||
)
|
)
|
||||||
@@ -1442,6 +1440,29 @@ def query(
|
|||||||
help="Cleanup export directory by deleting any files which were not included in this export set. "
|
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.",
|
"For example, photos which had previously been exported and were subsequently deleted in Photos.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--load-config",
|
||||||
|
required=False,
|
||||||
|
metavar="<config file path>",
|
||||||
|
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 <lots of options here> --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="<config file path>",
|
||||||
|
default=None,
|
||||||
|
help=("Save options to file for use with --load-config. File format is TOML."),
|
||||||
|
type=click.Path(),
|
||||||
|
)
|
||||||
@DB_ARGUMENT
|
@DB_ARGUMENT
|
||||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
@@ -1473,7 +1494,7 @@ def export(
|
|||||||
not_shared,
|
not_shared,
|
||||||
from_date,
|
from_date,
|
||||||
to_date,
|
to_date,
|
||||||
verbose_,
|
verbose,
|
||||||
missing,
|
missing,
|
||||||
update,
|
update,
|
||||||
dry_run,
|
dry_run,
|
||||||
@@ -1537,6 +1558,8 @@ def export(
|
|||||||
use_photokit,
|
use_photokit,
|
||||||
report,
|
report,
|
||||||
cleanup,
|
cleanup,
|
||||||
|
load_config,
|
||||||
|
save_config,
|
||||||
):
|
):
|
||||||
"""Export photos from the Photos database.
|
"""Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -1549,9 +1572,134 @@ def export(
|
|||||||
See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options
|
See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options
|
||||||
to modify this behavior.
|
to modify this behavior.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# NOTE: because of the way ExportOptions 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 = ExportOptions(**locals())
|
||||||
|
|
||||||
|
# print(jpeg_quality, edited_suffix, original_suffix)
|
||||||
|
|
||||||
global VERBOSE
|
global VERBOSE
|
||||||
VERBOSE = bool(verbose_)
|
VERBOSE = bool(verbose)
|
||||||
|
|
||||||
|
if load_config:
|
||||||
|
cfg, error = ExportOptions().load_from_file(load_config, cfg)
|
||||||
|
# print(cfg.asdict())
|
||||||
|
if error:
|
||||||
|
click.echo(f"Error parsing {load_config} config file: {error}", err=True)
|
||||||
|
raise click.Abort()
|
||||||
|
# re-set the local function 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
|
||||||
|
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
|
||||||
|
current_name = cfg.current_name
|
||||||
|
convert_to_jpeg = cfg.convert_to_jpeg
|
||||||
|
jpeg_quality = cfg.jpeg_quality
|
||||||
|
sidecar = cfg.sidecar
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
no_extended_attributes = cfg.no_extended_attributes
|
||||||
|
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
|
||||||
|
|
||||||
|
# config file might have changed verbose
|
||||||
|
VERBOSE = bool(verbose)
|
||||||
|
verbose_(f"Loaded options from file {load_config}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cfg.validate(cli=True)
|
||||||
|
except InvalidOptions as e:
|
||||||
|
click.echo(f"{e.message}")
|
||||||
|
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
|
||||||
|
|
||||||
|
# print(jpeg_quality, edited_suffix, original_suffix)
|
||||||
|
|
||||||
if not os.path.isdir(dest):
|
if not os.path.isdir(dest):
|
||||||
click.echo(f"DEST {dest} must be valid path", err=True)
|
click.echo(f"DEST {dest} must be valid path", err=True)
|
||||||
@@ -1689,12 +1837,12 @@ def export(
|
|||||||
|
|
||||||
if verbose_:
|
if verbose_:
|
||||||
if export_db.was_created:
|
if export_db.was_created:
|
||||||
verbose(f"Created export database {export_db_path}")
|
verbose_(f"Created export database {export_db_path}")
|
||||||
else:
|
else:
|
||||||
verbose(f"Using export database {export_db_path}")
|
verbose_(f"Using export database {export_db_path}")
|
||||||
upgraded = export_db.was_upgraded
|
upgraded = export_db.was_upgraded
|
||||||
if upgraded:
|
if upgraded:
|
||||||
verbose(
|
verbose_(
|
||||||
f"Upgraded export database {export_db_path} from version {upgraded[0]} to {upgraded[1]}"
|
f"Upgraded export database {export_db_path} from version {upgraded[0]} to {upgraded[1]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1790,12 +1938,12 @@ def export(
|
|||||||
results_sidecar_xmp_skipped = []
|
results_sidecar_xmp_skipped = []
|
||||||
results_missing = []
|
results_missing = []
|
||||||
results_error = []
|
results_error = []
|
||||||
if verbose_:
|
if verbose:
|
||||||
for p in photos:
|
for p in photos:
|
||||||
results = export_photo(
|
results = export_photo(
|
||||||
photo=p,
|
photo=p,
|
||||||
dest=dest,
|
dest=dest,
|
||||||
verbose_=verbose_,
|
verbose=verbose,
|
||||||
export_by_date=export_by_date,
|
export_by_date=export_by_date,
|
||||||
sidecar=sidecar,
|
sidecar=sidecar,
|
||||||
update=update,
|
update=update,
|
||||||
@@ -1845,7 +1993,7 @@ def export(
|
|||||||
# for photo_file in set(
|
# for photo_file in set(
|
||||||
# results.exported + results.updated + results.exif_updated
|
# results.exported + results.updated + results.exif_updated
|
||||||
# ):
|
# ):
|
||||||
# verbose(f"Converting {photo_file} to jpeg")
|
# verbose_(f"Converting {photo_file} to jpeg")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# show progress bar
|
# show progress bar
|
||||||
@@ -1854,7 +2002,7 @@ def export(
|
|||||||
results = export_photo(
|
results = export_photo(
|
||||||
photo=p,
|
photo=p,
|
||||||
dest=dest,
|
dest=dest,
|
||||||
verbose_=verbose_,
|
verbose=verbose,
|
||||||
export_by_date=export_by_date,
|
export_by_date=export_by_date,
|
||||||
sidecar=sidecar,
|
sidecar=sidecar,
|
||||||
update=update,
|
update=update,
|
||||||
@@ -1939,7 +2087,7 @@ def export(
|
|||||||
click.echo(f"Deleted: {cleaned_files} {file_str}, {cleaned_dirs} {dir_str}")
|
click.echo(f"Deleted: {cleaned_files} {file_str}, {cleaned_dirs} {dir_str}")
|
||||||
|
|
||||||
if report:
|
if report:
|
||||||
verbose(f"Writing export report to {report}")
|
verbose_(f"Writing export report to {report}")
|
||||||
write_export_report(
|
write_export_report(
|
||||||
report,
|
report,
|
||||||
results_exported=results_exported,
|
results_exported=results_exported,
|
||||||
@@ -2168,7 +2316,7 @@ def _query(
|
|||||||
arguments must be passed in same order as query and export
|
arguments must be passed in same order as query and export
|
||||||
if either is modified, need to ensure all three functions are updated"""
|
if either is modified, need to ensure all three functions are updated"""
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
|
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_)
|
||||||
if deleted or deleted_only:
|
if deleted or deleted_only:
|
||||||
photos = photosdb.photos(
|
photos = photosdb.photos(
|
||||||
uuid=uuid,
|
uuid=uuid,
|
||||||
@@ -2425,7 +2573,7 @@ def get_photos_by_attribute(photos, attribute, values, ignore_case):
|
|||||||
def export_photo(
|
def export_photo(
|
||||||
photo=None,
|
photo=None,
|
||||||
dest=None,
|
dest=None,
|
||||||
verbose_=None,
|
verbose=None,
|
||||||
export_by_date=None,
|
export_by_date=None,
|
||||||
sidecar=None,
|
sidecar=None,
|
||||||
update=None,
|
update=None,
|
||||||
@@ -2462,7 +2610,7 @@ def export_photo(
|
|||||||
Args:
|
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
|
||||||
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
|
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: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
|
||||||
export_as_hardlink: boolean; hardlink files instead of copying them
|
export_as_hardlink: boolean; hardlink files instead of copying them
|
||||||
@@ -2498,7 +2646,7 @@ def export_photo(
|
|||||||
ValueError on invalid filename_template
|
ValueError on invalid filename_template
|
||||||
"""
|
"""
|
||||||
global VERBOSE
|
global VERBOSE
|
||||||
VERBOSE = bool(verbose_)
|
VERBOSE = bool(verbose)
|
||||||
|
|
||||||
results_exported = []
|
results_exported = []
|
||||||
results_new = []
|
results_new = []
|
||||||
@@ -2528,7 +2676,7 @@ def export_photo(
|
|||||||
# requested edited version but it's missing, download original
|
# requested edited version but it's missing, download original
|
||||||
export_original = True
|
export_original = True
|
||||||
export_edited = False
|
export_edited = False
|
||||||
verbose(
|
verbose_(
|
||||||
f"Edited file for {photo.original_filename} is missing, exporting original"
|
f"Edited file for {photo.original_filename} is missing, exporting original"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2567,7 +2715,7 @@ def export_photo(
|
|||||||
else:
|
else:
|
||||||
original_filename = filename
|
original_filename = filename
|
||||||
|
|
||||||
verbose(
|
verbose_(
|
||||||
f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename}"
|
f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2599,8 +2747,8 @@ def export_photo(
|
|||||||
# original is missing, in which case we should download the edited version
|
# original is missing, in which case we should download the edited version
|
||||||
if export_original:
|
if export_original:
|
||||||
if missing_original:
|
if missing_original:
|
||||||
space = " " if not verbose_ else ""
|
space = " " if not verbose else ""
|
||||||
verbose(f"{space}Skipping missing photo {photo.original_filename}")
|
verbose_(f"{space}Skipping missing photo {photo.original_filename}")
|
||||||
results_missing.append(
|
results_missing.append(
|
||||||
str(pathlib.Path(dest_path) / original_filename)
|
str(pathlib.Path(dest_path) / original_filename)
|
||||||
)
|
)
|
||||||
@@ -2631,7 +2779,7 @@ def export_photo(
|
|||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
verbose=verbose,
|
verbose=verbose_,
|
||||||
)
|
)
|
||||||
|
|
||||||
results_exported.extend(export_results.exported)
|
results_exported.extend(export_results.exported)
|
||||||
@@ -2663,7 +2811,7 @@ def export_photo(
|
|||||||
str(pathlib.Path(dest) / original_filename)
|
str(pathlib.Path(dest) / original_filename)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
verbose(f"Skipping original version of {photo.original_filename}")
|
verbose_(f"Skipping original version of {photo.original_filename}")
|
||||||
|
|
||||||
# if export-edited, also export the edited version
|
# if export-edited, also export the edited version
|
||||||
# verify the photo has adjustments and valid path to avoid raising an exception
|
# verify the photo has adjustments and valid path to avoid raising an exception
|
||||||
@@ -2680,12 +2828,12 @@ def export_photo(
|
|||||||
# will be corrected by use_photos_export
|
# will be corrected by use_photos_export
|
||||||
edited_ext = pathlib.Path(photo.filename).suffix
|
edited_ext = pathlib.Path(photo.filename).suffix
|
||||||
edited_filename = f"{edited_filename.stem}{edited_suffix}{edited_ext}"
|
edited_filename = f"{edited_filename.stem}{edited_suffix}{edited_ext}"
|
||||||
verbose(
|
verbose_(
|
||||||
f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}"
|
f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}"
|
||||||
)
|
)
|
||||||
if missing_edited:
|
if missing_edited:
|
||||||
space = " " if not verbose_ else ""
|
space = " " if not verbose else ""
|
||||||
verbose(f"{space}Skipping missing edited photo for {filename}")
|
verbose_(f"{space}Skipping missing edited photo for {filename}")
|
||||||
results_missing.append(
|
results_missing.append(
|
||||||
str(pathlib.Path(dest_path) / edited_filename)
|
str(pathlib.Path(dest_path) / edited_filename)
|
||||||
)
|
)
|
||||||
@@ -2715,7 +2863,7 @@ def export_photo(
|
|||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
verbose=verbose,
|
verbose=verbose_,
|
||||||
)
|
)
|
||||||
|
|
||||||
results_exported.extend(export_results_edited.exported)
|
results_exported.extend(export_results_edited.exported)
|
||||||
@@ -2747,19 +2895,19 @@ def export_photo(
|
|||||||
)
|
)
|
||||||
results_error.extend(str(pathlib.Path(dest) / edited_filename))
|
results_error.extend(str(pathlib.Path(dest) / edited_filename))
|
||||||
|
|
||||||
if verbose_:
|
if verbose:
|
||||||
if update:
|
if update:
|
||||||
for new in results_new:
|
for new in results_new:
|
||||||
verbose(f"Exported new file {new}")
|
verbose_(f"Exported new file {new}")
|
||||||
for updated in results_updated:
|
for updated in results_updated:
|
||||||
verbose(f"Exported updated file {updated}")
|
verbose_(f"Exported updated file {updated}")
|
||||||
for skipped in results_skipped:
|
for skipped in results_skipped:
|
||||||
verbose(f"Skipped up to date file {skipped}")
|
verbose_(f"Skipped up to date file {skipped}")
|
||||||
else:
|
else:
|
||||||
for exported in results_exported:
|
for exported in results_exported:
|
||||||
verbose(f"Exported {exported}")
|
verbose_(f"Exported {exported}")
|
||||||
for touched in results_touched:
|
for touched in results_touched:
|
||||||
verbose(f"Touched date on file {touched}")
|
verbose_(f"Touched date on file {touched}")
|
||||||
|
|
||||||
return ExportResults(
|
return ExportResults(
|
||||||
exported=results_exported,
|
exported=results_exported,
|
||||||
@@ -3064,7 +3212,7 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
|
|||||||
for p in pathlib.Path(dest_path).rglob("*"):
|
for p in pathlib.Path(dest_path).rglob("*"):
|
||||||
path = str(p).lower()
|
path = str(p).lower()
|
||||||
if p.is_file() and path not in keepers:
|
if p.is_file() and path not in keepers:
|
||||||
verbose(f"Deleting {p}")
|
verbose_(f"Deleting {p}")
|
||||||
fileutil.unlink(p)
|
fileutil.unlink(p)
|
||||||
deleted_files += 1
|
deleted_files += 1
|
||||||
|
|
||||||
@@ -3074,7 +3222,7 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
|
|||||||
path = str(p).lower()
|
path = str(p).lower()
|
||||||
# if directory and directory is empty
|
# if directory and directory is empty
|
||||||
if p.is_dir() and not next(p.iterdir(), False):
|
if p.is_dir() and not next(p.iterdir(), False):
|
||||||
verbose(f"Deleting empty directory {p}")
|
verbose_(f"Deleting empty directory {p}")
|
||||||
fileutil.rmdir(p)
|
fileutil.rmdir(p)
|
||||||
deleted_dirs += 1
|
deleted_dirs += 1
|
||||||
|
|
||||||
|
|||||||
@@ -109,3 +109,13 @@ MAX_FILENAME_LEN = 255
|
|||||||
# Max directory name length on MacOS
|
# Max directory name length on MacOS
|
||||||
MAX_DIRNAME_LEN = 255
|
MAX_DIRNAME_LEN = 255
|
||||||
|
|
||||||
|
# Default JPEG quality when converting to JPEG
|
||||||
|
DEFAULT_JPEG_QUALITY = 1.0
|
||||||
|
|
||||||
|
# Default suffix to add to edited images
|
||||||
|
DEFAULT_EDITED_SUFFIX = "_edited"
|
||||||
|
|
||||||
|
# Default suffix to add to original images
|
||||||
|
DEFAULT_ORIGINAL_SUFFIX = ""
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.37.6"
|
__version__ = "0.37.8"
|
||||||
|
|
||||||
|
|||||||
311
osxphotos/configoptions.py
Normal file
311
osxphotos/configoptions.py
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
""" Classes to load/save config settings for osxphotos CLI """
|
||||||
|
import toml
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidOptions(Exception):
|
||||||
|
""" Invalid combination of options. """
|
||||||
|
|
||||||
|
def __init__(self, message):
|
||||||
|
self.message = message
|
||||||
|
super().__init__(self.message)
|
||||||
|
|
||||||
|
|
||||||
|
class OSXPhotosOptions:
|
||||||
|
""" data class to store and load options for osxphotos commands """
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
args = locals()
|
||||||
|
|
||||||
|
self._attrs = {}
|
||||||
|
self._exclusive = []
|
||||||
|
|
||||||
|
self.set_attributes(args)
|
||||||
|
|
||||||
|
def set_attributes(self, args):
|
||||||
|
for attr in self._attrs:
|
||||||
|
try:
|
||||||
|
arg = args[attr]
|
||||||
|
# don't test 'not arg'; need to handle empty strings as valid values
|
||||||
|
if arg is None or arg == False:
|
||||||
|
if self._attrs[attr] == ():
|
||||||
|
setattr(self, attr, ())
|
||||||
|
else:
|
||||||
|
setattr(self, attr, self._attrs[attr])
|
||||||
|
else:
|
||||||
|
setattr(self, attr, arg)
|
||||||
|
except KeyError:
|
||||||
|
raise KeyError(f"Missing argument: {attr}")
|
||||||
|
|
||||||
|
def validate(self, cli=False):
|
||||||
|
""" validate combinations of otions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cli: bool, set to True if called to validate CLI options; will prepend '--' to option names in InvalidOptions.message
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if all options valid
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
InvalidOption if any combination of options is invalid
|
||||||
|
InvalidOption.message will be descriptive message of invalid options
|
||||||
|
"""
|
||||||
|
prefix = "--" if cli else ""
|
||||||
|
for opt_pair in self._exclusive:
|
||||||
|
val0 = getattr(self, opt_pair[0])
|
||||||
|
val1 = getattr(self, opt_pair[1])
|
||||||
|
val0 = any(val0) if self._attrs[opt_pair[0]] == () else val0
|
||||||
|
val1 = any(val1) if self._attrs[opt_pair[1]] == () else val1
|
||||||
|
if val0 and val1:
|
||||||
|
raise InvalidOptions(
|
||||||
|
f"{prefix}{opt_pair[0]} and {prefix}{opt_pair[1]} options cannot be used together"
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def write_to_file(self, filename):
|
||||||
|
""" Write self to TOML file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: full path to TOML file to write; filename will be overwritten if it exists
|
||||||
|
"""
|
||||||
|
data = {}
|
||||||
|
for attr in sorted(self._attrs.keys()):
|
||||||
|
val = getattr(self, attr)
|
||||||
|
if val in [False, ()]:
|
||||||
|
val = None
|
||||||
|
else:
|
||||||
|
val = list(val) if type(val) == tuple else val
|
||||||
|
|
||||||
|
data[attr] = val
|
||||||
|
|
||||||
|
with open(filename, "w") as fd:
|
||||||
|
toml.dump({"export": data}, fd)
|
||||||
|
|
||||||
|
def load_from_file(self, filename, override=None):
|
||||||
|
""" Load options from a TOML file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: full path to TOML file
|
||||||
|
override: optional ExportOptions object;
|
||||||
|
if provided, any value that's set in override will be used
|
||||||
|
to override what's in the TOML file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(ExportOptions, error): tuple of ExportOption object and error string;
|
||||||
|
if there are any errors during the parsing of the TOML file, error will be set
|
||||||
|
to a descriptive error message otherwise it will be None
|
||||||
|
"""
|
||||||
|
override = override or ExportOptions()
|
||||||
|
loaded = toml.load(filename)
|
||||||
|
options = ExportOptions()
|
||||||
|
if "export" not in loaded:
|
||||||
|
return options, f"[export] section missing from {filename}"
|
||||||
|
|
||||||
|
for attr in loaded["export"]:
|
||||||
|
if attr not in self._attrs:
|
||||||
|
return options, f"Unknown option: {attr}: {loaded['export'][attr]}"
|
||||||
|
val = loaded["export"][attr]
|
||||||
|
val = getattr(override, attr) or val
|
||||||
|
if self._attrs[attr] == ():
|
||||||
|
val = tuple(val)
|
||||||
|
setattr(options, attr, val)
|
||||||
|
return options, None
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
return {attr: getattr(self, attr) for attr in sorted(self._attrs.keys())}
|
||||||
|
|
||||||
|
|
||||||
|
class ExportOptions(OSXPhotosOptions):
|
||||||
|
""" data class to store and load options for export command """
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
db=None,
|
||||||
|
photos_library=None,
|
||||||
|
keyword=None,
|
||||||
|
person=None,
|
||||||
|
album=None,
|
||||||
|
folder=None,
|
||||||
|
uuid=None,
|
||||||
|
uuid_from_file=None,
|
||||||
|
title=None,
|
||||||
|
no_title=False,
|
||||||
|
description=None,
|
||||||
|
no_description=False,
|
||||||
|
uti=None,
|
||||||
|
ignore_case=False,
|
||||||
|
edited=False,
|
||||||
|
external_edit=False,
|
||||||
|
favorite=False,
|
||||||
|
not_favorite=False,
|
||||||
|
hidden=False,
|
||||||
|
not_hidden=False,
|
||||||
|
shared=False,
|
||||||
|
not_shared=False,
|
||||||
|
from_date=None,
|
||||||
|
to_date=None,
|
||||||
|
verbose=False,
|
||||||
|
missing=False,
|
||||||
|
update=True,
|
||||||
|
dry_run=False,
|
||||||
|
export_as_hardlink=False,
|
||||||
|
touch_file=False,
|
||||||
|
overwrite=False,
|
||||||
|
export_by_date=False,
|
||||||
|
skip_edited=False,
|
||||||
|
skip_original_if_edited=False,
|
||||||
|
skip_bursts=False,
|
||||||
|
skip_live=False,
|
||||||
|
skip_raw=False,
|
||||||
|
person_keyword=False,
|
||||||
|
album_keyword=False,
|
||||||
|
keyword_template=None,
|
||||||
|
description_template=None,
|
||||||
|
current_name=False,
|
||||||
|
convert_to_jpeg=False,
|
||||||
|
jpeg_quality=None,
|
||||||
|
sidecar=None,
|
||||||
|
only_photos=False,
|
||||||
|
only_movies=False,
|
||||||
|
burst=False,
|
||||||
|
not_burst=False,
|
||||||
|
live=False,
|
||||||
|
not_live=False,
|
||||||
|
download_missing=False,
|
||||||
|
exiftool=False,
|
||||||
|
ignore_date_modified=False,
|
||||||
|
portrait=False,
|
||||||
|
not_portrait=False,
|
||||||
|
screenshot=False,
|
||||||
|
not_screenshot=False,
|
||||||
|
slow_mo=False,
|
||||||
|
not_slow_mo=False,
|
||||||
|
time_lapse=False,
|
||||||
|
not_time_lapse=False,
|
||||||
|
hdr=False,
|
||||||
|
not_hdr=False,
|
||||||
|
selfie=False,
|
||||||
|
not_selfie=False,
|
||||||
|
panorama=False,
|
||||||
|
not_panorama=False,
|
||||||
|
has_raw=False,
|
||||||
|
directory=None,
|
||||||
|
filename_template=None,
|
||||||
|
edited_suffix=None,
|
||||||
|
original_suffix=None,
|
||||||
|
place=None,
|
||||||
|
no_place=False,
|
||||||
|
has_comment=False,
|
||||||
|
no_comment=False,
|
||||||
|
has_likes=False,
|
||||||
|
no_likes=False,
|
||||||
|
no_extended_attributes=False,
|
||||||
|
label=None,
|
||||||
|
deleted=False,
|
||||||
|
deleted_only=False,
|
||||||
|
use_photos_export=False,
|
||||||
|
use_photokit=False,
|
||||||
|
report=None,
|
||||||
|
cleanup=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
args = locals()
|
||||||
|
|
||||||
|
# valid attributes and default values
|
||||||
|
self._attrs = {
|
||||||
|
"db": None,
|
||||||
|
"photos_library": (),
|
||||||
|
"keyword": (),
|
||||||
|
"person": (),
|
||||||
|
"album": (),
|
||||||
|
"folder": (),
|
||||||
|
"uuid": (),
|
||||||
|
"uuid_from_file": None,
|
||||||
|
"title": (),
|
||||||
|
"no_title": False,
|
||||||
|
"description": (),
|
||||||
|
"no_description": False,
|
||||||
|
"uti": None,
|
||||||
|
"ignore_case": False,
|
||||||
|
"edited": False,
|
||||||
|
"external_edit": False,
|
||||||
|
"favorite": False,
|
||||||
|
"not_favorite": False,
|
||||||
|
"hidden": False,
|
||||||
|
"not_hidden": False,
|
||||||
|
"shared": False,
|
||||||
|
"not_shared": False,
|
||||||
|
"from_date": None,
|
||||||
|
"to_date": None,
|
||||||
|
"verbose": False,
|
||||||
|
"missing": False,
|
||||||
|
"update": False,
|
||||||
|
"dry_run": False,
|
||||||
|
"export_as_hardlink": False,
|
||||||
|
"touch_file": False,
|
||||||
|
"overwrite": False,
|
||||||
|
"export_by_date": False,
|
||||||
|
"skip_edited": False,
|
||||||
|
"skip_original_if_edited": False,
|
||||||
|
"skip_bursts": False,
|
||||||
|
"skip_live": False,
|
||||||
|
"skip_raw": False,
|
||||||
|
"person_keyword": False,
|
||||||
|
"album_keyword": False,
|
||||||
|
"keyword_template": (),
|
||||||
|
"description_template": None,
|
||||||
|
"current_name": False,
|
||||||
|
"convert_to_jpeg": False,
|
||||||
|
"jpeg_quality": None,
|
||||||
|
"sidecar": (),
|
||||||
|
"only_photos": False,
|
||||||
|
"only_movies": False,
|
||||||
|
"burst": False,
|
||||||
|
"not_burst": False,
|
||||||
|
"live": False,
|
||||||
|
"not_live": False,
|
||||||
|
"download_missing": False,
|
||||||
|
"exiftool": False,
|
||||||
|
"ignore_date_modified": False,
|
||||||
|
"portrait": False,
|
||||||
|
"not_portrait": False,
|
||||||
|
"screenshot": False,
|
||||||
|
"not_screenshot": False,
|
||||||
|
"slow_mo": False,
|
||||||
|
"not_slow_mo": False,
|
||||||
|
"time_lapse": False,
|
||||||
|
"not_time_lapse": False,
|
||||||
|
"hdr": False,
|
||||||
|
"not_hdr": False,
|
||||||
|
"selfie": False,
|
||||||
|
"not_selfie": False,
|
||||||
|
"panorama": False,
|
||||||
|
"not_panorama": False,
|
||||||
|
"has_raw": False,
|
||||||
|
"directory": None,
|
||||||
|
"filename_template": None,
|
||||||
|
"edited_suffix": None,
|
||||||
|
"original_suffix": None,
|
||||||
|
"place": (),
|
||||||
|
"no_place": False,
|
||||||
|
"has_comment": False,
|
||||||
|
"no_comment": False,
|
||||||
|
"has_likes": False,
|
||||||
|
"no_likes": False,
|
||||||
|
"no_extended_attributes": False,
|
||||||
|
"label": (),
|
||||||
|
"deleted": False,
|
||||||
|
"deleted_only": False,
|
||||||
|
"use_photos_export": False,
|
||||||
|
"use_photokit": False,
|
||||||
|
"report": None,
|
||||||
|
"cleanup": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
self._exclusive = [
|
||||||
|
["favorite", "not_favorite"],
|
||||||
|
["hidden", "not_hidden"],
|
||||||
|
["title", "no_title"],
|
||||||
|
["description", "no_description"],
|
||||||
|
]
|
||||||
|
|
||||||
|
self.set_attributes(args)
|
||||||
Reference in New Issue
Block a user