Added --force-update, #621
This commit is contained in:
15
README.md
15
README.md
@@ -783,8 +783,15 @@ Options:
|
|||||||
folder.
|
folder.
|
||||||
--deleted-only Include only photos from the 'Recently
|
--deleted-only Include only photos from the 'Recently
|
||||||
Deleted' folder.
|
Deleted' folder.
|
||||||
--update Only export new or updated files. See notes
|
--update Only export new or updated files. See also
|
||||||
below on export and --update.
|
--force-update and notes below on export and
|
||||||
|
--update.
|
||||||
|
--force-update Only export new or updated files. Unlike
|
||||||
|
--update, --force-update will re-export photos
|
||||||
|
if their metadata has changed even if this
|
||||||
|
would not otherwise trigger an export. See
|
||||||
|
also --update and notes below on export and
|
||||||
|
--update.
|
||||||
--ignore-signature When used with '--update', ignores file
|
--ignore-signature When used with '--update', ignores file
|
||||||
signature when updating files. This is useful
|
signature when updating files. This is useful
|
||||||
if you have processed or edited exported
|
if you have processed or edited exported
|
||||||
@@ -1725,7 +1732,7 @@ Substitution Description
|
|||||||
{lf} A line feed: '\n', alias for {newline}
|
{lf} A line feed: '\n', alias for {newline}
|
||||||
{cr} A carriage return: '\r'
|
{cr} A carriage return: '\r'
|
||||||
{crlf} a carriage return + line feed: '\r\n'
|
{crlf} a carriage return + line feed: '\r\n'
|
||||||
{osxphotos_version} The osxphotos version, e.g. '0.45.8'
|
{osxphotos_version} The osxphotos version, e.g. '0.45.9'
|
||||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||||
|
|
||||||
The following substitutions may result in multiple values. Thus if specified for
|
The following substitutions may result in multiple values. Thus if specified for
|
||||||
@@ -3629,7 +3636,7 @@ The following template field substitutions are availabe for use the templating s
|
|||||||
|{lf}|A line feed: '\n', alias for {newline}|
|
|{lf}|A line feed: '\n', alias for {newline}|
|
||||||
|{cr}|A carriage return: '\r'|
|
|{cr}|A carriage return: '\r'|
|
||||||
|{crlf}|a carriage return + line feed: '\r\n'|
|
|{crlf}|a carriage return + line feed: '\r\n'|
|
||||||
|{osxphotos_version}|The osxphotos version, e.g. '0.45.8'|
|
|{osxphotos_version}|The osxphotos version, e.g. '0.45.9'|
|
||||||
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
||||||
|{album}|Album(s) photo is contained in|
|
|{album}|Album(s) photo is contained in|
|
||||||
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Sphinx build info version 1
|
# Sphinx build info version 1
|
||||||
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||||
config: bf43bf49b725c31ce72a8823e4f8012b
|
config: a320d2e66b198895ef0b12b1f5934727
|
||||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||||
|
|||||||
2
docs/_static/documentation_options.js
vendored
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
|||||||
var DOCUMENTATION_OPTIONS = {
|
var DOCUMENTATION_OPTIONS = {
|
||||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||||
VERSION: '0.45.8',
|
VERSION: '0.45.9',
|
||||||
LANGUAGE: 'None',
|
LANGUAGE: 'None',
|
||||||
COLLAPSE_INDEX: false,
|
COLLAPSE_INDEX: false,
|
||||||
BUILDER: 'html',
|
BUILDER: 'html',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>osxphotos command line interface (CLI) — osxphotos 0.45.8 documentation</title>
|
<title>osxphotos command line interface (CLI) — osxphotos 0.45.9 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Index — osxphotos 0.45.8 documentation</title>
|
<title>Index — osxphotos 0.45.9 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>Welcome to osxphotos’s documentation! — osxphotos 0.45.8 documentation</title>
|
<title>Welcome to osxphotos’s documentation! — osxphotos 0.45.9 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>osxphotos — osxphotos 0.45.8 documentation</title>
|
<title>osxphotos — osxphotos 0.45.9 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
|
||||||
|
|
||||||
<title>osxphotos package — osxphotos 0.45.8 documentation</title>
|
<title>osxphotos package — osxphotos 0.45.9 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Search — osxphotos 0.45.8 documentation</title>
|
<title>Search — osxphotos 0.45.9 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.45.8"
|
__version__ = "0.45.9"
|
||||||
|
|||||||
412
osxphotos/cli.py
412
osxphotos/cli.py
@@ -691,7 +691,15 @@ def cli(ctx, db, json_, debug):
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--update",
|
"--update",
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
help="Only export new or updated files. See notes below on export and --update.",
|
help="Only export new or updated files. "
|
||||||
|
"See also --force-update and notes below on export and --update.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--force-update",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only export new or updated files. Unlike --update, --force-update will re-export photos "
|
||||||
|
"if their metadata has changed even if this would not otherwise trigger an export. "
|
||||||
|
"See also --update and notes below on export and --update.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--ignore-signature",
|
"--ignore-signature",
|
||||||
@@ -1235,6 +1243,7 @@ def export(
|
|||||||
timestamp,
|
timestamp,
|
||||||
missing,
|
missing,
|
||||||
update,
|
update,
|
||||||
|
force_update,
|
||||||
ignore_signature,
|
ignore_signature,
|
||||||
only_new,
|
only_new,
|
||||||
dry_run,
|
dry_run,
|
||||||
@@ -1398,133 +1407,134 @@ def export(
|
|||||||
|
|
||||||
# re-set the local vars to the corresponding config value
|
# re-set the local vars to the corresponding config value
|
||||||
# this isn't elegant but avoids having to rewrite this function to use cfg.varname for every parameter
|
# this isn't elegant but avoids having to rewrite this function to use cfg.varname for every parameter
|
||||||
db = cfg.db
|
add_exported_to_album = cfg.add_exported_to_album
|
||||||
photos_library = cfg.photos_library
|
add_missing_to_album = cfg.add_missing_to_album
|
||||||
keyword = cfg.keyword
|
add_skipped_to_album = cfg.add_skipped_to_album
|
||||||
person = cfg.person
|
|
||||||
album = cfg.album
|
album = cfg.album
|
||||||
folder = cfg.folder
|
|
||||||
name = cfg.name
|
|
||||||
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
|
|
||||||
from_time = cfg.from_time
|
|
||||||
to_time = cfg.to_time
|
|
||||||
verbose = cfg.verbose
|
|
||||||
missing = cfg.missing
|
|
||||||
update = cfg.update
|
|
||||||
ignore_signature = cfg.ignore_signature
|
|
||||||
dry_run = cfg.dry_run
|
|
||||||
export_as_hardlink = cfg.export_as_hardlink
|
|
||||||
touch_file = cfg.touch_file
|
|
||||||
overwrite = cfg.overwrite
|
|
||||||
retry = cfg.retry
|
|
||||||
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
|
|
||||||
skip_uuid = cfg.skip_uuid
|
|
||||||
skip_uuid_from_file = cfg.skip_uuid_from_file
|
|
||||||
person_keyword = cfg.person_keyword
|
|
||||||
album_keyword = cfg.album_keyword
|
album_keyword = cfg.album_keyword
|
||||||
keyword_template = cfg.keyword_template
|
beta = cfg.beta
|
||||||
replace_keywords = cfg.replace_keywords
|
|
||||||
description_template = cfg.description_template
|
|
||||||
finder_tag_template = cfg.finder_tag_template
|
|
||||||
finder_tag_keywords = cfg.finder_tag_keywords
|
|
||||||
xattr_template = cfg.xattr_template
|
|
||||||
current_name = cfg.current_name
|
|
||||||
convert_to_jpeg = cfg.convert_to_jpeg
|
|
||||||
jpeg_quality = cfg.jpeg_quality
|
|
||||||
sidecar = cfg.sidecar
|
|
||||||
sidecar_drop_ext = cfg.sidecar_drop_ext
|
|
||||||
only_photos = cfg.only_photos
|
|
||||||
only_movies = cfg.only_movies
|
|
||||||
burst = cfg.burst
|
burst = cfg.burst
|
||||||
not_burst = cfg.not_burst
|
cleanup = cfg.cleanup
|
||||||
live = cfg.live
|
convert_to_jpeg = cfg.convert_to_jpeg
|
||||||
not_live = cfg.not_live
|
current_name = cfg.current_name
|
||||||
download_missing = cfg.download_missing
|
db = cfg.db
|
||||||
exiftool = cfg.exiftool
|
|
||||||
exiftool_path = cfg.exiftool_path
|
|
||||||
exiftool_option = cfg.exiftool_option
|
|
||||||
exiftool_merge_keywords = cfg.exiftool_merge_keywords
|
|
||||||
exiftool_merge_persons = cfg.exiftool_merge_persons
|
|
||||||
ignore_date_modified = cfg.ignore_date_modified
|
|
||||||
portrait = cfg.portrait
|
|
||||||
not_portrait = cfg.not_portrait
|
|
||||||
screenshot = cfg.screenshot
|
|
||||||
not_screenshot = cfg.not_screenshot
|
|
||||||
slow_mo = cfg.slow_mo
|
|
||||||
not_slow_mo = cfg.not_slow_mo
|
|
||||||
time_lapse = cfg.time_lapse
|
|
||||||
not_time_lapse = cfg.not_time_lapse
|
|
||||||
hdr = cfg.hdr
|
|
||||||
not_hdr = cfg.not_hdr
|
|
||||||
selfie = cfg.selfie
|
|
||||||
not_selfie = cfg.not_selfie
|
|
||||||
panorama = cfg.panorama
|
|
||||||
not_panorama = cfg.not_panorama
|
|
||||||
has_raw = cfg.has_raw
|
|
||||||
directory = cfg.directory
|
|
||||||
filename_template = cfg.filename_template
|
|
||||||
jpeg_ext = cfg.jpeg_ext
|
|
||||||
strip = cfg.strip
|
|
||||||
edited_suffix = cfg.edited_suffix
|
|
||||||
original_suffix = cfg.original_suffix
|
|
||||||
place = cfg.place
|
|
||||||
no_place = cfg.no_place
|
|
||||||
location = cfg.location
|
|
||||||
no_location = cfg.no_location
|
|
||||||
has_comment = cfg.has_comment
|
|
||||||
no_comment = cfg.no_comment
|
|
||||||
has_likes = cfg.has_likes
|
|
||||||
no_likes = cfg.no_likes
|
|
||||||
label = cfg.label
|
|
||||||
deleted = cfg.deleted
|
deleted = cfg.deleted
|
||||||
deleted_only = cfg.deleted_only
|
deleted_only = cfg.deleted_only
|
||||||
use_photos_export = cfg.use_photos_export
|
description = cfg.description
|
||||||
use_photokit = cfg.use_photokit
|
description_template = cfg.description_template
|
||||||
report = cfg.report
|
directory = cfg.directory
|
||||||
cleanup = cfg.cleanup
|
download_missing = cfg.download_missing
|
||||||
add_exported_to_album = cfg.add_exported_to_album
|
dry_run = cfg.dry_run
|
||||||
add_skipped_to_album = cfg.add_skipped_to_album
|
|
||||||
add_missing_to_album = cfg.add_missing_to_album
|
|
||||||
exportdb = cfg.exportdb
|
|
||||||
beta = cfg.beta
|
|
||||||
only_new = cfg.only_new
|
|
||||||
in_album = cfg.in_album
|
|
||||||
not_in_album = cfg.not_in_album
|
|
||||||
min_size = cfg.min_size
|
|
||||||
max_size = cfg.max_size
|
|
||||||
regex = cfg.regex
|
|
||||||
selected = cfg.selected
|
|
||||||
exif = cfg.exif
|
|
||||||
query_eval = cfg.query_eval
|
|
||||||
query_function = cfg.query_function
|
|
||||||
duplicate = cfg.duplicate
|
duplicate = cfg.duplicate
|
||||||
|
edited = cfg.edited
|
||||||
|
edited_suffix = cfg.edited_suffix
|
||||||
|
exif = cfg.exif
|
||||||
|
exiftool = cfg.exiftool
|
||||||
|
exiftool_merge_keywords = cfg.exiftool_merge_keywords
|
||||||
|
exiftool_merge_persons = cfg.exiftool_merge_persons
|
||||||
|
exiftool_option = cfg.exiftool_option
|
||||||
|
exiftool_path = cfg.exiftool_path
|
||||||
|
export_as_hardlink = cfg.export_as_hardlink
|
||||||
|
export_by_date = cfg.export_by_date
|
||||||
|
exportdb = cfg.exportdb
|
||||||
|
external_edit = cfg.external_edit
|
||||||
|
favorite = cfg.favorite
|
||||||
|
filename_template = cfg.filename_template
|
||||||
|
finder_tag_keywords = cfg.finder_tag_keywords
|
||||||
|
finder_tag_template = cfg.finder_tag_template
|
||||||
|
folder = cfg.folder
|
||||||
|
force_update = cfg.force_update
|
||||||
|
from_date = cfg.from_date
|
||||||
|
from_time = cfg.from_time
|
||||||
|
has_comment = cfg.has_comment
|
||||||
|
has_likes = cfg.has_likes
|
||||||
|
has_raw = cfg.has_raw
|
||||||
|
hdr = cfg.hdr
|
||||||
|
hidden = cfg.hidden
|
||||||
|
ignore_case = cfg.ignore_case
|
||||||
|
ignore_date_modified = cfg.ignore_date_modified
|
||||||
|
ignore_signature = cfg.ignore_signature
|
||||||
|
in_album = cfg.in_album
|
||||||
|
jpeg_ext = cfg.jpeg_ext
|
||||||
|
jpeg_quality = cfg.jpeg_quality
|
||||||
|
keyword = cfg.keyword
|
||||||
|
keyword_template = cfg.keyword_template
|
||||||
|
label = cfg.label
|
||||||
|
live = cfg.live
|
||||||
|
location = cfg.location
|
||||||
|
max_size = cfg.max_size
|
||||||
|
min_size = cfg.min_size
|
||||||
|
missing = cfg.missing
|
||||||
|
name = cfg.name
|
||||||
|
no_comment = cfg.no_comment
|
||||||
|
no_description = cfg.no_description
|
||||||
|
no_likes = cfg.no_likes
|
||||||
|
no_location = cfg.no_location
|
||||||
|
no_place = cfg.no_place
|
||||||
|
no_title = cfg.no_title
|
||||||
|
not_burst = cfg.not_burst
|
||||||
|
not_favorite = cfg.not_favorite
|
||||||
|
not_hdr = cfg.not_hdr
|
||||||
|
not_hidden = cfg.not_hidden
|
||||||
|
not_in_album = cfg.not_in_album
|
||||||
|
not_live = cfg.not_live
|
||||||
|
not_panorama = cfg.not_panorama
|
||||||
|
not_portrait = cfg.not_portrait
|
||||||
|
not_screenshot = cfg.not_screenshot
|
||||||
|
not_selfie = cfg.not_selfie
|
||||||
|
not_shared = cfg.not_shared
|
||||||
|
not_slow_mo = cfg.not_slow_mo
|
||||||
|
not_time_lapse = cfg.not_time_lapse
|
||||||
|
only_movies = cfg.only_movies
|
||||||
|
only_new = cfg.only_new
|
||||||
|
only_photos = cfg.only_photos
|
||||||
|
original_suffix = cfg.original_suffix
|
||||||
|
overwrite = cfg.overwrite
|
||||||
|
panorama = cfg.panorama
|
||||||
|
person = cfg.person
|
||||||
|
person_keyword = cfg.person_keyword
|
||||||
|
photos_library = cfg.photos_library
|
||||||
|
place = cfg.place
|
||||||
|
portrait = cfg.portrait
|
||||||
post_command = cfg.post_command
|
post_command = cfg.post_command
|
||||||
post_function = cfg.post_function
|
post_function = cfg.post_function
|
||||||
preview = cfg.preview
|
preview = cfg.preview
|
||||||
preview_suffix = cfg.preview_suffix
|
|
||||||
preview_if_missing = cfg.preview_if_missing
|
preview_if_missing = cfg.preview_if_missing
|
||||||
|
preview_suffix = cfg.preview_suffix
|
||||||
|
query_eval = cfg.query_eval
|
||||||
|
query_function = cfg.query_function
|
||||||
|
regex = cfg.regex
|
||||||
|
replace_keywords = cfg.replace_keywords
|
||||||
|
report = cfg.report
|
||||||
|
retry = cfg.retry
|
||||||
|
screenshot = cfg.screenshot
|
||||||
|
selected = cfg.selected
|
||||||
|
selfie = cfg.selfie
|
||||||
|
shared = cfg.shared
|
||||||
|
sidecar = cfg.sidecar
|
||||||
|
sidecar_drop_ext = cfg.sidecar_drop_ext
|
||||||
|
skip_bursts = cfg.skip_bursts
|
||||||
|
skip_edited = cfg.skip_edited
|
||||||
|
skip_live = cfg.skip_live
|
||||||
|
skip_original_if_edited = cfg.skip_original_if_edited
|
||||||
|
skip_raw = cfg.skip_raw
|
||||||
|
skip_uuid = cfg.skip_uuid
|
||||||
|
skip_uuid_from_file = cfg.skip_uuid_from_file
|
||||||
|
slow_mo = cfg.slow_mo
|
||||||
|
strip = cfg.strip
|
||||||
|
time_lapse = cfg.time_lapse
|
||||||
|
title = cfg.title
|
||||||
|
to_date = cfg.to_date
|
||||||
|
to_time = cfg.to_time
|
||||||
|
touch_file = cfg.touch_file
|
||||||
|
update = cfg.update
|
||||||
|
use_photokit = cfg.use_photokit
|
||||||
|
use_photos_export = cfg.use_photos_export
|
||||||
|
uti = cfg.uti
|
||||||
|
uuid = cfg.uuid
|
||||||
|
uuid_from_file = cfg.uuid_from_file
|
||||||
|
verbose = cfg.verbose
|
||||||
|
xattr_template = cfg.xattr_template
|
||||||
|
|
||||||
# config file might have changed verbose
|
# config file might have changed verbose
|
||||||
VERBOSE = bool(verbose)
|
VERBOSE = bool(verbose)
|
||||||
@@ -1564,8 +1574,8 @@ def export(
|
|||||||
dependent_options = [
|
dependent_options = [
|
||||||
("missing", ("download_missing", "use_photos_export")),
|
("missing", ("download_missing", "use_photos_export")),
|
||||||
("jpeg_quality", ("convert_to_jpeg")),
|
("jpeg_quality", ("convert_to_jpeg")),
|
||||||
("ignore_signature", ("update")),
|
("ignore_signature", ("update", "force_update")),
|
||||||
("only_new", ("update")),
|
("only_new", ("update", "force_update")),
|
||||||
("exiftool_option", ("exiftool")),
|
("exiftool_option", ("exiftool")),
|
||||||
("exiftool_merge_keywords", ("exiftool", "sidecar")),
|
("exiftool_merge_keywords", ("exiftool", "sidecar")),
|
||||||
("exiftool_merge_persons", ("exiftool", "sidecar")),
|
("exiftool_merge_persons", ("exiftool", "sidecar")),
|
||||||
@@ -1905,51 +1915,52 @@ def export(
|
|||||||
export_results = export_photo(
|
export_results = export_photo(
|
||||||
photo=p,
|
photo=p,
|
||||||
dest=dest,
|
dest=dest,
|
||||||
verbose=verbose,
|
album_keyword=album_keyword,
|
||||||
export_by_date=export_by_date,
|
convert_to_jpeg=convert_to_jpeg,
|
||||||
sidecar=sidecar,
|
description_template=description_template,
|
||||||
sidecar_drop_ext=sidecar_drop_ext,
|
directory=directory,
|
||||||
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,
|
download_missing=download_missing,
|
||||||
exiftool=exiftool,
|
dry_run=dry_run,
|
||||||
|
edited_suffix=edited_suffix,
|
||||||
exiftool_merge_keywords=exiftool_merge_keywords,
|
exiftool_merge_keywords=exiftool_merge_keywords,
|
||||||
exiftool_merge_persons=exiftool_merge_persons,
|
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,
|
exiftool_option=exiftool_option,
|
||||||
strip=strip,
|
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,
|
||||||
|
force_update=force_update,
|
||||||
|
ignore_date_modified=ignore_date_modified,
|
||||||
|
ignore_signature=ignore_signature,
|
||||||
jpeg_ext=jpeg_ext,
|
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,
|
replace_keywords=replace_keywords,
|
||||||
retry=retry,
|
retry=retry,
|
||||||
export_dir=dest,
|
sidecar_drop_ext=sidecar_drop_ext,
|
||||||
export_preview=preview,
|
sidecar=sidecar,
|
||||||
preview_suffix=preview_suffix,
|
skip_original_if_edited=skip_original_if_edited,
|
||||||
preview_if_missing=preview_if_missing,
|
strip=strip,
|
||||||
photo_num=photo_num,
|
touch_file=touch_file,
|
||||||
num_photos=num_photos,
|
update=update,
|
||||||
|
use_photokit=use_photokit,
|
||||||
|
use_photos_export=use_photos_export,
|
||||||
|
verbose=verbose,
|
||||||
)
|
)
|
||||||
|
|
||||||
if post_function:
|
if post_function:
|
||||||
@@ -2062,7 +2073,7 @@ def export(
|
|||||||
fp.close()
|
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 or force_update:
|
||||||
summary = (
|
summary = (
|
||||||
f"Processed: {len(photos)} {photo_str_total}, "
|
f"Processed: {len(photos)} {photo_str_total}, "
|
||||||
f"exported: {len(results.new)}, "
|
f"exported: {len(results.new)}, "
|
||||||
@@ -2592,6 +2603,7 @@ def export_photo(
|
|||||||
sidecar=None,
|
sidecar=None,
|
||||||
sidecar_drop_ext=False,
|
sidecar_drop_ext=False,
|
||||||
update=None,
|
update=None,
|
||||||
|
force_update=None,
|
||||||
ignore_signature=None,
|
ignore_signature=None,
|
||||||
export_as_hardlink=None,
|
export_as_hardlink=None,
|
||||||
overwrite=None,
|
overwrite=None,
|
||||||
@@ -2638,47 +2650,47 @@ 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
|
album_keyword: bool; if True, exports album names as keywords in metadata
|
||||||
export_by_date: boolean; create export folder in form dest/YYYY/MM/DD
|
convert_to_jpeg: bool; if True, converts non-jpeg images to jpeg
|
||||||
sidecar: list zero, 1 or 2 of ["json","xmp"] of sidecar variety to export
|
description_template: str; optional template string that will be rendered for use as photo description
|
||||||
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
|
directory: template used to determine output directory
|
||||||
filename_template: template use to determine output file
|
download_missing: attempt download of missing iCloud photos
|
||||||
export_raw: boolean; if True exports raw image associate with the photo
|
dry_run: bool; if True, doesn't actually export or update any files
|
||||||
export_edited: boolean; if True exports edited version of photo if there is one
|
exiftool_merge_keywords: bool; if True, merged keywords found in file's exif data (requires exiftool)
|
||||||
skip_original_if_edited: boolean; if True does not export original if photo has been edited
|
exiftool_merge_persons: bool; if True, merged persons found in file's exif data (requires exiftool)
|
||||||
album_keyword: boolean; if True, exports album names as keywords in metadata
|
|
||||||
person_keyword: boolean; if True, exports person names as keywords in metadata
|
|
||||||
keyword_template: list of strings; if provided use rendered template strings as keywords
|
|
||||||
description_template: string; optional template string that will be rendered for use as photo description
|
|
||||||
export_db: export database instance compatible with ExportDB_ABC
|
|
||||||
fileutil: file util class compatible with FileUtilABC
|
|
||||||
dry_run: boolean; if True, doesn't actually export or update any files
|
|
||||||
touch_file: boolean; sets file's modification time to match photo date
|
|
||||||
use_photos_export: boolean; if True forces the use of AppleScript to export even if photo not missing
|
|
||||||
convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg
|
|
||||||
jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
|
|
||||||
ignore_date_modified: if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
|
||||||
exiftool_option: optional list flags (e.g. ["-m", "-F"]) to pass to exiftool
|
exiftool_option: optional list flags (e.g. ["-m", "-F"]) to pass to exiftool
|
||||||
exiftool_merge_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
exiftool: bool; use exiftool to write EXIF metadata directly to exported photo
|
||||||
exiftool_merge_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
export_as_hardlink: bool; hardlink files instead of copying them
|
||||||
|
export_by_date: bool; 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: bool; if True exports edited version of photo if there is one
|
||||||
|
export_live: bool; 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: bool; if True exports raw image associate with the photo
|
||||||
|
filename_template: template use to determine output file
|
||||||
|
fileutil: file util class compatible with FileUtilABC
|
||||||
|
force_update: bool, only export updated photos but trigger export even if only metadata has changed
|
||||||
|
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: bool; use original filename instead of current filename
|
||||||
|
overwrite: bool; overwrite dest file if it already exists
|
||||||
|
person_keyword: bool; 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: bool; 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: bool; if True does not export original if photo has been edited
|
||||||
preview_if_missing: bool, export preview if original is missing
|
touch_file: bool; sets file's modification time to match photo date
|
||||||
photo_num: int, which number photo in total of num_photos is being exported
|
update: bool, only export updated photos
|
||||||
num_photos: int, total number of photos that will be exported
|
use_photos_export: bool; if True forces the use of AppleScript to export even if photo not missing
|
||||||
|
verbose: bool; print verbose output
|
||||||
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
|
||||||
|
|
||||||
@@ -2824,6 +2836,7 @@ def export_photo(
|
|||||||
export_raw=export_raw,
|
export_raw=export_raw,
|
||||||
filename=original_filename,
|
filename=original_filename,
|
||||||
fileutil=fileutil,
|
fileutil=fileutil,
|
||||||
|
force_update=force_update,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
ignore_signature=ignore_signature,
|
ignore_signature=ignore_signature,
|
||||||
jpeg_ext=jpeg_ext,
|
jpeg_ext=jpeg_ext,
|
||||||
@@ -2936,6 +2949,7 @@ def export_photo(
|
|||||||
export_raw=not export_original and export_raw,
|
export_raw=not export_original and export_raw,
|
||||||
filename=edited_filename,
|
filename=edited_filename,
|
||||||
fileutil=fileutil,
|
fileutil=fileutil,
|
||||||
|
force_update=force_update,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
ignore_signature=ignore_signature,
|
ignore_signature=ignore_signature,
|
||||||
jpeg_ext=jpeg_ext,
|
jpeg_ext=jpeg_ext,
|
||||||
@@ -3019,6 +3033,7 @@ def export_photo_to_directory(
|
|||||||
export_raw,
|
export_raw,
|
||||||
filename,
|
filename,
|
||||||
fileutil,
|
fileutil,
|
||||||
|
force_update,
|
||||||
ignore_date_modified,
|
ignore_date_modified,
|
||||||
ignore_signature,
|
ignore_signature,
|
||||||
jpeg_ext,
|
jpeg_ext,
|
||||||
@@ -3077,6 +3092,7 @@ def export_photo_to_directory(
|
|||||||
export_as_hardlink=export_as_hardlink,
|
export_as_hardlink=export_as_hardlink,
|
||||||
export_db=export_db,
|
export_db=export_db,
|
||||||
fileutil=fileutil,
|
fileutil=fileutil,
|
||||||
|
force_update=force_update,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
ignore_signature=ignore_signature,
|
ignore_signature=ignore_signature,
|
||||||
jpeg_ext=jpeg_ext,
|
jpeg_ext=jpeg_ext,
|
||||||
@@ -3179,7 +3195,7 @@ def get_filenames_from_template(
|
|||||||
Args:
|
Args:
|
||||||
photo: a PhotoInfo instance
|
photo: a PhotoInfo instance
|
||||||
filename_template: a PhotoTemplate template string, may be None
|
filename_template: a PhotoTemplate template string, may be None
|
||||||
original_name: boolean; if True, use photo's original filename instead of current filename
|
original_name: bool; if True, use photo's original filename instead of current filename
|
||||||
dest_path: the path the photo will be exported to
|
dest_path: the path the photo will be exported to
|
||||||
strip: if True, strips leading/trailing white space from resulting template
|
strip: if True, strips leading/trailing white space from resulting template
|
||||||
edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version
|
edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version
|
||||||
@@ -3240,9 +3256,9 @@ def get_dirnames_from_template(
|
|||||||
Args:
|
Args:
|
||||||
photo: a PhotoInstance object
|
photo: a PhotoInstance object
|
||||||
directory: a PhotoTemplate template string, may be None
|
directory: a PhotoTemplate template string, may be None
|
||||||
export_by_date: boolean; if True, creates output directories in form YYYY-MM-DD
|
export_by_date: bool; if True, creates output directories in form YYYY-MM-DD
|
||||||
dest: top-level destination directory
|
dest: top-level destination directory
|
||||||
dry_run: boolean; if True, runs in dry-run mode and does not create output directories
|
dry_run: bool; if True, runs in dry-run mode and does not create output directories
|
||||||
strip: if True, strips leading/trailing white space from resulting template
|
strip: if True, strips leading/trailing white space from resulting template
|
||||||
edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version
|
edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ from .utils import normalize_fs_path
|
|||||||
|
|
||||||
__all__ = ["ExportDB_ABC", "ExportDBNoOp", "ExportDB", "ExportDBInMemory"]
|
__all__ = ["ExportDB_ABC", "ExportDBNoOp", "ExportDB", "ExportDBInMemory"]
|
||||||
|
|
||||||
OSXPHOTOS_EXPORTDB_VERSION = "4.3"
|
OSXPHOTOS_EXPORTDB_VERSION = "5.0"
|
||||||
OSXPHOTOS_EXPORTDB_VERSION_MIGRATE_FILEPATH = "4.3"
|
OSXPHOTOS_EXPORTDB_VERSION_MIGRATE_FILEPATH = "4.3"
|
||||||
|
OSXPHOTOS_EXPORTDB_VERSION_MIGRATE_TABLES = "4.3"
|
||||||
|
|
||||||
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
|
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
|
||||||
|
|
||||||
@@ -103,6 +104,14 @@ class ExportDB_ABC(ABC):
|
|||||||
def set_detected_text_for_uuid(self, uuid, json_text):
|
def set_detected_text_for_uuid(self, uuid, json_text):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_metadata_for_file(self, filename, metadata):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_metadata_for_file(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def set_data(
|
def set_data(
|
||||||
self,
|
self,
|
||||||
@@ -114,6 +123,7 @@ class ExportDB_ABC(ABC):
|
|||||||
edited_stat=None,
|
edited_stat=None,
|
||||||
info_json=None,
|
info_json=None,
|
||||||
exif_json=None,
|
exif_json=None,
|
||||||
|
metadata=None,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -183,6 +193,12 @@ class ExportDBNoOp(ExportDB_ABC):
|
|||||||
def set_detected_text_for_uuid(self, uuid, json_text):
|
def set_detected_text_for_uuid(self, uuid, json_text):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def set_metadata_for_file(self, filename, metadata):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_metadata_for_file(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
def set_data(
|
def set_data(
|
||||||
self,
|
self,
|
||||||
filename,
|
filename,
|
||||||
@@ -193,6 +209,7 @@ class ExportDBNoOp(ExportDB_ABC):
|
|||||||
edited_stat=None,
|
edited_stat=None,
|
||||||
info_json=None,
|
info_json=None,
|
||||||
exif_json=None,
|
exif_json=None,
|
||||||
|
metadata=None,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -507,6 +524,39 @@ class ExportDB(ExportDB_ABC):
|
|||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
|
|
||||||
|
def set_metadata_for_file(self, filename, metadata):
|
||||||
|
"""set metadata of filename in the database"""
|
||||||
|
filename = str(pathlib.Path(filename).relative_to(self._path))
|
||||||
|
filename_normalized = self._normalize_filepath(filename)
|
||||||
|
conn = self._conn
|
||||||
|
try:
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"UPDATE files SET metadata = ? WHERE filepath_normalized = ?;",
|
||||||
|
(metadata, filename_normalized),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Error as e:
|
||||||
|
logging.warning(e)
|
||||||
|
|
||||||
|
def get_metadata_for_file(self, filename):
|
||||||
|
"""get metadata value for file"""
|
||||||
|
filename = self._normalize_filepath_relative(filename)
|
||||||
|
conn = self._conn
|
||||||
|
try:
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"SELECT metadata FROM files WHERE filepath_normalized = ?",
|
||||||
|
(filename,),
|
||||||
|
)
|
||||||
|
results = c.fetchone()
|
||||||
|
metadata = results[0] if results else None
|
||||||
|
except Error as e:
|
||||||
|
logging.warning(e)
|
||||||
|
metadata = None
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
|
||||||
def set_data(
|
def set_data(
|
||||||
self,
|
self,
|
||||||
filename,
|
filename,
|
||||||
@@ -517,6 +567,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
edited_stat=None,
|
edited_stat=None,
|
||||||
info_json=None,
|
info_json=None,
|
||||||
exif_json=None,
|
exif_json=None,
|
||||||
|
metadata=None,
|
||||||
):
|
):
|
||||||
"""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))
|
||||||
@@ -570,6 +621,15 @@ class ExportDB(ExportDB_ABC):
|
|||||||
"INSERT OR REPLACE INTO exifdata(filepath_normalized, json_exifdata) VALUES (?, ?);",
|
"INSERT OR REPLACE INTO exifdata(filepath_normalized, json_exifdata) VALUES (?, ?);",
|
||||||
(filename_normalized, exif_json),
|
(filename_normalized, exif_json),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if metadata is not None:
|
||||||
|
c.execute(
|
||||||
|
"UPDATE files "
|
||||||
|
+ "SET metadata = ? "
|
||||||
|
+ "WHERE filepath_normalized = ?;",
|
||||||
|
(metadata, filename_normalized),
|
||||||
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
@@ -622,7 +682,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
conn = self._get_db_connection(dbfile)
|
conn = self._get_db_connection(dbfile)
|
||||||
if not conn:
|
if not conn:
|
||||||
raise Exception("Error getting connection to database {dbfile}")
|
raise Exception("Error getting connection to database {dbfile}")
|
||||||
self._create_db_tables(conn)
|
self._create_or_migrate_db_tables(conn)
|
||||||
self.was_created = True
|
self.was_created = True
|
||||||
self.was_upgraded = ()
|
self.was_upgraded = ()
|
||||||
else:
|
else:
|
||||||
@@ -630,9 +690,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
self.was_created = False
|
self.was_created = False
|
||||||
version_info = self._get_database_version(conn)
|
version_info = self._get_database_version(conn)
|
||||||
if version_info[1] < OSXPHOTOS_EXPORTDB_VERSION:
|
if version_info[1] < OSXPHOTOS_EXPORTDB_VERSION:
|
||||||
self._create_db_tables(conn)
|
self._create_or_migrate_db_tables(conn)
|
||||||
if version_info[1] < OSXPHOTOS_EXPORTDB_VERSION_MIGRATE_FILEPATH:
|
|
||||||
self._migrate_normalized_filepath(conn)
|
|
||||||
self.was_upgraded = (version_info[1], OSXPHOTOS_EXPORTDB_VERSION)
|
self.was_upgraded = (version_info[1], OSXPHOTOS_EXPORTDB_VERSION)
|
||||||
else:
|
else:
|
||||||
self.was_upgraded = ()
|
self.was_upgraded = ()
|
||||||
@@ -664,104 +722,97 @@ class ExportDB(ExportDB_ABC):
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
return (version_info[0], version_info[1])
|
return (version_info[0], version_info[1])
|
||||||
|
|
||||||
def _create_db_tables(self, conn):
|
def _create_or_migrate_db_tables(self, conn):
|
||||||
"""create (if not already created) the necessary db tables for the export database
|
"""create (if not already created) the necessary db tables for the export database and apply any needed migrations
|
||||||
conn: sqlite3 db connection
|
|
||||||
|
Args:
|
||||||
|
conn: sqlite3 db connection
|
||||||
"""
|
"""
|
||||||
sql_commands = {
|
try:
|
||||||
"sql_version_table": """ CREATE TABLE IF NOT EXISTS version (
|
version = self._get_database_version(conn)
|
||||||
id INTEGER PRIMARY KEY,
|
except Exception as e:
|
||||||
osxphotos TEXT,
|
version = (__version__, OSXPHOTOS_EXPORTDB_VERSION_MIGRATE_TABLES)
|
||||||
exportdb TEXT
|
|
||||||
); """,
|
# Current for version 4.3, for anything greater, do a migration after creation
|
||||||
"sql_about_table": """ CREATE TABLE IF NOT EXISTS about (
|
sql_commands = [
|
||||||
id INTEGER PRIMARY KEY,
|
""" CREATE TABLE IF NOT EXISTS version (
|
||||||
about TEXT
|
id INTEGER PRIMARY KEY,
|
||||||
);""",
|
osxphotos TEXT,
|
||||||
"sql_files_table": """ CREATE TABLE IF NOT EXISTS files (
|
exportdb TEXT
|
||||||
id INTEGER PRIMARY KEY,
|
); """,
|
||||||
filepath TEXT NOT NULL,
|
""" CREATE TABLE IF NOT EXISTS about (
|
||||||
filepath_normalized TEXT NOT NULL,
|
id INTEGER PRIMARY KEY,
|
||||||
uuid TEXT,
|
about TEXT
|
||||||
orig_mode INTEGER,
|
);""",
|
||||||
orig_size INTEGER,
|
""" CREATE TABLE IF NOT EXISTS files (
|
||||||
orig_mtime REAL,
|
id INTEGER PRIMARY KEY,
|
||||||
exif_mode INTEGER,
|
filepath TEXT NOT NULL,
|
||||||
exif_size INTEGER,
|
filepath_normalized TEXT NOT NULL,
|
||||||
exif_mtime REAL
|
uuid TEXT,
|
||||||
); """,
|
orig_mode INTEGER,
|
||||||
"sql_files_table_migrate": """ CREATE TABLE IF NOT EXISTS files_migrate (
|
orig_size INTEGER,
|
||||||
id INTEGER PRIMARY KEY,
|
orig_mtime REAL,
|
||||||
filepath TEXT NOT NULL,
|
exif_mode INTEGER,
|
||||||
filepath_normalized TEXT NOT NULL,
|
exif_size INTEGER,
|
||||||
uuid TEXT,
|
exif_mtime REAL
|
||||||
orig_mode INTEGER,
|
); """,
|
||||||
orig_size INTEGER,
|
""" CREATE TABLE IF NOT EXISTS runs (
|
||||||
orig_mtime REAL,
|
id INTEGER PRIMARY KEY,
|
||||||
exif_mode INTEGER,
|
datetime TEXT,
|
||||||
exif_size INTEGER,
|
python_path TEXT,
|
||||||
exif_mtime REAL,
|
script_name TEXT,
|
||||||
UNIQUE(filepath_normalized)
|
args TEXT,
|
||||||
); """,
|
cwd TEXT
|
||||||
"sql_files_migrate": """ INSERT INTO files_migrate SELECT * FROM files;""",
|
); """,
|
||||||
"sql_files_drop_tables": """ DROP TABLE files;""",
|
""" CREATE TABLE IF NOT EXISTS info (
|
||||||
"sql_files_alter": """ ALTER TABLE files_migrate RENAME TO files;""",
|
id INTEGER PRIMARY KEY,
|
||||||
"sql_runs_table": """ CREATE TABLE IF NOT EXISTS runs (
|
uuid text NOT NULL,
|
||||||
id INTEGER PRIMARY KEY,
|
json_info JSON
|
||||||
datetime TEXT,
|
); """,
|
||||||
python_path TEXT,
|
""" CREATE TABLE IF NOT EXISTS exifdata (
|
||||||
script_name TEXT,
|
id INTEGER PRIMARY KEY,
|
||||||
args TEXT,
|
filepath_normalized TEXT NOT NULL,
|
||||||
cwd TEXT
|
json_exifdata JSON
|
||||||
); """,
|
); """,
|
||||||
"sql_info_table": """ CREATE TABLE IF NOT EXISTS info (
|
""" CREATE TABLE IF NOT EXISTS edited (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
uuid text NOT NULL,
|
filepath_normalized TEXT NOT NULL,
|
||||||
json_info JSON
|
mode INTEGER,
|
||||||
); """,
|
size INTEGER,
|
||||||
"sql_exifdata_table": """ CREATE TABLE IF NOT EXISTS exifdata (
|
mtime REAL
|
||||||
id INTEGER PRIMARY KEY,
|
); """,
|
||||||
filepath_normalized TEXT NOT NULL,
|
""" CREATE TABLE IF NOT EXISTS converted (
|
||||||
json_exifdata JSON
|
id INTEGER PRIMARY KEY,
|
||||||
); """,
|
filepath_normalized TEXT NOT NULL,
|
||||||
"sql_edited_table": """ CREATE TABLE IF NOT EXISTS edited (
|
mode INTEGER,
|
||||||
id INTEGER PRIMARY KEY,
|
size INTEGER,
|
||||||
filepath_normalized TEXT NOT NULL,
|
mtime REAL
|
||||||
mode INTEGER,
|
); """,
|
||||||
size INTEGER,
|
""" CREATE TABLE IF NOT EXISTS sidecar (
|
||||||
mtime REAL
|
id INTEGER PRIMARY KEY,
|
||||||
); """,
|
filepath_normalized TEXT NOT NULL,
|
||||||
"sql_converted_table": """ CREATE TABLE IF NOT EXISTS converted (
|
sidecar_data TEXT,
|
||||||
id INTEGER PRIMARY KEY,
|
mode INTEGER,
|
||||||
filepath_normalized TEXT NOT NULL,
|
size INTEGER,
|
||||||
mode INTEGER,
|
mtime REAL
|
||||||
size INTEGER,
|
); """,
|
||||||
mtime REAL
|
""" CREATE TABLE IF NOT EXISTS detected_text (
|
||||||
); """,
|
id INTEGER PRIMARY KEY,
|
||||||
"sql_sidecar_table": """ CREATE TABLE IF NOT EXISTS sidecar (
|
uuid TEXT NOT NULL,
|
||||||
id INTEGER PRIMARY KEY,
|
text_data JSON
|
||||||
filepath_normalized TEXT NOT NULL,
|
); """,
|
||||||
sidecar_data TEXT,
|
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """,
|
||||||
mode INTEGER,
|
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_info_uuid on info (uuid); """,
|
||||||
size INTEGER,
|
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_exifdata_filename on exifdata (filepath_normalized); """,
|
||||||
mtime REAL
|
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_edited_filename on edited (filepath_normalized);""",
|
||||||
); """,
|
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""",
|
||||||
"sql_detected_text_table": """ CREATE TABLE IF NOT EXISTS detected_text (
|
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_sidecar_filename on sidecar (filepath_normalized);""",
|
||||||
id INTEGER PRIMARY KEY,
|
""" CREATE UNIQUE INDEX IF NOT EXISTS idx_detected_text on detected_text (uuid);""",
|
||||||
uuid TEXT NOT NULL,
|
]
|
||||||
text_data JSON
|
# create the tables if needed
|
||||||
); """,
|
|
||||||
"sql_files_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """,
|
|
||||||
"sql_info_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_info_uuid on info (uuid); """,
|
|
||||||
"sql_exifdata_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_exifdata_filename on exifdata (filepath_normalized); """,
|
|
||||||
"sql_edited_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_edited_filename on edited (filepath_normalized);""",
|
|
||||||
"sql_converted_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""",
|
|
||||||
"sql_sidecar_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_sidecar_filename on sidecar (filepath_normalized);""",
|
|
||||||
"sql_detected_text_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_detected_text on detected_text (uuid);""",
|
|
||||||
}
|
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
for cmd in sql_commands.values():
|
for cmd in sql_commands:
|
||||||
c.execute(cmd)
|
c.execute(cmd)
|
||||||
c.execute(
|
c.execute(
|
||||||
"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);",
|
"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);",
|
||||||
@@ -772,6 +823,19 @@ class ExportDB(ExportDB_ABC):
|
|||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
|
|
||||||
|
# perform needed migrations
|
||||||
|
if version[1] < OSXPHOTOS_EXPORTDB_VERSION_MIGRATE_FILEPATH:
|
||||||
|
self._migrate_normalized_filepath(conn)
|
||||||
|
|
||||||
|
if version[1] < OSXPHOTOS_EXPORTDB_VERSION:
|
||||||
|
try:
|
||||||
|
c = conn.cursor()
|
||||||
|
# add metadata column to files to support --force-update
|
||||||
|
c.execute("ALTER TABLE files ADD COLUMN metadata TEXT;")
|
||||||
|
conn.commit()
|
||||||
|
except Error as e:
|
||||||
|
logging.warning(e)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""ensure the database connection is closed"""
|
"""ensure the database connection is closed"""
|
||||||
try:
|
try:
|
||||||
@@ -810,6 +874,28 @@ class ExportDB(ExportDB_ABC):
|
|||||||
"""Fix all filepath_normalized columns for unicode normalization"""
|
"""Fix all filepath_normalized columns for unicode normalization"""
|
||||||
# Prior to database version 4.3, filepath_normalized was not normalized for unicode
|
# Prior to database version 4.3, filepath_normalized was not normalized for unicode
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
migration_sql = [
|
||||||
|
""" CREATE TABLE IF NOT EXISTS files_migrate (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
filepath TEXT NOT NULL,
|
||||||
|
filepath_normalized TEXT NOT NULL,
|
||||||
|
uuid TEXT,
|
||||||
|
orig_mode INTEGER,
|
||||||
|
orig_size INTEGER,
|
||||||
|
orig_mtime REAL,
|
||||||
|
exif_mode INTEGER,
|
||||||
|
exif_size INTEGER,
|
||||||
|
exif_mtime REAL,
|
||||||
|
UNIQUE(filepath_normalized)
|
||||||
|
); """,
|
||||||
|
""" INSERT INTO files_migrate SELECT * FROM files;""",
|
||||||
|
""" DROP TABLE files;""",
|
||||||
|
""" ALTER TABLE files_migrate RENAME TO files;""",
|
||||||
|
]
|
||||||
|
for sql in migration_sql:
|
||||||
|
c.execute(sql)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
for table in ["converted", "edited", "exifdata", "files", "sidecar"]:
|
for table in ["converted", "edited", "exifdata", "files", "sidecar"]:
|
||||||
old_values = c.execute(
|
old_values = c.execute(
|
||||||
f"SELECT filepath_normalized, id FROM {table}"
|
f"SELECT filepath_normalized, id FROM {table}"
|
||||||
@@ -848,7 +934,7 @@ class ExportDBInMemory(ExportDB):
|
|||||||
conn = self._get_db_connection()
|
conn = self._get_db_connection()
|
||||||
if not conn:
|
if not conn:
|
||||||
raise Exception("Error getting connection to in-memory database")
|
raise Exception("Error getting connection to in-memory database")
|
||||||
self._create_db_tables(conn)
|
self._create_or_migrate_db_tables(conn)
|
||||||
self.was_created = True
|
self.was_created = True
|
||||||
self.was_upgraded = ()
|
self.was_upgraded = ()
|
||||||
else:
|
else:
|
||||||
@@ -871,7 +957,7 @@ class ExportDBInMemory(ExportDB):
|
|||||||
self.was_created = False
|
self.was_created = False
|
||||||
_, exportdb_ver = self._get_database_version(conn)
|
_, exportdb_ver = self._get_database_version(conn)
|
||||||
if exportdb_ver < OSXPHOTOS_EXPORTDB_VERSION:
|
if exportdb_ver < OSXPHOTOS_EXPORTDB_VERSION:
|
||||||
self._create_db_tables(conn)
|
self._create_or_migrate_db_tables(conn)
|
||||||
self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION)
|
self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION)
|
||||||
else:
|
else:
|
||||||
self.was_upgraded = ()
|
self.was_upgraded = ()
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class ExportOptions:
|
|||||||
export_as_hardlink: (bool, default=False): if True, will hardlink files instead of copying them
|
export_as_hardlink: (bool, default=False): if True, will hardlink files instead of copying them
|
||||||
export_db: (ExportDB_ABC): instance of a class that conforms to ExportDB_ABC with methods for getting/setting data related to exported files to compare update state
|
export_db: (ExportDB_ABC): instance of a class that conforms to ExportDB_ABC with methods for getting/setting data related to exported files to compare update state
|
||||||
fileutil: (FileUtilABC): class that conforms to FileUtilABC with various file utilities
|
fileutil: (FileUtilABC): class that conforms to FileUtilABC with various file utilities
|
||||||
|
force_update: (bool, default=False): if True, will export photo if any metadata has changed but export otherwise would not be triggered (e.g. metadata changed but not using exiftool)
|
||||||
ignore_date_modified (bool): for use with sidecar and exiftool; if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
ignore_date_modified (bool): for use with sidecar and exiftool; if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||||
ignore_signature (bool, default=False): ignore file signature when used with update (look only at filename)
|
ignore_signature (bool, default=False): ignore file signature when used with update (look only at filename)
|
||||||
increment (bool, default=True): if True, will increment file name until a non-existant name is found if overwrite=False and increment=False, export will fail if destination file already exists
|
increment (bool, default=True): if True, will increment file name until a non-existant name is found if overwrite=False and increment=False, export will fail if destination file already exists
|
||||||
@@ -128,6 +129,7 @@ class ExportOptions:
|
|||||||
export_as_hardlink: bool = False
|
export_as_hardlink: bool = False
|
||||||
export_db: Optional[ExportDB_ABC] = None
|
export_db: Optional[ExportDB_ABC] = None
|
||||||
fileutil: Optional[FileUtil] = None
|
fileutil: Optional[FileUtil] = None
|
||||||
|
force_update: bool = False
|
||||||
ignore_date_modified: bool = False
|
ignore_date_modified: bool = False
|
||||||
ignore_signature: bool = False
|
ignore_signature: bool = False
|
||||||
increment: bool = True
|
increment: bool = True
|
||||||
@@ -523,7 +525,7 @@ class PhotoExporter:
|
|||||||
# the export directory
|
# the export directory
|
||||||
preview_name = (
|
preview_name = (
|
||||||
preview_name
|
preview_name
|
||||||
if options.overwrite or options.update
|
if any([options.overwrite, options.update, options.force_update])
|
||||||
else pathlib.Path(increment_filename(preview_name))
|
else pathlib.Path(increment_filename(preview_name))
|
||||||
)
|
)
|
||||||
all_results += self._export_photo(
|
all_results += self._export_photo(
|
||||||
@@ -589,7 +591,7 @@ class PhotoExporter:
|
|||||||
|
|
||||||
# if overwrite==False and #increment==False, export should fail if file exists
|
# if overwrite==False and #increment==False, export should fail if file exists
|
||||||
if dest.exists() and not any(
|
if dest.exists() and not any(
|
||||||
[options.increment, options.update, options.overwrite]
|
[options.increment, options.update, options.force_update, options.overwrite]
|
||||||
):
|
):
|
||||||
raise FileExistsError(
|
raise FileExistsError(
|
||||||
f"destination exists ({dest}); overwrite={options.overwrite}, increment={options.increment}"
|
f"destination exists ({dest}); overwrite={options.overwrite}, increment={options.increment}"
|
||||||
@@ -601,11 +603,13 @@ class PhotoExporter:
|
|||||||
# e.g. exporting sidecar for file1.png and file1.jpeg
|
# e.g. exporting sidecar for file1.png and file1.jpeg
|
||||||
# if file1.png exists and exporting file1.jpeg,
|
# if file1.png exists and exporting file1.jpeg,
|
||||||
# dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision
|
# dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision
|
||||||
if options.increment and not options.update and not options.overwrite:
|
if options.increment and not any(
|
||||||
|
[options.update, options.force_update, options.overwrite]
|
||||||
|
):
|
||||||
return pathlib.Path(increment_filename(dest))
|
return pathlib.Path(increment_filename(dest))
|
||||||
|
|
||||||
# if update and file exists, need to check to see if it's the write file by checking export db
|
# if update and file exists, need to check to see if it's the write file by checking export db
|
||||||
if options.update and dest.exists() and src:
|
if (options.update or options.force_update) and dest.exists() and src:
|
||||||
export_db = options.export_db
|
export_db = options.export_db
|
||||||
fileutil = options.fileutil
|
fileutil = options.fileutil
|
||||||
# destination exists, check to see if destination is the right UUID
|
# destination exists, check to see if destination is the right UUID
|
||||||
@@ -735,7 +739,7 @@ class PhotoExporter:
|
|||||||
# export live_photo .mov file?
|
# export live_photo .mov file?
|
||||||
live_photo = bool(options.live_photo and self.photo.live_photo)
|
live_photo = bool(options.live_photo and self.photo.live_photo)
|
||||||
|
|
||||||
overwrite = options.overwrite or options.update
|
overwrite = any([options.overwrite, options.update, options.force_update])
|
||||||
|
|
||||||
# figure out which photo version to request
|
# figure out which photo version to request
|
||||||
if options.edited or self.photo.shared:
|
if options.edited or self.photo.shared:
|
||||||
@@ -843,7 +847,7 @@ class PhotoExporter:
|
|||||||
|
|
||||||
# export live_photo .mov file?
|
# export live_photo .mov file?
|
||||||
live_photo = bool(options.live_photo and self.photo.live_photo)
|
live_photo = bool(options.live_photo and self.photo.live_photo)
|
||||||
overwrite = options.overwrite or options.update
|
overwrite = any([options.overwrite, options.update, options.force_update])
|
||||||
edited_version = options.edited or self.photo.shared
|
edited_version = options.edited or self.photo.shared
|
||||||
# shared photos (in shared albums) show up as not having adjustments (not edited)
|
# shared photos (in shared albums) show up as not having adjustments (not edited)
|
||||||
# but Photos is unable to export the "original" as only a jpeg copy is shared in iCloud
|
# but Photos is unable to export the "original" as only a jpeg copy is shared in iCloud
|
||||||
@@ -995,16 +999,11 @@ class PhotoExporter:
|
|||||||
fileutil = options.fileutil
|
fileutil = options.fileutil
|
||||||
export_db = options.export_db
|
export_db = options.export_db
|
||||||
|
|
||||||
if options.update: # updating
|
if options.update or options.force_update: # updating
|
||||||
cmp_touch, cmp_orig = False, False
|
cmp_touch, cmp_orig = False, False
|
||||||
if dest_exists:
|
if dest_exists:
|
||||||
# update, destination exists, but we might not need to replace it...
|
# update, destination exists, but we might not need to replace it...
|
||||||
if options.ignore_signature:
|
if options.exiftool:
|
||||||
cmp_orig = True
|
|
||||||
cmp_touch = fileutil.cmp(
|
|
||||||
src, dest, mtime1=int(self.photo.date.timestamp())
|
|
||||||
)
|
|
||||||
elif options.exiftool:
|
|
||||||
sig_exif = export_db.get_stat_exif_for_file(dest_str)
|
sig_exif = export_db.get_stat_exif_for_file(dest_str)
|
||||||
cmp_orig = fileutil.cmp_file_sig(dest_str, sig_exif)
|
cmp_orig = fileutil.cmp_file_sig(dest_str, sig_exif)
|
||||||
if cmp_orig:
|
if cmp_orig:
|
||||||
@@ -1026,10 +1025,17 @@ class PhotoExporter:
|
|||||||
)
|
)
|
||||||
cmp_touch = fileutil.cmp_file_sig(dest_str, sig_converted)
|
cmp_touch = fileutil.cmp_file_sig(dest_str, sig_converted)
|
||||||
else:
|
else:
|
||||||
cmp_orig = fileutil.cmp(src, dest)
|
cmp_orig = options.ignore_signature or fileutil.cmp(src, dest)
|
||||||
cmp_touch = fileutil.cmp(
|
cmp_touch = fileutil.cmp(
|
||||||
src, dest, mtime1=int(self.photo.date.timestamp())
|
src, dest, mtime1=int(self.photo.date.timestamp())
|
||||||
)
|
)
|
||||||
|
if options.force_update:
|
||||||
|
# need to also check the photo's metadata to that in the database
|
||||||
|
# and if anything changed, we need to update the file
|
||||||
|
# ony the hex digest of the metadata is stored in the database
|
||||||
|
cmp_orig = hexdigest(
|
||||||
|
self.photo.json()
|
||||||
|
) == export_db.get_metadata_for_file(dest_str)
|
||||||
|
|
||||||
sig_cmp = cmp_touch if options.touch_file else cmp_orig
|
sig_cmp = cmp_touch if options.touch_file else cmp_orig
|
||||||
|
|
||||||
@@ -1043,7 +1049,7 @@ class PhotoExporter:
|
|||||||
if sig_edited != (None, None, None)
|
if sig_edited != (None, None, None)
|
||||||
else False
|
else False
|
||||||
)
|
)
|
||||||
sig_cmp = sig_cmp and cmp_edited
|
sig_cmp = sig_cmp and (options.force_update or cmp_edited)
|
||||||
|
|
||||||
if (options.export_as_hardlink and dest.samefile(src)) or (
|
if (options.export_as_hardlink and dest.samefile(src)) or (
|
||||||
not options.export_as_hardlink
|
not options.export_as_hardlink
|
||||||
@@ -1086,7 +1092,9 @@ class PhotoExporter:
|
|||||||
edited_stat = (
|
edited_stat = (
|
||||||
fileutil.file_sig(src) if options.edited else (None, None, None)
|
fileutil.file_sig(src) if options.edited else (None, None, None)
|
||||||
)
|
)
|
||||||
if dest_exists and (options.update or options.overwrite):
|
if dest_exists and any(
|
||||||
|
[options.overwrite, options.update, options.force_update]
|
||||||
|
):
|
||||||
# need to remove the destination first
|
# need to remove the destination first
|
||||||
try:
|
try:
|
||||||
fileutil.unlink(dest)
|
fileutil.unlink(dest)
|
||||||
@@ -1129,13 +1137,15 @@ class PhotoExporter:
|
|||||||
f"Error copying file {src} to {dest_str}: {e} ({lineno(__file__)})"
|
f"Error copying file {src} to {dest_str}: {e} ({lineno(__file__)})"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
json_info = self.photo.json()
|
||||||
export_db.set_data(
|
export_db.set_data(
|
||||||
filename=dest_str,
|
filename=dest_str,
|
||||||
uuid=self.photo.uuid,
|
uuid=self.photo.uuid,
|
||||||
orig_stat=fileutil.file_sig(dest_str),
|
orig_stat=fileutil.file_sig(dest_str),
|
||||||
converted_stat=converted_stat,
|
converted_stat=converted_stat,
|
||||||
edited_stat=edited_stat,
|
edited_stat=edited_stat,
|
||||||
info_json=self.photo.json(),
|
info_json=json_info,
|
||||||
|
metadata=hexdigest(json_info),
|
||||||
)
|
)
|
||||||
|
|
||||||
return ExportResults(
|
return ExportResults(
|
||||||
@@ -1235,10 +1245,13 @@ class PhotoExporter:
|
|||||||
sidecar_filename
|
sidecar_filename
|
||||||
)
|
)
|
||||||
write_sidecar = (
|
write_sidecar = (
|
||||||
not options.update
|
not (options.update or options.force_update)
|
||||||
or (options.update and not sidecar_filename.exists())
|
|
||||||
or (
|
or (
|
||||||
options.update
|
(options.update or options.force_update)
|
||||||
|
and not sidecar_filename.exists()
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
(options.update or options.force_update)
|
||||||
and (sidecar_digest != old_sidecar_digest)
|
and (sidecar_digest != old_sidecar_digest)
|
||||||
or not fileutil.cmp_file_sig(sidecar_filename, sidecar_sig)
|
or not fileutil.cmp_file_sig(sidecar_filename, sidecar_sig)
|
||||||
)
|
)
|
||||||
@@ -1333,8 +1346,8 @@ class PhotoExporter:
|
|||||||
|
|
||||||
def _should_run_exiftool(self, dest, options: ExportOptions) -> bool:
|
def _should_run_exiftool(self, dest, options: ExportOptions) -> bool:
|
||||||
"""Return True if exiftool should be run to update metadata"""
|
"""Return True if exiftool should be run to update metadata"""
|
||||||
run_exiftool = not options.update
|
run_exiftool = not (options.update or options.force_update)
|
||||||
if options.update:
|
if options.update or options.force_update:
|
||||||
files_are_different = False
|
files_are_different = False
|
||||||
old_data = options.export_db.get_exifdata_for_file(dest)
|
old_data = options.export_db.get_exifdata_for_file(dest)
|
||||||
if old_data is not None:
|
if old_data is not None:
|
||||||
|
|||||||
@@ -1728,7 +1728,8 @@ class PhotoInfo:
|
|||||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||||
return o.isoformat()
|
return o.isoformat()
|
||||||
|
|
||||||
return json.dumps(self.asdict(), sort_keys=True, default=default)
|
dict_data = self.asdict()
|
||||||
|
return json.dumps(dict_data, sort_keys=True, default=default)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
"""Compare two PhotoInfo objects for equality"""
|
"""Compare two PhotoInfo objects for equality"""
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ else:
|
|||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def reset_singletons():
|
def reset_singletons():
|
||||||
""" Need to clean up any ExifTool singletons between tests """
|
"""Need to clean up any ExifTool singletons between tests"""
|
||||||
_ExifToolProc.instance = None
|
_ExifToolProc.instance = None
|
||||||
|
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ def pytest_collection_modifyitems(config, items):
|
|||||||
|
|
||||||
|
|
||||||
def copy_photos_library(photos_library=TEST_LIBRARY, delay=0):
|
def copy_photos_library(photos_library=TEST_LIBRARY, delay=0):
|
||||||
""" copy the test library and open Photos, returns path to copied library """
|
"""copy the test library and open Photos, returns path to copied library"""
|
||||||
script = AppleScript(
|
script = AppleScript(
|
||||||
"""
|
"""
|
||||||
tell application "Photos"
|
tell application "Photos"
|
||||||
@@ -118,3 +118,9 @@ def copy_photos_library(photos_library=TEST_LIBRARY, delay=0):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def addalbum_library():
|
def addalbum_library():
|
||||||
copy_photos_library(delay=10)
|
copy_photos_library(delay=10)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_photos_library_to_path(photos_library_path: str, dest_path: str) -> str:
|
||||||
|
"""Copy a photos library to a folder"""
|
||||||
|
ditto(photos_library_path, dest_path)
|
||||||
|
return dest_path
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
r""" Test the command line interface (CLI) """
|
""" Test the command line interface (CLI) """
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import sqlite3
|
||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
from conftest import copy_photos_library_to_path
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos.exiftool import get_exiftool_path
|
from osxphotos.exiftool import get_exiftool_path
|
||||||
@@ -2034,7 +2036,6 @@ def test_export_exiftool_template_change():
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "updated EXIF data: 1" in result.output
|
assert "updated EXIF data: 1" in result.output
|
||||||
|
|
||||||
|
|
||||||
# export with --update, nothing should export
|
# export with --update, nothing should export
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
export,
|
export,
|
||||||
@@ -2054,6 +2055,7 @@ def test_export_exiftool_template_change():
|
|||||||
assert "exported: 0" in result.output
|
assert "exported: 0" in result.output
|
||||||
assert "updated EXIF data: 0" in result.output
|
assert "updated EXIF data: 0" in result.output
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
|
||||||
def test_export_exiftool_path():
|
def test_export_exiftool_path():
|
||||||
"""test --exiftool with --exiftool-path"""
|
"""test --exiftool with --exiftool-path"""
|
||||||
@@ -4809,6 +4811,75 @@ def test_export_update_basic():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_force_update():
|
||||||
|
"""test export with --force-update"""
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import osxphotos
|
||||||
|
from osxphotos.cli import OSXPHOTOS_EXPORT_DB, export
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
# basic export
|
||||||
|
result = runner.invoke(export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "-V"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
files = glob.glob("*")
|
||||||
|
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES)
|
||||||
|
assert os.path.isfile(OSXPHOTOS_EXPORT_DB)
|
||||||
|
|
||||||
|
src = os.path.join(cwd, CLI_PHOTOS_DB)
|
||||||
|
dest = os.path.join(os.getcwd(), "export_force_update.photoslibrary")
|
||||||
|
photos_db_path = copy_photos_library_to_path(src, dest)
|
||||||
|
|
||||||
|
# update
|
||||||
|
result = runner.invoke(
|
||||||
|
export, [os.path.join(cwd, photos_db_path), ".", "--update"]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (
|
||||||
|
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 3, error: 0"
|
||||||
|
in result.output
|
||||||
|
)
|
||||||
|
|
||||||
|
# force update
|
||||||
|
result = runner.invoke(
|
||||||
|
export, [os.path.join(cwd, photos_db_path), ".", "--force-update"]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (
|
||||||
|
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 3, error: 0"
|
||||||
|
in result.output
|
||||||
|
)
|
||||||
|
|
||||||
|
# update a file
|
||||||
|
dbpath = os.path.join(photos_db_path, "database/Photos.sqlite")
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(dbpath)
|
||||||
|
c = conn.cursor()
|
||||||
|
except sqlite3.Error as e:
|
||||||
|
pytest.exit(f"An error occurred opening sqlite file")
|
||||||
|
|
||||||
|
# photo is IMG_4547.jpg
|
||||||
|
c.execute(
|
||||||
|
"UPDATE ZADDITIONALASSETATTRIBUTES SET Z_OPT=9, ZTITLE='My Updated Title' WHERE Z_PK=8;"
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# run again to see if updated metadata forced update
|
||||||
|
result = runner.invoke(
|
||||||
|
export, [os.path.join(cwd, photos_db_path), ".", "--force-update"]
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert (
|
||||||
|
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 1, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7-1}, updated EXIF data: 0, missing: 3, error: 0"
|
||||||
|
in result.output
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
"OSXPHOTOS_TEST_EXPORT" not in os.environ,
|
"OSXPHOTOS_TEST_EXPORT" not in os.environ,
|
||||||
reason="Skip if not running on author's personal library.",
|
reason="Skip if not running on author's personal library.",
|
||||||
@@ -4931,7 +5002,7 @@ def test_export_update_exiftool():
|
|||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert (
|
assert (
|
||||||
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 2, error: 0"
|
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 3, error: 0"
|
||||||
in result.output
|
in result.output
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import pytest
|
|||||||
EXIF_DATA = """[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos", "EXIF:ImageDescription": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "XMP:Description": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "XMP:Title": "Elder Park", "EXIF:GPSLatitude": "34 deg 55' 8.01\" S", "EXIF:GPSLongitude": "138 deg 35' 48.70\" E", "Composite:GPSPosition": "34 deg 55' 8.01\" S, 138 deg 35' 48.70\" E", "EXIF:GPSLatitudeRef": "South", "EXIF:GPSLongitudeRef": "East", "EXIF:DateTimeOriginal": "2017:06:20 17:18:56", "EXIF:OffsetTimeOriginal": "+09:30", "EXIF:ModifyDate": "2020:05:18 14:42:04"}]"""
|
EXIF_DATA = """[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos", "EXIF:ImageDescription": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "XMP:Description": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "XMP:Title": "Elder Park", "EXIF:GPSLatitude": "34 deg 55' 8.01\" S", "EXIF:GPSLongitude": "138 deg 35' 48.70\" E", "Composite:GPSPosition": "34 deg 55' 8.01\" S, 138 deg 35' 48.70\" E", "EXIF:GPSLatitudeRef": "South", "EXIF:GPSLongitudeRef": "East", "EXIF:DateTimeOriginal": "2017:06:20 17:18:56", "EXIF:OffsetTimeOriginal": "+09:30", "EXIF:ModifyDate": "2020:05:18 14:42:04"}]"""
|
||||||
INFO_DATA = """{"uuid": "3DD2C897-F19E-4CA6-8C22-B027D5A71907", "filename": "3DD2C897-F19E-4CA6-8C22-B027D5A71907.jpeg", "original_filename": "IMG_4547.jpg", "date": "2017-06-20T17:18:56.518000+09:30", "description": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "title": "Elder Park", "keywords": [], "labels": ["Statue", "Art"], "albums": ["AlbumInFolder"], "folders": {"AlbumInFolder": ["Folder1", "SubFolder2"]}, "persons": [], "path": "/Users/rhet/Pictures/Test-10.15.4.photoslibrary/originals/3/3DD2C897-F19E-4CA6-8C22-B027D5A71907.jpeg", "ismissing": false, "hasadjustments": true, "external_edit": false, "favorite": false, "hidden": false, "latitude": -34.91889167000001, "longitude": 138.59686167, "path_edited": "/Users/rhet/Pictures/Test-10.15.4.photoslibrary/resources/renders/3/3DD2C897-F19E-4CA6-8C22-B027D5A71907_1_201_a.jpeg", "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null, "date_modified": "2020-05-18T14:42:04.608664+09:30", "portrait": false, "screenshot": false, "slow_mo": false, "time_lapse": false, "hdr": false, "selfie": false, "panorama": false, "has_raw": false, "uti_raw": null, "path_raw": null, "place": {"name": "Elder Park, Adelaide, South Australia, Australia, River Torrens", "names": {"field0": [], "country": ["Australia"], "state_province": ["South Australia"], "sub_administrative_area": ["Adelaide"], "city": ["Adelaide", "Adelaide"], "field5": [], "additional_city_info": ["Adelaide CBD", "Tarndanya"], "ocean": [], "area_of_interest": ["Elder Park", ""], "inland_water": ["River Torrens", "River Torrens"], "field10": [], "region": [], "sub_throughfare": [], "field13": [], "postal_code": [], "field15": [], "field16": [], "street_address": [], "body_of_water": ["River Torrens", "River Torrens"]}, "country_code": "AU", "ishome": false, "address_str": "River Torrens, Adelaide SA, Australia", "address": {"street": null, "sub_locality": "Tarndanya", "city": "Adelaide", "sub_administrative_area": "Adelaide", "state_province": "SA", "postal_code": null, "country": "Australia", "iso_country_code": "AU"}}, "exif": {"flash_fired": false, "iso": 320, "metering_mode": 3, "sample_rate": null, "track_format": null, "white_balance": 0, "aperture": 2.2, "bit_rate": null, "duration": null, "exposure_bias": 0.0, "focal_length": 4.15, "fps": null, "latitude": null, "longitude": null, "shutter_speed": 0.058823529411764705, "camera_make": "Apple", "camera_model": "iPhone 6s", "codec": null, "lens_model": "iPhone 6s back camera 4.15mm f/2.2"}}"""
|
INFO_DATA = """{"uuid": "3DD2C897-F19E-4CA6-8C22-B027D5A71907", "filename": "3DD2C897-F19E-4CA6-8C22-B027D5A71907.jpeg", "original_filename": "IMG_4547.jpg", "date": "2017-06-20T17:18:56.518000+09:30", "description": "\u2068Elder Park\u2069, \u2068Adelaide\u2069, \u2068Australia\u2069", "title": "Elder Park", "keywords": [], "labels": ["Statue", "Art"], "albums": ["AlbumInFolder"], "folders": {"AlbumInFolder": ["Folder1", "SubFolder2"]}, "persons": [], "path": "/Users/rhet/Pictures/Test-10.15.4.photoslibrary/originals/3/3DD2C897-F19E-4CA6-8C22-B027D5A71907.jpeg", "ismissing": false, "hasadjustments": true, "external_edit": false, "favorite": false, "hidden": false, "latitude": -34.91889167000001, "longitude": 138.59686167, "path_edited": "/Users/rhet/Pictures/Test-10.15.4.photoslibrary/resources/renders/3/3DD2C897-F19E-4CA6-8C22-B027D5A71907_1_201_a.jpeg", "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null, "date_modified": "2020-05-18T14:42:04.608664+09:30", "portrait": false, "screenshot": false, "slow_mo": false, "time_lapse": false, "hdr": false, "selfie": false, "panorama": false, "has_raw": false, "uti_raw": null, "path_raw": null, "place": {"name": "Elder Park, Adelaide, South Australia, Australia, River Torrens", "names": {"field0": [], "country": ["Australia"], "state_province": ["South Australia"], "sub_administrative_area": ["Adelaide"], "city": ["Adelaide", "Adelaide"], "field5": [], "additional_city_info": ["Adelaide CBD", "Tarndanya"], "ocean": [], "area_of_interest": ["Elder Park", ""], "inland_water": ["River Torrens", "River Torrens"], "field10": [], "region": [], "sub_throughfare": [], "field13": [], "postal_code": [], "field15": [], "field16": [], "street_address": [], "body_of_water": ["River Torrens", "River Torrens"]}, "country_code": "AU", "ishome": false, "address_str": "River Torrens, Adelaide SA, Australia", "address": {"street": null, "sub_locality": "Tarndanya", "city": "Adelaide", "sub_administrative_area": "Adelaide", "state_province": "SA", "postal_code": null, "country": "Australia", "iso_country_code": "AU"}}, "exif": {"flash_fired": false, "iso": 320, "metering_mode": 3, "sample_rate": null, "track_format": null, "white_balance": 0, "aperture": 2.2, "bit_rate": null, "duration": null, "exposure_bias": 0.0, "focal_length": 4.15, "fps": null, "latitude": null, "longitude": null, "shutter_speed": 0.058823529411764705, "camera_make": "Apple", "camera_model": "iPhone 6s", "codec": null, "lens_model": "iPhone 6s back camera 4.15mm f/2.2"}}"""
|
||||||
SIDECAR_DATA = """FOO_BAR"""
|
SIDECAR_DATA = """FOO_BAR"""
|
||||||
|
METADATA_DATA = "FIZZ"
|
||||||
|
|
||||||
EXIF_DATA2 = """[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos", "XMP:Title": "St. James's Park", "XMP:TagsList": ["London 2018", "St. James's Park", "England", "United Kingdom", "UK", "London"], "IPTC:Keywords": ["London 2018", "St. James's Park", "England", "United Kingdom", "UK", "London"], "XMP:Subject": ["London 2018", "St. James's Park", "England", "United Kingdom", "UK", "London"], "EXIF:GPSLatitude": "51 deg 30' 12.86\" N", "EXIF:GPSLongitude": "0 deg 7' 54.50\" W", "Composite:GPSPosition": "51 deg 30' 12.86\" N, 0 deg 7' 54.50\" W", "EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "EXIF:ModifyDate": "2019:12:08 14:06:44"}]"""
|
EXIF_DATA2 = """[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos", "XMP:Title": "St. James's Park", "XMP:TagsList": ["London 2018", "St. James's Park", "England", "United Kingdom", "UK", "London"], "IPTC:Keywords": ["London 2018", "St. James's Park", "England", "United Kingdom", "UK", "London"], "XMP:Subject": ["London 2018", "St. James's Park", "England", "United Kingdom", "UK", "London"], "EXIF:GPSLatitude": "51 deg 30' 12.86\" N", "EXIF:GPSLongitude": "0 deg 7' 54.50\" W", "Composite:GPSPosition": "51 deg 30' 12.86\" N, 0 deg 7' 54.50\" W", "EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West", "EXIF:DateTimeOriginal": "2018:10:13 09:18:12", "EXIF:OffsetTimeOriginal": "-04:00", "EXIF:ModifyDate": "2019:12:08 14:06:44"}]"""
|
||||||
INFO_DATA2 = """{"uuid": "F2BB3F98-90F0-4E4C-A09B-25C6822A4529", "filename": "F2BB3F98-90F0-4E4C-A09B-25C6822A4529.jpeg", "original_filename": "IMG_8440.JPG", "date": "2019-06-11T11:42:06.711805-07:00", "description": null, "title": null, "keywords": [], "labels": ["Sky", "Cloudy", "Fence", "Land", "Outdoor", "Park", "Amusement Park", "Roller Coaster"], "albums": [], "folders": {}, "persons": [], "path": "/Volumes/MacBook Catalina - Data/Users/rhet/Pictures/Photos Library.photoslibrary/originals/F/F2BB3F98-90F0-4E4C-A09B-25C6822A4529.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": 33.81558666666667, "longitude": -117.99298, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": true, "incloud": true, "date_modified": "2019-10-14T00:51:47.141950-07:00", "portrait": false, "screenshot": false, "slow_mo": false, "time_lapse": false, "hdr": false, "selfie": false, "panorama": false, "has_raw": false, "uti_raw": null, "path_raw": null, "place": {"name": "Adventure City, Stanton, California, United States", "names": {"field0": [], "country": ["United States"], "state_province": ["California"], "sub_administrative_area": ["Orange"], "city": ["Stanton", "Anaheim", "Anaheim"], "field5": [], "additional_city_info": ["West Anaheim"], "ocean": [], "area_of_interest": ["Adventure City", "Adventure City"], "inland_water": [], "field10": [], "region": [], "sub_throughfare": [], "field13": [], "postal_code": [], "field15": [], "field16": [], "street_address": [], "body_of_water": []}, "country_code": "US", "ishome": false, "address_str": "Adventure City, 1240 S Beach Blvd, Anaheim, CA 92804, United States", "address": {"street": "1240 S Beach Blvd", "sub_locality": "West Anaheim", "city": "Stanton", "sub_administrative_area": "Orange", "state_province": "CA", "postal_code": "92804", "country": "United States", "iso_country_code": "US"}}, "exif": {"flash_fired": false, "iso": 25, "metering_mode": 5, "sample_rate": null, "track_format": null, "white_balance": 0, "aperture": 2.2, "bit_rate": null, "duration": null, "exposure_bias": 0.0, "focal_length": 4.15, "fps": null, "latitude": null, "longitude": null, "shutter_speed": 0.0004940711462450593, "camera_make": "Apple", "camera_model": "iPhone 6s", "codec": null, "lens_model": "iPhone 6s back camera 4.15mm f/2.2"}}"""
|
INFO_DATA2 = """{"uuid": "F2BB3F98-90F0-4E4C-A09B-25C6822A4529", "filename": "F2BB3F98-90F0-4E4C-A09B-25C6822A4529.jpeg", "original_filename": "IMG_8440.JPG", "date": "2019-06-11T11:42:06.711805-07:00", "description": null, "title": null, "keywords": [], "labels": ["Sky", "Cloudy", "Fence", "Land", "Outdoor", "Park", "Amusement Park", "Roller Coaster"], "albums": [], "folders": {}, "persons": [], "path": "/Volumes/MacBook Catalina - Data/Users/rhet/Pictures/Photos Library.photoslibrary/originals/F/F2BB3F98-90F0-4E4C-A09B-25C6822A4529.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": 33.81558666666667, "longitude": -117.99298, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": true, "incloud": true, "date_modified": "2019-10-14T00:51:47.141950-07:00", "portrait": false, "screenshot": false, "slow_mo": false, "time_lapse": false, "hdr": false, "selfie": false, "panorama": false, "has_raw": false, "uti_raw": null, "path_raw": null, "place": {"name": "Adventure City, Stanton, California, United States", "names": {"field0": [], "country": ["United States"], "state_province": ["California"], "sub_administrative_area": ["Orange"], "city": ["Stanton", "Anaheim", "Anaheim"], "field5": [], "additional_city_info": ["West Anaheim"], "ocean": [], "area_of_interest": ["Adventure City", "Adventure City"], "inland_water": [], "field10": [], "region": [], "sub_throughfare": [], "field13": [], "postal_code": [], "field15": [], "field16": [], "street_address": [], "body_of_water": []}, "country_code": "US", "ishome": false, "address_str": "Adventure City, 1240 S Beach Blvd, Anaheim, CA 92804, United States", "address": {"street": "1240 S Beach Blvd", "sub_locality": "West Anaheim", "city": "Stanton", "sub_administrative_area": "Orange", "state_province": "CA", "postal_code": "92804", "country": "United States", "iso_country_code": "US"}}, "exif": {"flash_fired": false, "iso": 25, "metering_mode": 5, "sample_rate": null, "track_format": null, "white_balance": 0, "aperture": 2.2, "bit_rate": null, "duration": null, "exposure_bias": 0.0, "focal_length": 4.15, "fps": null, "latitude": null, "longitude": null, "shutter_speed": 0.0004940711462450593, "camera_make": "Apple", "camera_model": "iPhone 6s", "codec": null, "lens_model": "iPhone 6s back camera 4.15mm f/2.2"}}"""
|
||||||
@@ -64,6 +65,7 @@ def test_export_db():
|
|||||||
(10, 11, 12),
|
(10, 11, 12),
|
||||||
INFO_DATA,
|
INFO_DATA,
|
||||||
EXIF_DATA,
|
EXIF_DATA,
|
||||||
|
METADATA_DATA,
|
||||||
)
|
)
|
||||||
assert db.get_uuid_for_file(filepath2) == "BAR-FOO"
|
assert db.get_uuid_for_file(filepath2) == "BAR-FOO"
|
||||||
assert db.get_info_for_uuid("BAR-FOO") == INFO_DATA
|
assert db.get_info_for_uuid("BAR-FOO") == INFO_DATA
|
||||||
@@ -73,6 +75,7 @@ def test_export_db():
|
|||||||
assert db.get_stat_converted_for_file(filepath2) == (7, 8, 9)
|
assert db.get_stat_converted_for_file(filepath2) == (7, 8, 9)
|
||||||
assert db.get_stat_edited_for_file(filepath2) == (10, 11, 12)
|
assert db.get_stat_edited_for_file(filepath2) == (10, 11, 12)
|
||||||
assert sorted(db.get_previous_uuids()) == (["BAR-FOO", "FOO-BAR"])
|
assert sorted(db.get_previous_uuids()) == (["BAR-FOO", "FOO-BAR"])
|
||||||
|
assert db.get_metadata_for_file(filepath2) == METADATA_DATA
|
||||||
|
|
||||||
# test set_data value=None doesn't overwrite existing data
|
# test set_data value=None doesn't overwrite existing data
|
||||||
db.set_data(
|
db.set_data(
|
||||||
@@ -84,6 +87,7 @@ def test_export_db():
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
assert db.get_uuid_for_file(filepath2) == "BAR-FOO"
|
assert db.get_uuid_for_file(filepath2) == "BAR-FOO"
|
||||||
assert db.get_info_for_uuid("BAR-FOO") == INFO_DATA
|
assert db.get_info_for_uuid("BAR-FOO") == INFO_DATA
|
||||||
@@ -93,6 +97,7 @@ def test_export_db():
|
|||||||
assert db.get_stat_converted_for_file(filepath2) == (7, 8, 9)
|
assert db.get_stat_converted_for_file(filepath2) == (7, 8, 9)
|
||||||
assert db.get_stat_edited_for_file(filepath2) == (10, 11, 12)
|
assert db.get_stat_edited_for_file(filepath2) == (10, 11, 12)
|
||||||
assert sorted(db.get_previous_uuids()) == (["BAR-FOO", "FOO-BAR"])
|
assert sorted(db.get_previous_uuids()) == (["BAR-FOO", "FOO-BAR"])
|
||||||
|
assert db.get_metadata_for_file(filepath2) == METADATA_DATA
|
||||||
|
|
||||||
# close and re-open
|
# close and re-open
|
||||||
db.close()
|
db.close()
|
||||||
@@ -107,6 +112,8 @@ def test_export_db():
|
|||||||
assert db.get_stat_edited_for_file(filepath2) == (10, 11, 12)
|
assert db.get_stat_edited_for_file(filepath2) == (10, 11, 12)
|
||||||
assert sorted(db.get_previous_uuids()) == (["BAR-FOO", "FOO-BAR"])
|
assert sorted(db.get_previous_uuids()) == (["BAR-FOO", "FOO-BAR"])
|
||||||
assert json.loads(db.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
|
assert json.loads(db.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
|
||||||
|
assert db.get_metadata_for_file(filepath2) == METADATA_DATA
|
||||||
|
|
||||||
|
|
||||||
# update data
|
# update data
|
||||||
db.set_uuid_for_file(filepath, "FUBAR")
|
db.set_uuid_for_file(filepath, "FUBAR")
|
||||||
@@ -148,9 +155,10 @@ def test_export_db_no_op():
|
|||||||
db.set_sidecar_for_file(filepath, SIDECAR_DATA, (13, 14, 15))
|
db.set_sidecar_for_file(filepath, SIDECAR_DATA, (13, 14, 15))
|
||||||
assert db.get_sidecar_for_file(filepath) == (None, (None, None, None))
|
assert db.get_sidecar_for_file(filepath) == (None, (None, None, None))
|
||||||
assert db.get_previous_uuids() == []
|
assert db.get_previous_uuids() == []
|
||||||
|
|
||||||
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
|
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
|
||||||
assert db.get_detected_text_for_uuid("FOO-BAR") is None
|
assert db.get_detected_text_for_uuid("FOO-BAR") is None
|
||||||
|
db.set_metadata_for_file(filepath, METADATA_DATA)
|
||||||
|
assert db.get_metadata_for_file(filepath) is None
|
||||||
|
|
||||||
# test set_data which sets all at the same time
|
# test set_data which sets all at the same time
|
||||||
filepath2 = os.path.join(tempdir.name, "test2.jpg")
|
filepath2 = os.path.join(tempdir.name, "test2.jpg")
|
||||||
@@ -163,6 +171,7 @@ def test_export_db_no_op():
|
|||||||
(10, 11, 12),
|
(10, 11, 12),
|
||||||
INFO_DATA,
|
INFO_DATA,
|
||||||
EXIF_DATA,
|
EXIF_DATA,
|
||||||
|
METADATA_DATA,
|
||||||
)
|
)
|
||||||
assert db.get_uuid_for_file(filepath2) is None
|
assert db.get_uuid_for_file(filepath2) is None
|
||||||
assert db.get_info_for_uuid("BAR-FOO") is None
|
assert db.get_info_for_uuid("BAR-FOO") is None
|
||||||
@@ -172,6 +181,7 @@ def test_export_db_no_op():
|
|||||||
assert db.get_stat_converted_for_file(filepath) is None
|
assert db.get_stat_converted_for_file(filepath) is None
|
||||||
assert db.get_stat_edited_for_file(filepath) is None
|
assert db.get_stat_edited_for_file(filepath) is None
|
||||||
assert db.get_previous_uuids() == []
|
assert db.get_previous_uuids() == []
|
||||||
|
assert db.get_metadata_for_file(filepath) is None
|
||||||
|
|
||||||
# update data
|
# update data
|
||||||
db.set_uuid_for_file(filepath, "FUBAR")
|
db.set_uuid_for_file(filepath, "FUBAR")
|
||||||
@@ -207,7 +217,7 @@ def test_export_db_in_memory():
|
|||||||
db.set_sidecar_for_file(filepath, SIDECAR_DATA, (13, 14, 15))
|
db.set_sidecar_for_file(filepath, SIDECAR_DATA, (13, 14, 15))
|
||||||
assert db.get_previous_uuids() == ["FOO-BAR"]
|
assert db.get_previous_uuids() == ["FOO-BAR"]
|
||||||
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
|
db.set_detected_text_for_uuid("FOO-BAR", json.dumps([["foo", 0.5]]))
|
||||||
|
db.set_metadata_for_file(filepath, METADATA_DATA)
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
dbram = ExportDBInMemory(dbname, tempdir.name)
|
dbram = ExportDBInMemory(dbname, tempdir.name)
|
||||||
@@ -226,6 +236,7 @@ def test_export_db_in_memory():
|
|||||||
assert dbram.get_sidecar_for_file(filepath) == (SIDECAR_DATA, (13, 14, 15))
|
assert dbram.get_sidecar_for_file(filepath) == (SIDECAR_DATA, (13, 14, 15))
|
||||||
assert dbram.get_previous_uuids() == ["FOO-BAR"]
|
assert dbram.get_previous_uuids() == ["FOO-BAR"]
|
||||||
assert json.loads(dbram.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
|
assert json.loads(dbram.get_detected_text_for_uuid("FOO-BAR")) == [["foo", 0.5]]
|
||||||
|
assert dbram.get_metadata_for_file(filepath) == METADATA_DATA
|
||||||
|
|
||||||
# change a value
|
# change a value
|
||||||
dbram.set_uuid_for_file(filepath, "FUBAR")
|
dbram.set_uuid_for_file(filepath, "FUBAR")
|
||||||
@@ -237,6 +248,7 @@ def test_export_db_in_memory():
|
|||||||
dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
|
dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
|
||||||
dbram.set_sidecar_for_file(filepath, "FUBAR", (20, 21, 22))
|
dbram.set_sidecar_for_file(filepath, "FUBAR", (20, 21, 22))
|
||||||
dbram.set_detected_text_for_uuid("FUBAR", json.dumps([["bar", 0.5]]))
|
dbram.set_detected_text_for_uuid("FUBAR", json.dumps([["bar", 0.5]]))
|
||||||
|
dbram.set_metadata_for_file(filepath, "FUBAR")
|
||||||
|
|
||||||
assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
|
assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
|
||||||
assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
|
assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
|
||||||
@@ -248,6 +260,7 @@ def test_export_db_in_memory():
|
|||||||
assert dbram.get_sidecar_for_file(filepath) == ("FUBAR", (20, 21, 22))
|
assert dbram.get_sidecar_for_file(filepath) == ("FUBAR", (20, 21, 22))
|
||||||
assert dbram.get_previous_uuids() == ["FUBAR"]
|
assert dbram.get_previous_uuids() == ["FUBAR"]
|
||||||
assert json.loads(dbram.get_detected_text_for_uuid("FUBAR")) == [["bar", 0.5]]
|
assert json.loads(dbram.get_detected_text_for_uuid("FUBAR")) == [["bar", 0.5]]
|
||||||
|
assert dbram.get_metadata_for_file(filepath) == "FUBAR"
|
||||||
|
|
||||||
dbram.close()
|
dbram.close()
|
||||||
|
|
||||||
@@ -265,6 +278,7 @@ def test_export_db_in_memory():
|
|||||||
|
|
||||||
assert db.get_info_for_uuid("FUBAR") is None
|
assert db.get_info_for_uuid("FUBAR") is None
|
||||||
assert db.get_detected_text_for_uuid("FUBAR") is None
|
assert db.get_detected_text_for_uuid("FUBAR") is None
|
||||||
|
assert db.get_metadata_for_file(filepath) == METADATA_DATA
|
||||||
|
|
||||||
|
|
||||||
def test_export_db_in_memory_nofile():
|
def test_export_db_in_memory_nofile():
|
||||||
@@ -295,6 +309,7 @@ def test_export_db_in_memory_nofile():
|
|||||||
dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
|
dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
|
||||||
dbram.set_sidecar_for_file(filepath, "FUBAR", (20, 21, 22))
|
dbram.set_sidecar_for_file(filepath, "FUBAR", (20, 21, 22))
|
||||||
dbram.set_detected_text_for_uuid("FUBAR", json.dumps([["bar", 0.5]]))
|
dbram.set_detected_text_for_uuid("FUBAR", json.dumps([["bar", 0.5]]))
|
||||||
|
dbram.set_metadata_for_file(filepath, METADATA_DATA)
|
||||||
|
|
||||||
assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
|
assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
|
||||||
assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
|
assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
|
||||||
@@ -306,5 +321,6 @@ def test_export_db_in_memory_nofile():
|
|||||||
assert dbram.get_sidecar_for_file(filepath) == ("FUBAR", (20, 21, 22))
|
assert dbram.get_sidecar_for_file(filepath) == ("FUBAR", (20, 21, 22))
|
||||||
assert dbram.get_previous_uuids() == ["FUBAR"]
|
assert dbram.get_previous_uuids() == ["FUBAR"]
|
||||||
assert json.loads(dbram.get_detected_text_for_uuid("FUBAR")) == [["bar", 0.5]]
|
assert json.loads(dbram.get_detected_text_for_uuid("FUBAR")) == [["bar", 0.5]]
|
||||||
|
assert dbram.get_metadata_for_file(filepath) == METADATA_DATA
|
||||||
|
|
||||||
dbram.close()
|
dbram.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user