Initial multiprocessing implementation, #582
This commit is contained in:
@@ -1180,6 +1180,9 @@ Options:
|
|||||||
--save-config <config file path>
|
--save-config <config file path>
|
||||||
Save options to file for use with --load-
|
Save options to file for use with --load-
|
||||||
config. File format is TOML.
|
config. File format is TOML.
|
||||||
|
-M, --multiprocess NUMBER_OF_PROCESSES
|
||||||
|
Run export in parallel using
|
||||||
|
NUMBER_OF_PROCESSES processes. [x>=1]
|
||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
|
|
||||||
** Export **
|
** Export **
|
||||||
|
|||||||
767
osxphotos/cli.py
767
osxphotos/cli.py
@@ -8,6 +8,7 @@ import dataclasses
|
|||||||
import datetime
|
import datetime
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
|
import multiprocessing as mp
|
||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import pathlib
|
import pathlib
|
||||||
@@ -27,8 +28,10 @@ import osxmetadata
|
|||||||
import photoscript
|
import photoscript
|
||||||
import rich.traceback
|
import rich.traceback
|
||||||
import yaml
|
import yaml
|
||||||
|
from more_itertools import divide
|
||||||
from rich import pretty, print
|
from rich import pretty, print
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from rich.progress import Progress
|
||||||
from rich.syntax import Syntax
|
from rich.syntax import Syntax
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@@ -148,7 +151,7 @@ def verbose_(*args, **kwargs):
|
|||||||
"""print output if verbose flag set"""
|
"""print output if verbose flag set"""
|
||||||
if VERBOSE:
|
if VERBOSE:
|
||||||
styled_args = []
|
styled_args = []
|
||||||
timestamp = str(datetime.datetime.now()) + " -- " if VERBOSE_TIMESTAMP else ""
|
timestamp = f"[{datetime.datetime.now()}] -- " if VERBOSE_TIMESTAMP else ""
|
||||||
for arg in args:
|
for arg in args:
|
||||||
if type(arg) == str:
|
if type(arg) == str:
|
||||||
arg = timestamp + arg
|
arg = timestamp + arg
|
||||||
@@ -1169,6 +1172,13 @@ def cli(ctx, db, json_, debug):
|
|||||||
help=("Save options to file for use with --load-config. File format is TOML."),
|
help=("Save options to file for use with --load-config. File format is TOML."),
|
||||||
type=click.Path(),
|
type=click.Path(),
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--multiprocess",
|
||||||
|
"-M",
|
||||||
|
metavar="NUMBER_OF_PROCESSES",
|
||||||
|
help="Run export in parallel using NUMBER_OF_PROCESSES processes. ",
|
||||||
|
type=click.IntRange(min=1),
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--beta",
|
"--beta",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -1338,6 +1348,7 @@ def export(
|
|||||||
preview_if_missing,
|
preview_if_missing,
|
||||||
profile,
|
profile,
|
||||||
profile_sort,
|
profile_sort,
|
||||||
|
multiprocess,
|
||||||
):
|
):
|
||||||
"""Export photos from the Photos database.
|
"""Export photos from the Photos database.
|
||||||
Export path DEST is required.
|
Export path DEST is required.
|
||||||
@@ -1868,198 +1879,28 @@ def export(
|
|||||||
# store results of export
|
# store results of export
|
||||||
results = ExportResults()
|
results = ExportResults()
|
||||||
|
|
||||||
if photos:
|
if not photos:
|
||||||
|
click.echo("Did not find any photos to export")
|
||||||
|
else:
|
||||||
num_photos = len(photos)
|
num_photos = len(photos)
|
||||||
# TODO: photos or photo appears several times, pull into a separate function
|
# 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()
|
||||||
# though the command line option is current_name, internally all processing
|
if multiprocess:
|
||||||
# logic uses original_name which is the boolean inverse of current_name
|
results = _export_photos_with_multiprocessing(
|
||||||
# because the original code used --original-name as an option
|
photos, kwargs={**locals(), **globals()}
|
||||||
original_name = not current_name
|
|
||||||
|
|
||||||
# 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 = (
|
else:
|
||||||
PhotosAlbum(add_skipped_to_album, verbose=verbose_)
|
# some hackery to get the arguments for export_photos
|
||||||
if add_skipped_to_album
|
export_args = export_photos.__code__.co_varnames
|
||||||
else None
|
results = export_photos(
|
||||||
|
**{
|
||||||
|
k: v
|
||||||
|
for k, v in {**locals(), **globals()}.items()
|
||||||
|
if k in export_args
|
||||||
|
}
|
||||||
)
|
)
|
||||||
album_missing = (
|
|
||||||
PhotosAlbum(add_missing_to_album, verbose=verbose_)
|
|
||||||
if add_missing_to_album
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
photo_num = 0
|
|
||||||
# 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, show_pos=True, file=fp) as bar:
|
|
||||||
for p in bar:
|
|
||||||
photo_num += 1
|
|
||||||
export_results = export_photo(
|
|
||||||
photo=p,
|
|
||||||
dest=dest,
|
|
||||||
verbose=verbose,
|
|
||||||
export_by_date=export_by_date,
|
|
||||||
sidecar=sidecar,
|
|
||||||
sidecar_drop_ext=sidecar_drop_ext,
|
|
||||||
update=update,
|
|
||||||
ignore_signature=ignore_signature,
|
|
||||||
export_as_hardlink=export_as_hardlink,
|
|
||||||
overwrite=overwrite,
|
|
||||||
export_edited=export_edited,
|
|
||||||
skip_original_if_edited=skip_original_if_edited,
|
|
||||||
original_name=original_name,
|
|
||||||
export_live=export_live,
|
|
||||||
download_missing=download_missing,
|
|
||||||
exiftool=exiftool,
|
|
||||||
exiftool_merge_keywords=exiftool_merge_keywords,
|
|
||||||
exiftool_merge_persons=exiftool_merge_persons,
|
|
||||||
directory=directory,
|
|
||||||
filename_template=filename_template,
|
|
||||||
export_raw=export_raw,
|
|
||||||
album_keyword=album_keyword,
|
|
||||||
person_keyword=person_keyword,
|
|
||||||
keyword_template=keyword_template,
|
|
||||||
description_template=description_template,
|
|
||||||
export_db=export_db,
|
|
||||||
fileutil=fileutil,
|
|
||||||
dry_run=dry_run,
|
|
||||||
touch_file=touch_file,
|
|
||||||
edited_suffix=edited_suffix,
|
|
||||||
original_suffix=original_suffix,
|
|
||||||
use_photos_export=use_photos_export,
|
|
||||||
convert_to_jpeg=convert_to_jpeg,
|
|
||||||
jpeg_quality=jpeg_quality,
|
|
||||||
ignore_date_modified=ignore_date_modified,
|
|
||||||
use_photokit=use_photokit,
|
|
||||||
exiftool_option=exiftool_option,
|
|
||||||
strip=strip,
|
|
||||||
jpeg_ext=jpeg_ext,
|
|
||||||
replace_keywords=replace_keywords,
|
|
||||||
retry=retry,
|
|
||||||
export_dir=dest,
|
|
||||||
export_preview=preview,
|
|
||||||
preview_suffix=preview_suffix,
|
|
||||||
preview_if_missing=preview_if_missing,
|
|
||||||
photo_num=photo_num,
|
|
||||||
num_photos=num_photos,
|
|
||||||
)
|
|
||||||
|
|
||||||
if post_function:
|
|
||||||
for function in post_function:
|
|
||||||
# post function is tuple of (function, filename.py::function_name)
|
|
||||||
verbose_(f"Calling post-function {function[1]}")
|
|
||||||
if not dry_run:
|
|
||||||
try:
|
|
||||||
function[0](p, export_results, verbose_)
|
|
||||||
except Exception as e:
|
|
||||||
click.secho(
|
|
||||||
f"Error running post-function {function[1]}: {e}",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
err=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
run_post_command(
|
|
||||||
photo=p,
|
|
||||||
post_command=post_command,
|
|
||||||
export_results=export_results,
|
|
||||||
export_dir=dest,
|
|
||||||
dry_run=dry_run,
|
|
||||||
exiftool_path=exiftool_path,
|
|
||||||
export_db=export_db,
|
|
||||||
)
|
|
||||||
|
|
||||||
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.secho(
|
|
||||||
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_export.name}: {e}",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
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.secho(
|
|
||||||
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_skipped.name}: {e}",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
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.secho(
|
|
||||||
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_missing.name}: {e}",
|
|
||||||
fg=CLI_COLOR_ERROR,
|
|
||||||
err=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
results += export_results
|
|
||||||
|
|
||||||
# all photo files (not including sidecars) that are part of this export set
|
|
||||||
# used below for applying Finder tags, etc.
|
|
||||||
photo_files = set(
|
|
||||||
export_results.exported
|
|
||||||
+ export_results.new
|
|
||||||
+ export_results.updated
|
|
||||||
+ export_results.exif_updated
|
|
||||||
+ export_results.converted_to_jpeg
|
|
||||||
+ export_results.skipped
|
|
||||||
)
|
|
||||||
|
|
||||||
if finder_tag_keywords or finder_tag_template:
|
|
||||||
tags_written, tags_skipped = write_finder_tags(
|
|
||||||
p,
|
|
||||||
photo_files,
|
|
||||||
keywords=finder_tag_keywords,
|
|
||||||
keyword_template=keyword_template,
|
|
||||||
album_keyword=album_keyword,
|
|
||||||
person_keyword=person_keyword,
|
|
||||||
exiftool_merge_keywords=exiftool_merge_keywords,
|
|
||||||
finder_tag_template=finder_tag_template,
|
|
||||||
strip=strip,
|
|
||||||
export_dir=dest,
|
|
||||||
)
|
|
||||||
results.xattr_written.extend(tags_written)
|
|
||||||
results.xattr_skipped.extend(tags_skipped)
|
|
||||||
|
|
||||||
if xattr_template:
|
|
||||||
xattr_written, xattr_skipped = write_extended_attributes(
|
|
||||||
p,
|
|
||||||
photo_files,
|
|
||||||
xattr_template,
|
|
||||||
strip=strip,
|
|
||||||
export_dir=dest,
|
|
||||||
)
|
|
||||||
results.xattr_written.extend(xattr_written)
|
|
||||||
results.xattr_skipped.extend(xattr_skipped)
|
|
||||||
|
|
||||||
if fp is not None:
|
|
||||||
fp.close()
|
|
||||||
|
|
||||||
photo_str_total = "photos" if len(photos) != 1 else "photo"
|
photo_str_total = "photos" if len(photos) != 1 else "photo"
|
||||||
if update:
|
if update:
|
||||||
@@ -2082,8 +1923,6 @@ def export(
|
|||||||
click.echo(summary)
|
click.echo(summary)
|
||||||
stop_time = time.perf_counter()
|
stop_time = time.perf_counter()
|
||||||
click.echo(f"Elapsed time: {format_sec_to_hhmmss(stop_time-start_time)}")
|
click.echo(f"Elapsed time: {format_sec_to_hhmmss(stop_time-start_time)}")
|
||||||
else:
|
|
||||||
click.echo("Did not find any photos to export")
|
|
||||||
|
|
||||||
# cleanup files and do report if needed
|
# cleanup files and do report if needed
|
||||||
if cleanup:
|
if cleanup:
|
||||||
@@ -2124,16 +1963,102 @@ def export(
|
|||||||
export_db.close()
|
export_db.close()
|
||||||
|
|
||||||
|
|
||||||
def _export_with_profiler(args: Dict):
|
def _export_photos_with_multiprocessing(photos: List, kwargs: Dict) -> ExportResults():
|
||||||
""" "Run export with cProfile"""
|
"""Run export using multiple processes"""
|
||||||
try:
|
try:
|
||||||
args.pop("profile")
|
num_procs = kwargs.get("multiprocess")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
raise ValueError("_export_runner called without multiprocess param")
|
||||||
|
|
||||||
|
# build kwargs for export_photos
|
||||||
|
# keep only the params export_photos expects
|
||||||
|
export_args = export_photos.__code__.co_varnames
|
||||||
|
kwargs = {arg: value for arg, value in kwargs.items() if arg in export_args}
|
||||||
|
for arg in ["photosdb", "photos"]:
|
||||||
|
kwargs.pop(arg, None)
|
||||||
|
kwargs["photos"] = None
|
||||||
|
|
||||||
|
# can't pickle an open sqlite connection so ensure export_db is closed
|
||||||
|
export_db = kwargs.get("export_db")
|
||||||
|
export_db.close()
|
||||||
|
|
||||||
|
# verbose output?
|
||||||
|
verbose = kwargs.get("verbose", None)
|
||||||
|
|
||||||
|
# get list of uuids to pass to export_photos
|
||||||
|
uuids = [p.uuid for p in photos]
|
||||||
|
uuid_chunks = [list(chunk) for chunk in divide(num_procs, uuids)]
|
||||||
|
|
||||||
|
# create a queue to communicate with processes
|
||||||
|
q = mp.Queue()
|
||||||
|
processes = []
|
||||||
|
if len(uuid_chunks) < num_procs:
|
||||||
|
num_procs = len(uuid_chunks)
|
||||||
|
for i in range(num_procs):
|
||||||
|
kwargs = kwargs.copy()
|
||||||
|
kwargs["_mp_queue"] = q
|
||||||
|
kwargs["_mp_process_total"] = num_procs
|
||||||
|
kwargs["_mp_process_num"] = i
|
||||||
|
kwargs["_mp_uuids"] = uuid_chunks[i]
|
||||||
|
if not kwargs["_mp_uuids"]:
|
||||||
|
click.echo(f"Out of UUIDs to process, skipping process {i}")
|
||||||
|
continue
|
||||||
|
click.echo(f"Starting process number #{i}")
|
||||||
|
p = mp.Process(target=export_photos, kwargs=kwargs)
|
||||||
|
p.start()
|
||||||
|
processes.append(p)
|
||||||
|
|
||||||
|
class FakeProgress:
|
||||||
|
def __init__(self):
|
||||||
|
self.finished = False
|
||||||
|
self.console = Console()
|
||||||
|
|
||||||
|
def add_task(self, task, total):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
cProfile.runctx(
|
def update(self, task_id, completed):
|
||||||
"_export(**args)", globals=globals(), locals=locals(), sort="tottime"
|
pass
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||||
|
pass
|
||||||
|
|
||||||
|
progress_class = Progress if not verbose else FakeProgress
|
||||||
|
export_results = ExportResults()
|
||||||
|
with progress_class() as progress:
|
||||||
|
tasks = []
|
||||||
|
for i, p in enumerate(processes):
|
||||||
|
tasks.append(
|
||||||
|
progress.add_task(
|
||||||
|
f"Process {i} ({len(uuid_chunks[i])} photos)...",
|
||||||
|
total=len(uuid_chunks[i]),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
while not progress.finished:
|
||||||
|
while True:
|
||||||
|
if not any(mp.active_children()):
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
results = q.get(timeout=0.5)
|
||||||
|
# print(results)
|
||||||
|
if results[1] == "VERBOSE":
|
||||||
|
progress.console.print(f"{results[0]}: {results[2]}")
|
||||||
|
# verbose_(f"{results[0]}: {results[2]}")
|
||||||
|
elif results[1] == "DONE":
|
||||||
|
# click.echo(f"Process {results[0]} is done")
|
||||||
|
export_results += ExportResults(**results[2])
|
||||||
|
if isinstance(progress, FakeProgress):
|
||||||
|
progress.finished = True
|
||||||
|
elif results[1] == "PROGRESS":
|
||||||
|
progress.update(tasks[results[0]], completed=results[2])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
click.echo("All processes finished")
|
||||||
|
return export_results
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@@ -2582,100 +2507,387 @@ def print_photo_info(photos, json=False):
|
|||||||
csv_writer.writerow(row)
|
csv_writer.writerow(row)
|
||||||
|
|
||||||
|
|
||||||
|
def export_photos(
|
||||||
|
add_exported_to_album,
|
||||||
|
add_missing_to_album,
|
||||||
|
add_skipped_to_album,
|
||||||
|
album_keyword,
|
||||||
|
convert_to_jpeg,
|
||||||
|
current_name,
|
||||||
|
db,
|
||||||
|
description_template,
|
||||||
|
dest,
|
||||||
|
directory,
|
||||||
|
download_missing,
|
||||||
|
dry_run,
|
||||||
|
edited_suffix,
|
||||||
|
exiftool_merge_keywords,
|
||||||
|
exiftool_merge_persons,
|
||||||
|
exiftool_option,
|
||||||
|
exiftool_path,
|
||||||
|
exiftool,
|
||||||
|
export_as_hardlink,
|
||||||
|
export_by_date,
|
||||||
|
export_db,
|
||||||
|
export_edited,
|
||||||
|
export_live,
|
||||||
|
export_raw,
|
||||||
|
filename_template,
|
||||||
|
fileutil,
|
||||||
|
finder_tag_keywords,
|
||||||
|
finder_tag_template,
|
||||||
|
ignore_date_modified,
|
||||||
|
ignore_signature,
|
||||||
|
jpeg_ext,
|
||||||
|
jpeg_quality,
|
||||||
|
keyword_template,
|
||||||
|
multiprocess,
|
||||||
|
original_suffix,
|
||||||
|
overwrite,
|
||||||
|
person_keyword,
|
||||||
|
photos,
|
||||||
|
post_command,
|
||||||
|
post_function,
|
||||||
|
preview_if_missing,
|
||||||
|
preview_suffix,
|
||||||
|
preview,
|
||||||
|
replace_keywords,
|
||||||
|
retry,
|
||||||
|
sidecar_drop_ext,
|
||||||
|
sidecar,
|
||||||
|
skip_original_if_edited,
|
||||||
|
strip,
|
||||||
|
touch_file,
|
||||||
|
update,
|
||||||
|
use_photokit,
|
||||||
|
use_photos_export,
|
||||||
|
verbose,
|
||||||
|
verbose_,
|
||||||
|
xattr_template,
|
||||||
|
_mp_uuids=None,
|
||||||
|
_mp_process_total=None,
|
||||||
|
_mp_process_num=None,
|
||||||
|
_mp_queue=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""export photos"""
|
||||||
|
|
||||||
|
# Need to pass the verbose_ method if for multiprocessing to work
|
||||||
|
_mp_verbose = None
|
||||||
|
if multiprocess:
|
||||||
|
_mp_queue.put(
|
||||||
|
[
|
||||||
|
_mp_process_num,
|
||||||
|
"START",
|
||||||
|
f"multiprocess mode: {_mp_process_num}, {_mp_process_total}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
def _mp_verbose(*args, **kwargs):
|
||||||
|
_mp_queue.put([_mp_process_num, "VERBOSE", args])
|
||||||
|
|
||||||
|
verbose_ = _mp_verbose
|
||||||
|
photosdb = osxphotos.PhotosDB(db, verbose=verbose_)
|
||||||
|
verbose_(f"_mp_uuids: {len(_mp_uuids)}")
|
||||||
|
photos = photosdb.photos_by_uuid(_mp_uuids)
|
||||||
|
verbose_(f"photos: {len(photos)}")
|
||||||
|
|
||||||
|
results = ExportResults()
|
||||||
|
num_photos = len(photos)
|
||||||
|
# though the command line option is current_name, internally all processing
|
||||||
|
# logic uses original_name which is the boolean inverse of current_name
|
||||||
|
# because the original code used --original-name as an option
|
||||||
|
original_name = not current_name
|
||||||
|
|
||||||
|
# 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
|
||||||
|
)
|
||||||
|
|
||||||
|
photo_num = 0
|
||||||
|
# send progress bar output to /dev/null if verbose or multiprocess to hide the progress bar
|
||||||
|
fp = open(os.devnull, "w") if verbose or multiprocess else None
|
||||||
|
with click.progressbar(photos, show_pos=True, file=fp) as bar:
|
||||||
|
for p in bar:
|
||||||
|
photo_num += 1
|
||||||
|
if multiprocess:
|
||||||
|
_mp_queue.put([_mp_process_num, "PROGRESS", photo_num, num_photos])
|
||||||
|
export_results = export_photo(
|
||||||
|
photo=p,
|
||||||
|
dest=dest,
|
||||||
|
album_keyword=album_keyword,
|
||||||
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
|
description_template=description_template,
|
||||||
|
directory=directory,
|
||||||
|
download_missing=download_missing,
|
||||||
|
dry_run=dry_run,
|
||||||
|
edited_suffix=edited_suffix,
|
||||||
|
exiftool_merge_keywords=exiftool_merge_keywords,
|
||||||
|
exiftool_merge_persons=exiftool_merge_persons,
|
||||||
|
exiftool_option=exiftool_option,
|
||||||
|
exiftool=exiftool,
|
||||||
|
export_as_hardlink=export_as_hardlink,
|
||||||
|
export_by_date=export_by_date,
|
||||||
|
export_db=export_db,
|
||||||
|
export_dir=dest,
|
||||||
|
export_edited=export_edited,
|
||||||
|
export_live=export_live,
|
||||||
|
export_preview=preview,
|
||||||
|
export_raw=export_raw,
|
||||||
|
filename_template=filename_template,
|
||||||
|
fileutil=fileutil,
|
||||||
|
ignore_date_modified=ignore_date_modified,
|
||||||
|
ignore_signature=ignore_signature,
|
||||||
|
jpeg_ext=jpeg_ext,
|
||||||
|
jpeg_quality=jpeg_quality,
|
||||||
|
keyword_template=keyword_template,
|
||||||
|
num_photos=num_photos,
|
||||||
|
original_name=original_name,
|
||||||
|
original_suffix=original_suffix,
|
||||||
|
overwrite=overwrite,
|
||||||
|
person_keyword=person_keyword,
|
||||||
|
photo_num=photo_num,
|
||||||
|
preview_if_missing=preview_if_missing,
|
||||||
|
preview_suffix=preview_suffix,
|
||||||
|
replace_keywords=replace_keywords,
|
||||||
|
retry=retry,
|
||||||
|
sidecar_drop_ext=sidecar_drop_ext,
|
||||||
|
sidecar=sidecar,
|
||||||
|
skip_original_if_edited=skip_original_if_edited,
|
||||||
|
strip=strip,
|
||||||
|
touch_file=touch_file,
|
||||||
|
update=update,
|
||||||
|
use_photokit=use_photokit,
|
||||||
|
use_photos_export=use_photos_export,
|
||||||
|
verbose_=verbose_,
|
||||||
|
verbose=verbose,
|
||||||
|
_mp_verbose=_mp_verbose,
|
||||||
|
)
|
||||||
|
|
||||||
|
if post_function:
|
||||||
|
for function in post_function:
|
||||||
|
# post function is tuple of (function, filename.py::function_name)
|
||||||
|
verbose_(f"Calling post-function {function[1]}")
|
||||||
|
if not dry_run:
|
||||||
|
try:
|
||||||
|
function[0](p, export_results, verbose_)
|
||||||
|
except Exception as e:
|
||||||
|
click.secho(
|
||||||
|
f"Error running post-function {function[1]}: {e}",
|
||||||
|
fg=CLI_COLOR_ERROR,
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
run_post_command(
|
||||||
|
photo=p,
|
||||||
|
post_command=post_command,
|
||||||
|
export_results=export_results,
|
||||||
|
export_dir=dest,
|
||||||
|
dry_run=dry_run,
|
||||||
|
exiftool_path=exiftool_path,
|
||||||
|
export_db=export_db,
|
||||||
|
)
|
||||||
|
|
||||||
|
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.secho(
|
||||||
|
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_export.name}: {e}",
|
||||||
|
fg=CLI_COLOR_ERROR,
|
||||||
|
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.secho(
|
||||||
|
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_skipped.name}: {e}",
|
||||||
|
fg=CLI_COLOR_ERROR,
|
||||||
|
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.secho(
|
||||||
|
f"Error adding photo {p.original_filename} ({p.uuid}) to album {album_missing.name}: {e}",
|
||||||
|
fg=CLI_COLOR_ERROR,
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
results += export_results
|
||||||
|
|
||||||
|
# all photo files (not including sidecars) that are part of this export set
|
||||||
|
# used below for applying Finder tags, etc.
|
||||||
|
photo_files = set(
|
||||||
|
export_results.exported
|
||||||
|
+ export_results.new
|
||||||
|
+ export_results.updated
|
||||||
|
+ export_results.exif_updated
|
||||||
|
+ export_results.converted_to_jpeg
|
||||||
|
+ export_results.skipped
|
||||||
|
)
|
||||||
|
|
||||||
|
if finder_tag_keywords or finder_tag_template:
|
||||||
|
tags_written, tags_skipped = write_finder_tags(
|
||||||
|
p,
|
||||||
|
photo_files,
|
||||||
|
keywords=finder_tag_keywords,
|
||||||
|
keyword_template=keyword_template,
|
||||||
|
album_keyword=album_keyword,
|
||||||
|
person_keyword=person_keyword,
|
||||||
|
exiftool_merge_keywords=exiftool_merge_keywords,
|
||||||
|
finder_tag_template=finder_tag_template,
|
||||||
|
strip=strip,
|
||||||
|
export_dir=dest,
|
||||||
|
)
|
||||||
|
results.xattr_written.extend(tags_written)
|
||||||
|
results.xattr_skipped.extend(tags_skipped)
|
||||||
|
|
||||||
|
if xattr_template:
|
||||||
|
xattr_written, xattr_skipped = write_extended_attributes(
|
||||||
|
p,
|
||||||
|
photo_files,
|
||||||
|
xattr_template,
|
||||||
|
strip=strip,
|
||||||
|
export_dir=dest,
|
||||||
|
)
|
||||||
|
results.xattr_written.extend(xattr_written)
|
||||||
|
results.xattr_skipped.extend(xattr_skipped)
|
||||||
|
|
||||||
|
if fp is not None:
|
||||||
|
fp.close()
|
||||||
|
|
||||||
|
if multiprocess:
|
||||||
|
_mp_queue.put([_mp_process_num, "DONE", results.asdict()])
|
||||||
|
else:
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def export_photo(
|
def export_photo(
|
||||||
photo=None,
|
photo=None,
|
||||||
dest=None,
|
dest=None,
|
||||||
verbose=None,
|
album_keyword=None,
|
||||||
export_by_date=None,
|
convert_to_jpeg=False,
|
||||||
sidecar=None,
|
description_template=None,
|
||||||
sidecar_drop_ext=False,
|
directory=None,
|
||||||
update=None,
|
|
||||||
ignore_signature=None,
|
|
||||||
export_as_hardlink=None,
|
|
||||||
overwrite=None,
|
|
||||||
export_edited=None,
|
|
||||||
skip_original_if_edited=None,
|
|
||||||
original_name=None,
|
|
||||||
export_live=None,
|
|
||||||
download_missing=None,
|
download_missing=None,
|
||||||
exiftool=None,
|
dry_run=None,
|
||||||
|
edited_suffix="_edited",
|
||||||
exiftool_merge_keywords=False,
|
exiftool_merge_keywords=False,
|
||||||
exiftool_merge_persons=False,
|
exiftool_merge_persons=False,
|
||||||
directory=None,
|
|
||||||
filename_template=None,
|
|
||||||
export_raw=None,
|
|
||||||
album_keyword=None,
|
|
||||||
person_keyword=None,
|
|
||||||
keyword_template=None,
|
|
||||||
description_template=None,
|
|
||||||
export_db=None,
|
|
||||||
fileutil=FileUtil,
|
|
||||||
dry_run=None,
|
|
||||||
touch_file=None,
|
|
||||||
edited_suffix="_edited",
|
|
||||||
original_suffix="",
|
|
||||||
use_photos_export=False,
|
|
||||||
convert_to_jpeg=False,
|
|
||||||
jpeg_quality=1.0,
|
|
||||||
ignore_date_modified=False,
|
|
||||||
use_photokit=False,
|
|
||||||
exiftool_option=None,
|
exiftool_option=None,
|
||||||
strip=False,
|
exiftool=None,
|
||||||
|
export_as_hardlink=None,
|
||||||
|
export_by_date=None,
|
||||||
|
export_db=None,
|
||||||
|
export_dir=None,
|
||||||
|
export_edited=None,
|
||||||
|
export_live=None,
|
||||||
|
export_preview=False,
|
||||||
|
export_raw=None,
|
||||||
|
filename_template=None,
|
||||||
|
fileutil=FileUtil,
|
||||||
|
ignore_date_modified=False,
|
||||||
|
ignore_signature=None,
|
||||||
jpeg_ext=None,
|
jpeg_ext=None,
|
||||||
|
jpeg_quality=1.0,
|
||||||
|
keyword_template=None,
|
||||||
|
num_photos=1,
|
||||||
|
original_name=None,
|
||||||
|
original_suffix="",
|
||||||
|
overwrite=None,
|
||||||
|
person_keyword=None,
|
||||||
|
photo_num=1,
|
||||||
|
preview_if_missing=False,
|
||||||
|
preview_suffix=None,
|
||||||
replace_keywords=False,
|
replace_keywords=False,
|
||||||
retry=0,
|
retry=0,
|
||||||
export_dir=None,
|
sidecar_drop_ext=False,
|
||||||
export_preview=False,
|
sidecar=None,
|
||||||
preview_suffix=None,
|
skip_original_if_edited=None,
|
||||||
preview_if_missing=False,
|
strip=False,
|
||||||
photo_num=1,
|
touch_file=None,
|
||||||
num_photos=1,
|
update=None,
|
||||||
|
use_photokit=False,
|
||||||
|
use_photos_export=False,
|
||||||
|
verbose_=None,
|
||||||
|
verbose=None,
|
||||||
|
_mp_verbose=None,
|
||||||
):
|
):
|
||||||
"""Helper function for export that does the actual export
|
"""Helper function for export that does the actual export
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
photo: PhotoInfo object
|
photo: PhotoInfo object
|
||||||
dest: destination path as string
|
dest: destination path as string
|
||||||
verbose: boolean; print verbose output
|
|
||||||
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
|
|
||||||
sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
|
|
||||||
sidecar_drop_ext: boolean; if True, drops photo extension from sidecar name
|
|
||||||
export_as_hardlink: boolean; hardlink files instead of copying them
|
|
||||||
overwrite: boolean; overwrite dest file if it already exists
|
|
||||||
original_name: boolean; use original filename instead of current filename
|
|
||||||
export_live: boolean; also export live video component if photo is a live photo
|
|
||||||
live video will have same name as photo but with .mov extension
|
|
||||||
download_missing: attempt download of missing iCloud photos
|
|
||||||
exiftool: use exiftool to write EXIF metadata directly to exported photo
|
|
||||||
directory: template used to determine output directory
|
|
||||||
filename_template: template use to determine output file
|
|
||||||
export_raw: boolean; if True exports raw image associate with the photo
|
|
||||||
export_edited: boolean; if True exports edited version of photo if there is one
|
|
||||||
skip_original_if_edited: boolean; if True does not export original if photo has been edited
|
|
||||||
album_keyword: boolean; if True, exports album names as keywords in metadata
|
album_keyword: boolean; if True, exports album names as keywords in metadata
|
||||||
person_keyword: boolean; if True, exports person names as keywords in metadata
|
|
||||||
keyword_template: list of strings; if provided use rendered template strings as keywords
|
|
||||||
description_template: string; optional template string that will be rendered for use as photo description
|
|
||||||
export_db: export database instance compatible with ExportDB_ABC
|
|
||||||
fileutil: file util class compatible with FileUtilABC
|
|
||||||
dry_run: boolean; if True, doesn't actually export or update any files
|
|
||||||
touch_file: boolean; sets file's modification time to match photo date
|
|
||||||
use_photos_export: boolean; if True forces the use of AppleScript to export even if photo not missing
|
|
||||||
convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg
|
convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg
|
||||||
jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
|
description_template: string; optional template string that will be rendered for use as photo description
|
||||||
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
directory: template used to determine output directory
|
||||||
exiftool_option: optional list flags (e.g. ["-m", "-F"]) to pass to exiftool
|
download_missing: attempt download of missing iCloud photos
|
||||||
|
dry_run: boolean; if True, doesn't actually export or update any files
|
||||||
exiftool_merge_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
exiftool_merge_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
||||||
exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
||||||
|
exiftool_option: optional list flags (e.g. ["-m", "-F"]) to pass to exiftool
|
||||||
|
exiftool: use exiftool to write EXIF metadata directly to exported photo
|
||||||
|
export_as_hardlink: boolean; hardlink files instead of copying them
|
||||||
|
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
|
||||||
|
export_db: export database instance compatible with ExportDB_ABC
|
||||||
|
export_dir: top-level export directory for {export_dir} template
|
||||||
|
export_edited: boolean; if True exports edited version of photo if there is one
|
||||||
|
export_live: boolean; also export live video component if photo is a live photo; live video will have same name as photo but with .mov extension
|
||||||
|
export_preview: export the preview image generated by Photos
|
||||||
|
export_raw: boolean; if True exports raw image associate with the photo
|
||||||
|
filename_template: template use to determine output file
|
||||||
|
fileutil: file util class compatible with FileUtilABC
|
||||||
|
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||||
jpeg_ext: if not None, specify the extension to use for all JPEG images on export
|
jpeg_ext: if not None, specify the extension to use for all JPEG images on export
|
||||||
|
jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
|
||||||
|
keyword_template: list of strings; if provided use rendered template strings as keywords
|
||||||
|
num_photos: int, total number of photos that will be exported
|
||||||
|
original_name: boolean; use original filename instead of current filename
|
||||||
|
overwrite: boolean; overwrite dest file if it already exists
|
||||||
|
person_keyword: boolean; if True, exports person names as keywords in metadata
|
||||||
|
photo_num: int, which number photo in total of num_photos is being exported
|
||||||
|
preview_if_missing: bool, export preview if original is missing
|
||||||
|
preview_suffix: str, template to use as suffix for preview images
|
||||||
replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords
|
replace_keywords: if True, --keyword-template replaces keywords instead of adding keywords
|
||||||
retry: retry up to retry # of times if there's an error
|
retry: retry up to retry # of times if there's an error
|
||||||
export_dir: top-level export directory for {export_dir} template
|
sidecar_drop_ext: boolean; if True, drops photo extension from sidecar name
|
||||||
export_preview: export the preview image generated by Photos
|
sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
|
||||||
preview_suffix: str, template to use as suffix for preview images
|
skip_original_if_edited: boolean; if True does not export original if photo has been edited
|
||||||
preview_if_missing: bool, export preview if original is missing
|
touch_file: boolean; sets file's modification time to match photo date
|
||||||
photo_num: int, which number photo in total of num_photos is being exported
|
use_photos_export: boolean; if True forces the use of AppleScript to export even if photo not missing
|
||||||
num_photos: int, total number of photos that will be exported
|
verbose_: Callable; verbose output function
|
||||||
|
verbose: bool; print verbose output
|
||||||
|
_mp_verbose: Callable; print verbose output for multiprocessing
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list of path(s) of exported photo or None if photo was missing
|
list of path(s) of exported photo or None if photo was missing
|
||||||
@@ -2683,8 +2895,7 @@ def export_photo(
|
|||||||
Raises:
|
Raises:
|
||||||
ValueError on invalid filename_template
|
ValueError on invalid filename_template
|
||||||
"""
|
"""
|
||||||
global VERBOSE
|
verbose_ = _mp_verbose or verbose_
|
||||||
VERBOSE = bool(verbose)
|
|
||||||
|
|
||||||
export_original = not (skip_original_if_edited and photo.hasadjustments)
|
export_original = not (skip_original_if_edited and photo.hasadjustments)
|
||||||
|
|
||||||
@@ -2801,11 +3012,12 @@ def export_photo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
results += export_photo_to_directory(
|
results += export_photo_to_directory(
|
||||||
|
photo=photo,
|
||||||
|
dest=dest,
|
||||||
album_keyword=album_keyword,
|
album_keyword=album_keyword,
|
||||||
convert_to_jpeg=convert_to_jpeg,
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
dest_path=dest_path,
|
dest_path=dest_path,
|
||||||
dest=dest,
|
|
||||||
download_missing=download_missing,
|
download_missing=download_missing,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
edited=False,
|
edited=False,
|
||||||
@@ -2830,7 +3042,6 @@ def export_photo(
|
|||||||
missing=missing_original,
|
missing=missing_original,
|
||||||
overwrite=overwrite,
|
overwrite=overwrite,
|
||||||
person_keyword=person_keyword,
|
person_keyword=person_keyword,
|
||||||
photo=photo,
|
|
||||||
preview_if_missing=preview_if_missing,
|
preview_if_missing=preview_if_missing,
|
||||||
preview_suffix=rendered_preview_suffix,
|
preview_suffix=rendered_preview_suffix,
|
||||||
replace_keywords=replace_keywords,
|
replace_keywords=replace_keywords,
|
||||||
@@ -2839,9 +3050,11 @@ def export_photo(
|
|||||||
sidecar_flags=sidecar_flags,
|
sidecar_flags=sidecar_flags,
|
||||||
touch_file=touch_file,
|
touch_file=touch_file,
|
||||||
update=update,
|
update=update,
|
||||||
use_photos_export=use_photos_export,
|
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
|
use_photos_export=use_photos_export,
|
||||||
|
verbose_=verbose_,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
|
_mp_verbose=_mp_verbose,
|
||||||
)
|
)
|
||||||
|
|
||||||
if export_edited and photo.hasadjustments:
|
if export_edited and photo.hasadjustments:
|
||||||
@@ -2913,11 +3126,12 @@ def export_photo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
results += export_photo_to_directory(
|
results += export_photo_to_directory(
|
||||||
|
photo=photo,
|
||||||
|
dest=dest,
|
||||||
album_keyword=album_keyword,
|
album_keyword=album_keyword,
|
||||||
convert_to_jpeg=convert_to_jpeg,
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
dest_path=dest_path,
|
dest_path=dest_path,
|
||||||
dest=dest,
|
|
||||||
download_missing=download_missing,
|
download_missing=download_missing,
|
||||||
dry_run=dry_run,
|
dry_run=dry_run,
|
||||||
edited=True,
|
edited=True,
|
||||||
@@ -2942,7 +3156,6 @@ def export_photo(
|
|||||||
missing=missing_edited,
|
missing=missing_edited,
|
||||||
overwrite=overwrite,
|
overwrite=overwrite,
|
||||||
person_keyword=person_keyword,
|
person_keyword=person_keyword,
|
||||||
photo=photo,
|
|
||||||
preview_if_missing=preview_if_missing,
|
preview_if_missing=preview_if_missing,
|
||||||
preview_suffix=rendered_preview_suffix,
|
preview_suffix=rendered_preview_suffix,
|
||||||
replace_keywords=replace_keywords,
|
replace_keywords=replace_keywords,
|
||||||
@@ -2951,9 +3164,11 @@ def export_photo(
|
|||||||
sidecar_flags=sidecar_flags if not export_original else 0,
|
sidecar_flags=sidecar_flags if not export_original else 0,
|
||||||
touch_file=touch_file,
|
touch_file=touch_file,
|
||||||
update=update,
|
update=update,
|
||||||
use_photos_export=use_photos_export,
|
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
|
use_photos_export=use_photos_export,
|
||||||
|
verbose_=verbose_,
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
|
_mp_verbose=_mp_verbose,
|
||||||
)
|
)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -3037,8 +3252,12 @@ def export_photo_to_directory(
|
|||||||
use_photos_export,
|
use_photos_export,
|
||||||
use_photokit,
|
use_photokit,
|
||||||
verbose,
|
verbose,
|
||||||
|
verbose_,
|
||||||
|
_mp_verbose=None,
|
||||||
):
|
):
|
||||||
"""Export photo to directory dest_path"""
|
"""Export photo to directory dest_path"""
|
||||||
|
# Need to pass the verbose_ method if for multiprocessing to work
|
||||||
|
verbose_ = _mp_verbose or verbose_
|
||||||
|
|
||||||
results = ExportResults()
|
results = ExportResults()
|
||||||
# TODO: can be updated to let export do all the missing logic
|
# TODO: can be updated to let export do all the missing logic
|
||||||
@@ -4112,11 +4331,13 @@ def _list_libraries(json_=False, error=True):
|
|||||||
default=False,
|
default=False,
|
||||||
help="Include filename of selected photos in output",
|
help="Include filename of selected photos in output",
|
||||||
)
|
)
|
||||||
def uuid(ctx, cli_obj, filename):
|
def uuid_(ctx, cli_obj, filename):
|
||||||
"""Print out unique IDs (UUID) of photos selected in Photos
|
"""Print out unique IDs (UUID) of photos selected in Photos
|
||||||
|
|
||||||
Prints outs UUIDs in form suitable for --uuid-from-file and --skip-uuid-from-file
|
Prints outs UUIDs in form suitable for --uuid-from-file and --skip-uuid-from-file
|
||||||
"""
|
"""
|
||||||
|
# Note: This is named uuid_ because multiprocessing complains about use of photo.uuid if
|
||||||
|
# this function is also called uuid. Something weird happenign with pickling.
|
||||||
for photo in photoscript.PhotosLibrary().selection:
|
for photo in photoscript.PhotosLibrary().selection:
|
||||||
if filename:
|
if filename:
|
||||||
print(f"# {photo.filename}")
|
print(f"# {photo.filename}")
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ class ExportDB_ABC(ABC):
|
|||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ExportDBNoOp(ExportDB_ABC):
|
class ExportDBNoOp(ExportDB_ABC):
|
||||||
"""An ExportDB with NoOp methods"""
|
"""An ExportDB with NoOp methods"""
|
||||||
@@ -192,6 +195,9 @@ class ExportDBNoOp(ExportDB_ABC):
|
|||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ExportDB(ExportDB_ABC):
|
class ExportDB(ExportDB_ABC):
|
||||||
"""Interface to sqlite3 database used to store state information for osxphotos export command"""
|
"""Interface to sqlite3 database used to store state information for osxphotos export command"""
|
||||||
@@ -212,7 +218,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
returns None if filename not found in database
|
returns None if filename not found in database
|
||||||
"""
|
"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -229,7 +235,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
"""set UUID of filename to uuid in the database"""
|
"""set UUID of filename to uuid in the database"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path))
|
filename = str(pathlib.Path(filename).relative_to(self._path))
|
||||||
filename_normalized = filename.lower()
|
filename_normalized = filename.lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -249,7 +255,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
if len(stats) != 3:
|
if len(stats) != 3:
|
||||||
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
||||||
|
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -267,7 +273,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
returns: tuple of (mode, size, mtime)
|
returns: tuple of (mode, size, mtime)
|
||||||
"""
|
"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -306,7 +312,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
if len(stats) != 3:
|
if len(stats) != 3:
|
||||||
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
||||||
|
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -324,7 +330,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
returns: tuple of (mode, size, mtime)
|
returns: tuple of (mode, size, mtime)
|
||||||
"""
|
"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -357,7 +363,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def get_info_for_uuid(self, uuid):
|
def get_info_for_uuid(self, uuid):
|
||||||
"""returns the info JSON struct for a UUID"""
|
"""returns the info JSON struct for a UUID"""
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT json_info FROM info WHERE uuid = ?", (uuid,))
|
c.execute("SELECT json_info FROM info WHERE uuid = ?", (uuid,))
|
||||||
@@ -371,7 +377,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def set_info_for_uuid(self, uuid, info):
|
def set_info_for_uuid(self, uuid, info):
|
||||||
"""sets the info JSON struct for a UUID"""
|
"""sets the info JSON struct for a UUID"""
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -385,7 +391,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
def get_exifdata_for_file(self, filename):
|
def get_exifdata_for_file(self, filename):
|
||||||
"""returns the exifdata JSON struct for a file"""
|
"""returns the exifdata JSON struct for a file"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -403,7 +409,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
def set_exifdata_for_file(self, filename, exifdata):
|
def set_exifdata_for_file(self, filename, exifdata):
|
||||||
"""sets the exifdata JSON struct for a file"""
|
"""sets the exifdata JSON struct for a file"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -417,7 +423,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
def get_sidecar_for_file(self, filename):
|
def get_sidecar_for_file(self, filename):
|
||||||
"""returns the sidecar data and signature for a file"""
|
"""returns the sidecar data and signature for a file"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -445,7 +451,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
|
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
|
||||||
"""sets the sidecar data and signature for a file"""
|
"""sets the sidecar data and signature for a file"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -458,7 +464,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def get_previous_uuids(self):
|
def get_previous_uuids(self):
|
||||||
"""returns list of UUIDs of previously exported photos found in export database"""
|
"""returns list of UUIDs of previously exported photos found in export database"""
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
previous_uuids = []
|
previous_uuids = []
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
@@ -471,7 +477,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def get_detected_text_for_uuid(self, uuid):
|
def get_detected_text_for_uuid(self, uuid):
|
||||||
"""Get the detected_text for a uuid"""
|
"""Get the detected_text for a uuid"""
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -488,7 +494,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def set_detected_text_for_uuid(self, uuid, text_json):
|
def set_detected_text_for_uuid(self, uuid, text_json):
|
||||||
"""Set the detected text for uuid"""
|
"""Set the detected text for uuid"""
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -516,7 +522,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
"""sets all the data for file and uuid at once; if any value is None, does not set it"""
|
"""sets all the data for file and uuid at once; if any value is None, does not set it"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path))
|
filename = str(pathlib.Path(filename).relative_to(self._path))
|
||||||
filename_normalized = filename.lower()
|
filename_normalized = filename.lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
# update files table (if needed);
|
# update files table (if needed);
|
||||||
@@ -572,16 +578,23 @@ class ExportDB(ExportDB_ABC):
|
|||||||
def close(self):
|
def close(self):
|
||||||
"""close the database connection"""
|
"""close the database connection"""
|
||||||
try:
|
try:
|
||||||
|
if self._conn:
|
||||||
self._conn.close()
|
self._conn.close()
|
||||||
|
self._conn = None
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
if self._conn is None:
|
||||||
|
self._conn = self._open_export_db(self._dbfile)
|
||||||
|
return self._conn
|
||||||
|
|
||||||
def _set_stat_for_file(self, table, filename, stats):
|
def _set_stat_for_file(self, table, filename, stats):
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
if len(stats) != 3:
|
if len(stats) != 3:
|
||||||
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
||||||
|
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
f"INSERT OR REPLACE INTO {table}(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
|
f"INSERT OR REPLACE INTO {table}(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
|
||||||
@@ -591,7 +604,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def _get_stat_for_file(self, table, filename):
|
def _get_stat_for_file(self, table, filename):
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
f"SELECT mode, size, mtime FROM {table} WHERE filepath_normalized = ?",
|
f"SELECT mode, size, mtime FROM {table} WHERE filepath_normalized = ?",
|
||||||
@@ -770,7 +783,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
cmd = sys.argv[0]
|
cmd = sys.argv[0]
|
||||||
args = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else ""
|
args = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else ""
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
|
|||||||
@@ -348,6 +348,34 @@ class ExportResults:
|
|||||||
+ ")"
|
+ ")"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def asdict(self):
|
||||||
|
"""Return dict instance of class"""
|
||||||
|
return {
|
||||||
|
"exported": self.exported,
|
||||||
|
"new": self.new,
|
||||||
|
"updated": self.updated,
|
||||||
|
"skipped": self.skipped,
|
||||||
|
"exif_updated": self.exif_updated,
|
||||||
|
"touched": self.touched,
|
||||||
|
"to_touch": self.to_touch,
|
||||||
|
"converted_to_jpeg": self.converted_to_jpeg,
|
||||||
|
"sidecar_json_written": self.sidecar_json_written,
|
||||||
|
"sidecar_json_skipped": self.sidecar_json_skipped,
|
||||||
|
"sidecar_exiftool_written": self.sidecar_exiftool_written,
|
||||||
|
"sidecar_exiftool_skipped": self.sidecar_exiftool_skipped,
|
||||||
|
"sidecar_xmp_written": self.sidecar_xmp_written,
|
||||||
|
"sidecar_xmp_skipped": self.sidecar_xmp_skipped,
|
||||||
|
"missing": self.missing,
|
||||||
|
"error": self.error,
|
||||||
|
"exiftool_warning": self.exiftool_warning,
|
||||||
|
"exiftool_error": self.exiftool_error,
|
||||||
|
"deleted_files": self.deleted_files,
|
||||||
|
"deleted_directories": self.deleted_directories,
|
||||||
|
"exported_album": self.exported_album,
|
||||||
|
"skipped_album": self.skipped_album,
|
||||||
|
"missing_album": self.missing_album,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PhotoExporter:
|
class PhotoExporter:
|
||||||
def __init__(self, photo: "PhotoInfo"):
|
def __init__(self, photo: "PhotoInfo"):
|
||||||
|
|||||||
8
setup.py
8
setup.py
@@ -74,12 +74,11 @@ setup(
|
|||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"Click>=8.0.1,<9.0",
|
|
||||||
"Mako>=1.1.4,<1.2.0",
|
|
||||||
"PyYAML>=5.4.1,<5.5.0",
|
|
||||||
"bitmath>=1.3.3.1,<1.4.0.0",
|
"bitmath>=1.3.3.1,<1.4.0.0",
|
||||||
"bpylist2==3.0.2",
|
"bpylist2==3.0.2",
|
||||||
|
"Click>=8.0.1,<9.0",
|
||||||
"dataclasses==0.7;python_version<'3.7'",
|
"dataclasses==0.7;python_version<'3.7'",
|
||||||
|
"Mako>=1.1.4,<1.2.0",
|
||||||
"more-itertools>=8.8.0,<9.0.0",
|
"more-itertools>=8.8.0,<9.0.0",
|
||||||
"objexplore>=1.5.5,<1.6.0",
|
"objexplore>=1.5.5,<1.6.0",
|
||||||
"osxmetadata>=0.99.34,<1.0.0",
|
"osxmetadata>=0.99.34,<1.0.0",
|
||||||
@@ -87,15 +86,16 @@ setup(
|
|||||||
"photoscript>=0.1.4,<0.2.0",
|
"photoscript>=0.1.4,<0.2.0",
|
||||||
"ptpython>=3.0.20,<4.0.0",
|
"ptpython>=3.0.20,<4.0.0",
|
||||||
"pyobjc-core>=7.3,<9.0",
|
"pyobjc-core>=7.3,<9.0",
|
||||||
"pyobjc-framework-AVFoundation>=7.3,<9.0",
|
|
||||||
"pyobjc-framework-AppleScriptKit>=7.3,<9.0",
|
"pyobjc-framework-AppleScriptKit>=7.3,<9.0",
|
||||||
"pyobjc-framework-AppleScriptObjC>=7.3,<9.0",
|
"pyobjc-framework-AppleScriptObjC>=7.3,<9.0",
|
||||||
|
"pyobjc-framework-AVFoundation>=7.3,<9.0",
|
||||||
"pyobjc-framework-Cocoa>=7.3,<9.0",
|
"pyobjc-framework-Cocoa>=7.3,<9.0",
|
||||||
"pyobjc-framework-CoreServices>=7.2,<9.0",
|
"pyobjc-framework-CoreServices>=7.2,<9.0",
|
||||||
"pyobjc-framework-Metal>=7.3,<9.0",
|
"pyobjc-framework-Metal>=7.3,<9.0",
|
||||||
"pyobjc-framework-Photos>=7.3,<9.0",
|
"pyobjc-framework-Photos>=7.3,<9.0",
|
||||||
"pyobjc-framework-Quartz>=7.3,<9.0",
|
"pyobjc-framework-Quartz>=7.3,<9.0",
|
||||||
"pyobjc-framework-Vision>=7.3,<9.0",
|
"pyobjc-framework-Vision>=7.3,<9.0",
|
||||||
|
"PyYAML>=5.4.1,<5.5.0",
|
||||||
"rich>=10.6.0,<=11.0.0",
|
"rich>=10.6.0,<=11.0.0",
|
||||||
"textx>=2.3.0,<3.0.0",
|
"textx>=2.3.0,<3.0.0",
|
||||||
"toml>=0.10.2,<0.11.0",
|
"toml>=0.10.2,<0.11.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user