Initial implementation for issue #265
This commit is contained in:
@@ -1720,6 +1720,8 @@ def export(
|
|||||||
results_skipped = []
|
results_skipped = []
|
||||||
results_exif_updated = []
|
results_exif_updated = []
|
||||||
results_touched = []
|
results_touched = []
|
||||||
|
results_sidecar_json = []
|
||||||
|
results_sidecar_xmp = []
|
||||||
if verbose_:
|
if verbose_:
|
||||||
for p in photos:
|
for p in photos:
|
||||||
results = export_photo(
|
results = export_photo(
|
||||||
@@ -1762,6 +1764,8 @@ def export(
|
|||||||
results_skipped.extend(results.skipped)
|
results_skipped.extend(results.skipped)
|
||||||
results_exif_updated.extend(results.exif_updated)
|
results_exif_updated.extend(results.exif_updated)
|
||||||
results_touched.extend(results.touched)
|
results_touched.extend(results.touched)
|
||||||
|
results_sidecar_json.extend(results.sidecar_json)
|
||||||
|
results_sidecar_xmp.extend(results.sidecar_xmp)
|
||||||
|
|
||||||
# if convert_to_jpeg and p.isphoto and p.uti != "public.jpeg":
|
# if convert_to_jpeg and p.isphoto and p.uti != "public.jpeg":
|
||||||
# for photo_file in set(
|
# for photo_file in set(
|
||||||
@@ -1813,9 +1817,20 @@ def export(
|
|||||||
results_skipped.extend(results.skipped)
|
results_skipped.extend(results.skipped)
|
||||||
results_exif_updated.extend(results.exif_updated)
|
results_exif_updated.extend(results.exif_updated)
|
||||||
results_touched.extend(results.touched)
|
results_touched.extend(results.touched)
|
||||||
|
results_sidecar_json.extend(results.sidecar_json)
|
||||||
|
results_sidecar_xmp.extend(results.sidecar_xmp)
|
||||||
|
|
||||||
stop_time = time.perf_counter()
|
stop_time = time.perf_counter()
|
||||||
# print summary results
|
# print summary results
|
||||||
|
# print(f"results_exported: {results_exported}")
|
||||||
|
# print(f"results_new: {results_new}")
|
||||||
|
# print(f"results_updated: {results_updated}")
|
||||||
|
# print(f"results_skipped: {results_skipped}")
|
||||||
|
# print(f"results_exif_updated: {results_exif_updated}")
|
||||||
|
# print(f"results_touched: {results_touched}")
|
||||||
|
# print(f"results_sidecar_json: {results_sidecar_json}")
|
||||||
|
# print(f"results_sidecar_xmp: {results_sidecar_xmp}")
|
||||||
|
|
||||||
if update:
|
if update:
|
||||||
photo_str_new = "photos" if len(results_new) != 1 else "photo"
|
photo_str_new = "photos" if len(results_new) != 1 else "photo"
|
||||||
photo_str_updated = "photos" if len(results_updated) != 1 else "photo"
|
photo_str_updated = "photos" if len(results_updated) != 1 else "photo"
|
||||||
@@ -2364,19 +2379,19 @@ def export_photo(
|
|||||||
if photo.ismissing:
|
if photo.ismissing:
|
||||||
space = " " if not verbose_ else ""
|
space = " " if not verbose_ else ""
|
||||||
verbose(f"{space}Skipping missing photo {photo.original_filename}")
|
verbose(f"{space}Skipping missing photo {photo.original_filename}")
|
||||||
return ExportResults([], [], [], [], [], [])
|
return ExportResults([], [], [], [], [], [], [], [])
|
||||||
elif photo.path is None:
|
elif photo.path is None:
|
||||||
space = " " if not verbose_ else ""
|
space = " " if not verbose_ else ""
|
||||||
verbose(
|
verbose(
|
||||||
f"{space}WARNING: photo {photo.original_filename} ({photo.uuid}) is missing but ismissing=False, "
|
f"{space}WARNING: photo {photo.original_filename} ({photo.uuid}) is missing but ismissing=False, "
|
||||||
f"skipping {photo.original_filename}"
|
f"skipping {photo.original_filename}"
|
||||||
)
|
)
|
||||||
return ExportResults([], [], [], [], [], [])
|
return ExportResults([], [], [], [], [], [], [], [])
|
||||||
elif photo.ismissing and not photo.iscloudasset and not photo.incloud:
|
elif photo.ismissing and not photo.iscloudasset and not photo.incloud:
|
||||||
verbose(
|
verbose(
|
||||||
f"Skipping missing {photo.original_filename}: not iCloud asset or missing from cloud"
|
f"Skipping missing {photo.original_filename}: not iCloud asset or missing from cloud"
|
||||||
)
|
)
|
||||||
return ExportResults([], [], [], [], [], [])
|
return ExportResults([], [], [], [], [], [], [], [])
|
||||||
|
|
||||||
results_exported = []
|
results_exported = []
|
||||||
results_new = []
|
results_new = []
|
||||||
@@ -2384,6 +2399,8 @@ def export_photo(
|
|||||||
results_skipped = []
|
results_skipped = []
|
||||||
results_exif_updated = []
|
results_exif_updated = []
|
||||||
results_touched = []
|
results_touched = []
|
||||||
|
results_sidecar_json = []
|
||||||
|
results_sidecar_xmp = []
|
||||||
|
|
||||||
export_original = not (skip_original_if_edited and photo.hasadjustments)
|
export_original = not (skip_original_if_edited and photo.hasadjustments)
|
||||||
|
|
||||||
@@ -2459,6 +2476,7 @@ def export_photo(
|
|||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
|
verbose=verbose,
|
||||||
)
|
)
|
||||||
|
|
||||||
results_exported.extend(export_results.exported)
|
results_exported.extend(export_results.exported)
|
||||||
@@ -2467,6 +2485,8 @@ def export_photo(
|
|||||||
results_skipped.extend(export_results.skipped)
|
results_skipped.extend(export_results.skipped)
|
||||||
results_exif_updated.extend(export_results.exif_updated)
|
results_exif_updated.extend(export_results.exif_updated)
|
||||||
results_touched.extend(export_results.touched)
|
results_touched.extend(export_results.touched)
|
||||||
|
results_sidecar_json.extend(export_results.sidecar_json)
|
||||||
|
results_sidecar_xmp.extend(export_results.sidecar_xmp)
|
||||||
|
|
||||||
if verbose_:
|
if verbose_:
|
||||||
for exported in export_results.exported:
|
for exported in export_results.exported:
|
||||||
@@ -2522,6 +2542,7 @@ def export_photo(
|
|||||||
jpeg_quality=jpeg_quality,
|
jpeg_quality=jpeg_quality,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
use_photokit=use_photokit,
|
use_photokit=use_photokit,
|
||||||
|
verbose=verbose,
|
||||||
)
|
)
|
||||||
|
|
||||||
results_exported.extend(export_results_edited.exported)
|
results_exported.extend(export_results_edited.exported)
|
||||||
@@ -2530,6 +2551,8 @@ def export_photo(
|
|||||||
results_skipped.extend(export_results_edited.skipped)
|
results_skipped.extend(export_results_edited.skipped)
|
||||||
results_exif_updated.extend(export_results_edited.exif_updated)
|
results_exif_updated.extend(export_results_edited.exif_updated)
|
||||||
results_touched.extend(export_results_edited.touched)
|
results_touched.extend(export_results_edited.touched)
|
||||||
|
results_sidecar_json.extend(export_results_edited.sidecar_json)
|
||||||
|
results_sidecar_xmp.extend(export_results_edited.sidecar_xmp)
|
||||||
|
|
||||||
if verbose_:
|
if verbose_:
|
||||||
for exported in export_results_edited.exported:
|
for exported in export_results_edited.exported:
|
||||||
@@ -2550,6 +2573,8 @@ def export_photo(
|
|||||||
results_skipped,
|
results_skipped,
|
||||||
results_exif_updated,
|
results_exif_updated,
|
||||||
results_touched,
|
results_touched,
|
||||||
|
results_sidecar_json,
|
||||||
|
results_sidecar_xmp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2579,7 +2604,11 @@ def get_filenames_from_template(photo, filename_template, original_name):
|
|||||||
)
|
)
|
||||||
filenames = [f"{file_}{photo_ext}" for file_ in filenames]
|
filenames = [f"{file_}{photo_ext}" for file_ in filenames]
|
||||||
else:
|
else:
|
||||||
filenames = [photo.original_filename] if (original_name and (photo.original_filename is not None)) else [photo.filename]
|
filenames = (
|
||||||
|
[photo.original_filename]
|
||||||
|
if (original_name and (photo.original_filename is not None))
|
||||||
|
else [photo.filename]
|
||||||
|
)
|
||||||
|
|
||||||
filenames = [sanitize_filename(filename) for filename in filenames]
|
filenames = [sanitize_filename(filename) for filename in filenames]
|
||||||
return filenames
|
return filenames
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from sqlite3 import Error
|
|||||||
|
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
|
|
||||||
OSXPHOTOS_EXPORTDB_VERSION = "2.0"
|
OSXPHOTOS_EXPORTDB_VERSION = "3.0"
|
||||||
|
|
||||||
|
|
||||||
class ExportDB_ABC(ABC):
|
class ExportDB_ABC(ABC):
|
||||||
@@ -76,6 +76,22 @@ class ExportDB_ABC(ABC):
|
|||||||
def set_exifdata_for_file(self, uuid, exifdata):
|
def set_exifdata_for_file(self, uuid, exifdata):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_exiftool_json_sidecar_for_file(self, filename, stats):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_exiftool_json_sidecar_for_file(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_xmp_sidecar_for_file(self, filename, stats):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_xmp_sidecar_for_file(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def set_data(
|
def set_data(
|
||||||
self,
|
self,
|
||||||
@@ -87,6 +103,8 @@ class ExportDB_ABC(ABC):
|
|||||||
edited_stat,
|
edited_stat,
|
||||||
info_json,
|
info_json,
|
||||||
exif_json,
|
exif_json,
|
||||||
|
exiftool_json_sidecar,
|
||||||
|
xmp_sidecar,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -141,6 +159,18 @@ class ExportDBNoOp(ExportDB_ABC):
|
|||||||
def set_exifdata_for_file(self, uuid, exifdata):
|
def set_exifdata_for_file(self, uuid, exifdata):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def set_exiftool_json_sidecar_for_file(self, filename, stats):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_exiftool_json_sidecar_for_file(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_xmp_sidecar_for_file(self, filename, stats):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_xmp_sidecar_for_file(self, filename):
|
||||||
|
pass
|
||||||
|
|
||||||
def set_data(
|
def set_data(
|
||||||
self,
|
self,
|
||||||
filename,
|
filename,
|
||||||
@@ -151,6 +181,8 @@ class ExportDBNoOp(ExportDB_ABC):
|
|||||||
edited_stat,
|
edited_stat,
|
||||||
info_json,
|
info_json,
|
||||||
exif_json,
|
exif_json,
|
||||||
|
exiftool_json_sidecar,
|
||||||
|
xmp_sidecar,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -379,6 +411,70 @@ class ExportDB(ExportDB_ABC):
|
|||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
|
|
||||||
|
def get_exiftool_json_sidecar_for_file(self, filename):
|
||||||
|
""" returns the exiftool JSON sidecar data for a file """
|
||||||
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
|
conn = self._conn
|
||||||
|
try:
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"SELECT sidecar_data FROM exiftool_json_sidecar WHERE filepath_normalized = ?",
|
||||||
|
(filename,),
|
||||||
|
)
|
||||||
|
results = c.fetchone()
|
||||||
|
sidecar_data = results[0] if results else None
|
||||||
|
except Error as e:
|
||||||
|
logging.warning(e)
|
||||||
|
sidecar_data = None
|
||||||
|
|
||||||
|
return sidecar_data
|
||||||
|
|
||||||
|
def set_exiftool_json_sidecar_for_file(self, filename, sidecar_data):
|
||||||
|
""" sets the exiftool JSON sidecar data for a file """
|
||||||
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
|
conn = self._conn
|
||||||
|
try:
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO exiftool_json_sidecar(filepath_normalized, sidecar_data) VALUES (?, ?);",
|
||||||
|
(filename, sidecar_data),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Error as e:
|
||||||
|
logging.warning(e)
|
||||||
|
|
||||||
|
def get_xmp_sidecar_for_file(self, filename):
|
||||||
|
""" returns the XMP sidecar data for a file """
|
||||||
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
|
conn = self._conn
|
||||||
|
try:
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"SELECT sidecar_data FROM xmp_sidecar WHERE filepath_normalized = ?",
|
||||||
|
(filename,),
|
||||||
|
)
|
||||||
|
results = c.fetchone()
|
||||||
|
sidecar_data = results[0] if results else None
|
||||||
|
except Error as e:
|
||||||
|
logging.warning(e)
|
||||||
|
sidecar_data = None
|
||||||
|
|
||||||
|
return sidecar_data
|
||||||
|
|
||||||
|
def set_xmp_sidecar_for_file(self, filename, sidecar_data):
|
||||||
|
""" sets the XMP sidecar data for a file """
|
||||||
|
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
||||||
|
conn = self._conn
|
||||||
|
try:
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO xmp_sidecar(filepath_normalized, sidecar_data) VALUES (?, ?);",
|
||||||
|
(filename, sidecar_data),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
except Error as e:
|
||||||
|
logging.warning(e)
|
||||||
|
|
||||||
def set_data(
|
def set_data(
|
||||||
self,
|
self,
|
||||||
filename,
|
filename,
|
||||||
@@ -389,6 +485,8 @@ class ExportDB(ExportDB_ABC):
|
|||||||
edited_stat,
|
edited_stat,
|
||||||
info_json,
|
info_json,
|
||||||
exif_json,
|
exif_json,
|
||||||
|
exiftool_json_sidecar,
|
||||||
|
xmp_sidecar,
|
||||||
):
|
):
|
||||||
""" sets all the data for file and uuid at once
|
""" sets all the data for file and uuid at once
|
||||||
"""
|
"""
|
||||||
@@ -429,6 +527,14 @@ 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),
|
||||||
)
|
)
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO exiftool_json_sidecar(filepath_normalized, sidecar_data) VALUES (?, ?);",
|
||||||
|
(filename_normalized, exiftool_json_sidecar),
|
||||||
|
)
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO xmp_sidecar(filepath_normalized, sidecar_data) VALUES (?, ?);",
|
||||||
|
(filename_normalized, xmp_sidecar),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
@@ -479,13 +585,11 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
if not os.path.isfile(dbfile):
|
if not os.path.isfile(dbfile):
|
||||||
conn = self._get_db_connection(dbfile)
|
conn = self._get_db_connection(dbfile)
|
||||||
if conn:
|
if not conn:
|
||||||
self._create_db_tables(conn)
|
|
||||||
self.was_created = True
|
|
||||||
self.was_upgraded = ()
|
|
||||||
self.version = OSXPHOTOS_EXPORTDB_VERSION
|
|
||||||
else:
|
|
||||||
raise Exception("Error getting connection to database {dbfile}")
|
raise Exception("Error getting connection to database {dbfile}")
|
||||||
|
self._create_db_tables(conn)
|
||||||
|
self.was_created = True
|
||||||
|
self.was_upgraded = ()
|
||||||
else:
|
else:
|
||||||
conn = self._get_db_connection(dbfile)
|
conn = self._get_db_connection(dbfile)
|
||||||
self.was_created = False
|
self.was_created = False
|
||||||
@@ -495,8 +599,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
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 = ()
|
||||||
self.version = OSXPHOTOS_EXPORTDB_VERSION
|
self.version = OSXPHOTOS_EXPORTDB_VERSION
|
||||||
|
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
def _get_db_connection(self, dbfile):
|
def _get_db_connection(self, dbfile):
|
||||||
@@ -570,11 +673,23 @@ class ExportDB(ExportDB_ABC):
|
|||||||
size INTEGER,
|
size INTEGER,
|
||||||
mtime REAL
|
mtime REAL
|
||||||
); """,
|
); """,
|
||||||
|
"sql_xmp_table": """ CREATE TABLE IF NOT EXISTS xmp_sidecar (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
filepath_normalized TEXT NOT NULL,
|
||||||
|
sidecar_data TEXT
|
||||||
|
); """,
|
||||||
|
"sql_exiftool_json_table": """ CREATE TABLE IF NOT EXISTS exiftool_json_sidecar (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
filepath_normalized TEXT NOT NULL,
|
||||||
|
sidecar_data JSON
|
||||||
|
); """,
|
||||||
"sql_files_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """,
|
"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_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_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_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_converted_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""",
|
||||||
|
"sql_xmp_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_xmp_filename on xmp_sidecar (filepath_normalized);""",
|
||||||
|
"sql_exiftool_json_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_exiftool_json_filename on exiftool_json_sidecar (filepath_normalized);""",
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
# TODO: should this be its own PhotoExporter class?
|
# TODO: should this be its own PhotoExporter class?
|
||||||
|
|
||||||
import glob
|
import glob
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -41,14 +42,22 @@ from ..photokit import (
|
|||||||
PhotoLibrary,
|
PhotoLibrary,
|
||||||
PhotoKitFetchFailed,
|
PhotoKitFetchFailed,
|
||||||
)
|
)
|
||||||
from ..utils import dd_to_dms_str, findfiles
|
from ..utils import dd_to_dms_str, findfiles, noop
|
||||||
|
|
||||||
ExportResults = namedtuple(
|
ExportResults = namedtuple(
|
||||||
"ExportResults",
|
"ExportResults",
|
||||||
["exported", "new", "updated", "skipped", "exif_updated", "touched"],
|
["exported", "new", "updated", "skipped", "exif_updated", "touched", "sidecar_json", "sidecar_xmp"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# hexdigest is not a class method, don't import this into PhotoInfo
|
||||||
|
def hexdigest(strval):
|
||||||
|
""" hexdigest of a string, using blake2b """
|
||||||
|
h = hashlib.blake2b(digest_size=20)
|
||||||
|
h.update(bytes(strval, "utf-8"))
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
# _export_photo_uuid_applescript is not a class method, don't import this into PhotoInfo
|
# _export_photo_uuid_applescript is not a class method, don't import this into PhotoInfo
|
||||||
def _export_photo_uuid_applescript(
|
def _export_photo_uuid_applescript(
|
||||||
uuid,
|
uuid,
|
||||||
@@ -321,6 +330,7 @@ def export2(
|
|||||||
jpeg_quality=1.0,
|
jpeg_quality=1.0,
|
||||||
ignore_date_modified=False,
|
ignore_date_modified=False,
|
||||||
use_photokit=False,
|
use_photokit=False,
|
||||||
|
verbose=None,
|
||||||
):
|
):
|
||||||
""" export photo, like export but with update and dry_run options
|
""" export photo, like export but with update and dry_run options
|
||||||
dest: must be valid destination path or exception raised
|
dest: must be valid destination path or exception raised
|
||||||
@@ -364,6 +374,7 @@ def export2(
|
|||||||
convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg
|
convert_to_jpeg: boolean; if True, converts non-jpeg images to jpeg
|
||||||
jpeg_quality: float in range 0.0 <= jpeg_quality <= 1.0. A value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
|
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: for use with sidecar and exiftool; if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
ignore_date_modified: for use with sidecar and exiftool; if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||||
|
verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.
|
||||||
|
|
||||||
Returns: ExportResults namedtuple with fields: exported, new, updated, skipped
|
Returns: ExportResults namedtuple with fields: exported, new, updated, skipped
|
||||||
where each field is a list of file paths
|
where each field is a list of file paths
|
||||||
@@ -380,6 +391,12 @@ def export2(
|
|||||||
if export_db is None:
|
if export_db is None:
|
||||||
export_db = ExportDBNoOp()
|
export_db = ExportDBNoOp()
|
||||||
|
|
||||||
|
if verbose is None:
|
||||||
|
verbose = noop
|
||||||
|
elif not callable(verbose):
|
||||||
|
raise TypeError("verbose must be callable")
|
||||||
|
self._verbose = verbose
|
||||||
|
|
||||||
# suffix to add to edited files
|
# suffix to add to edited files
|
||||||
# e.g. name will be filename_edited.jpg
|
# e.g. name will be filename_edited.jpg
|
||||||
edited_identifier = "_edited"
|
edited_identifier = "_edited"
|
||||||
@@ -501,14 +518,16 @@ def export2(
|
|||||||
# might be exporting into a pre-ExportDB folder or the DB got deleted
|
# might be exporting into a pre-ExportDB folder or the DB got deleted
|
||||||
dest_uuid = self.uuid
|
dest_uuid = self.uuid
|
||||||
export_db.set_data(
|
export_db.set_data(
|
||||||
dest,
|
filename=dest,
|
||||||
self.uuid,
|
uuid=self.uuid,
|
||||||
fileutil.file_sig(dest),
|
orig_stat=fileutil.file_sig(dest),
|
||||||
(None, None, None),
|
exif_stat=(None, None, None),
|
||||||
(None, None, None),
|
converted_stat=(None, None, None),
|
||||||
(None, None, None),
|
edited_stat=(None, None, None),
|
||||||
self.json(),
|
info_json=self.json(),
|
||||||
None,
|
exif_json=None,
|
||||||
|
exiftool_json_sidecar=None,
|
||||||
|
xmp_sidecar=None,
|
||||||
)
|
)
|
||||||
if dest_uuid != self.uuid:
|
if dest_uuid != self.uuid:
|
||||||
# not the right file, find the right one
|
# not the right file, find the right one
|
||||||
@@ -527,14 +546,16 @@ def export2(
|
|||||||
dest = pathlib.Path(file_)
|
dest = pathlib.Path(file_)
|
||||||
found_match = True
|
found_match = True
|
||||||
export_db.set_data(
|
export_db.set_data(
|
||||||
dest,
|
filename=dest,
|
||||||
self.uuid,
|
uuid=self.uuid,
|
||||||
fileutil.file_sig(dest),
|
orig_stat=fileutil.file_sig(dest),
|
||||||
(None, None, None),
|
exif_stat=(None, None, None),
|
||||||
(None, None, None),
|
converted_stat=(None, None, None),
|
||||||
(None, None, None),
|
edited_stat=(None, None, None),
|
||||||
self.json(),
|
info_json=self.json(),
|
||||||
None,
|
exif_json=None,
|
||||||
|
exiftool_json_sidecar=None,
|
||||||
|
xmp_sidecar=None,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -722,8 +743,10 @@ def export2(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# export metadata
|
# export metadata
|
||||||
|
sidecar_json_files = []
|
||||||
if sidecar_json:
|
if sidecar_json:
|
||||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
|
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
|
||||||
|
sidecar_json_files.append(str(sidecar_filename))
|
||||||
sidecar_str = self._exiftool_json_sidecar(
|
sidecar_str = self._exiftool_json_sidecar(
|
||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
@@ -731,15 +754,29 @@ def export2(
|
|||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
ignore_date_modified=ignore_date_modified,
|
ignore_date_modified=ignore_date_modified,
|
||||||
)
|
)
|
||||||
if not dry_run:
|
sidecar_digest = hexdigest(sidecar_str)
|
||||||
try:
|
old_sidecar_digest = export_db.get_exiftool_json_sidecar_for_file(
|
||||||
|
sidecar_filename
|
||||||
|
)
|
||||||
|
write_sidecar = (
|
||||||
|
not update
|
||||||
|
or (update and not sidecar_filename.exists())
|
||||||
|
or (update and sidecar_digest != old_sidecar_digest)
|
||||||
|
)
|
||||||
|
if write_sidecar:
|
||||||
|
verbose(f"Writing exiftool JSON sidecar {sidecar_filename}")
|
||||||
|
if not dry_run:
|
||||||
self._write_sidecar(sidecar_filename, sidecar_str)
|
self._write_sidecar(sidecar_filename, sidecar_str)
|
||||||
except Exception as e:
|
export_db.set_exiftool_json_sidecar_for_file(
|
||||||
logging.warning(f"Error writing json sidecar to {sidecar_filename}")
|
sidecar_filename, sidecar_digest
|
||||||
raise e
|
)
|
||||||
|
else:
|
||||||
|
verbose(f"Skipped up to date exiftool JSON sidecar {sidecar_filename}")
|
||||||
|
|
||||||
|
sidecar_xmp_files = []
|
||||||
if sidecar_xmp:
|
if sidecar_xmp:
|
||||||
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.xmp")
|
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.xmp")
|
||||||
|
sidecar_xmp_files.append(str(sidecar_filename))
|
||||||
sidecar_str = self._xmp_sidecar(
|
sidecar_str = self._xmp_sidecar(
|
||||||
use_albums_as_keywords=use_albums_as_keywords,
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
use_persons_as_keywords=use_persons_as_keywords,
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
@@ -747,12 +784,20 @@ def export2(
|
|||||||
description_template=description_template,
|
description_template=description_template,
|
||||||
extension=dest.suffix[1:] if dest.suffix else None,
|
extension=dest.suffix[1:] if dest.suffix else None,
|
||||||
)
|
)
|
||||||
if not dry_run:
|
sidecar_digest = hexdigest(sidecar_str)
|
||||||
try:
|
old_sidecar_digest = export_db.get_xmp_sidecar_for_file(sidecar_filename)
|
||||||
|
write_sidecar = (
|
||||||
|
not update
|
||||||
|
or (update and not sidecar_filename.exists())
|
||||||
|
or (update and sidecar_digest != old_sidecar_digest)
|
||||||
|
)
|
||||||
|
if write_sidecar:
|
||||||
|
verbose(f"Writing XMP sidecar {sidecar_filename}")
|
||||||
|
if not dry_run:
|
||||||
self._write_sidecar(sidecar_filename, sidecar_str)
|
self._write_sidecar(sidecar_filename, sidecar_str)
|
||||||
except Exception as e:
|
export_db.set_xmp_sidecar_for_file(sidecar_filename, sidecar_digest)
|
||||||
logging.warning(f"Error writing xmp sidecar to {sidecar_filename}")
|
else:
|
||||||
raise e
|
verbose(f"Skipped up to date XMP sidecar {sidecar_filename}")
|
||||||
|
|
||||||
# if exiftool, write the metadata
|
# if exiftool, write the metadata
|
||||||
if update:
|
if update:
|
||||||
@@ -782,6 +827,7 @@ def export2(
|
|||||||
if old_data is None or files_are_different:
|
if old_data is None or files_are_different:
|
||||||
# didn't have old data, assume we need to write it
|
# didn't have old data, assume we need to write it
|
||||||
# or files were different
|
# or files were different
|
||||||
|
verbose(f"Writing metadata with exiftool for {exported_file}")
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
self._write_exif_data(
|
self._write_exif_data(
|
||||||
exported_file,
|
exported_file,
|
||||||
@@ -805,8 +851,11 @@ def export2(
|
|||||||
exported_file, fileutil.file_sig(exported_file)
|
exported_file, fileutil.file_sig(exported_file)
|
||||||
)
|
)
|
||||||
exif_files_updated.append(exported_file)
|
exif_files_updated.append(exported_file)
|
||||||
|
else:
|
||||||
|
verbose(f"Skipped up to date exiftool metadata for {exported_file}")
|
||||||
elif exiftool and exif_files:
|
elif exiftool and exif_files:
|
||||||
for exported_file in exif_files:
|
for exported_file in exif_files:
|
||||||
|
verbose(f"Writing metadata with exiftool for {exported_file}")
|
||||||
if not dry_run:
|
if not dry_run:
|
||||||
self._write_exif_data(
|
self._write_exif_data(
|
||||||
exported_file,
|
exported_file,
|
||||||
@@ -834,6 +883,7 @@ def export2(
|
|||||||
|
|
||||||
if touch_file:
|
if touch_file:
|
||||||
for exif_file in exif_files_updated:
|
for exif_file in exif_files_updated:
|
||||||
|
verbose(f"Updating file modification time for {exif_file}")
|
||||||
touched_files.append(exif_file)
|
touched_files.append(exif_file)
|
||||||
ts = int(self.date.timestamp())
|
ts = int(self.date.timestamp())
|
||||||
fileutil.utime(exif_file, (ts, ts))
|
fileutil.utime(exif_file, (ts, ts))
|
||||||
@@ -847,6 +897,8 @@ def export2(
|
|||||||
update_skipped_files,
|
update_skipped_files,
|
||||||
exif_files_updated,
|
exif_files_updated,
|
||||||
touched_files,
|
touched_files,
|
||||||
|
sidecar_json_files,
|
||||||
|
sidecar_xmp_files,
|
||||||
)
|
)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -996,14 +1048,16 @@ def _export_photo(
|
|||||||
fileutil.copy(src, dest_str, norsrc=no_xattr)
|
fileutil.copy(src, dest_str, norsrc=no_xattr)
|
||||||
|
|
||||||
export_db.set_data(
|
export_db.set_data(
|
||||||
dest_str,
|
filename=dest_str,
|
||||||
self.uuid,
|
uuid=self.uuid,
|
||||||
fileutil.file_sig(dest_str),
|
orig_stat=fileutil.file_sig(dest_str),
|
||||||
(None, None, None),
|
exif_stat=(None, None, None),
|
||||||
converted_stat,
|
converted_stat=converted_stat,
|
||||||
edited_stat,
|
edited_stat=edited_stat,
|
||||||
self.json(),
|
info_json=self.json(),
|
||||||
None,
|
exif_json=None,
|
||||||
|
exiftool_json_sidecar=None,
|
||||||
|
xmp_sidecar=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if touched_files:
|
if touched_files:
|
||||||
@@ -1017,6 +1071,8 @@ def _export_photo(
|
|||||||
update_skipped_files,
|
update_skipped_files,
|
||||||
[],
|
[],
|
||||||
touched_files,
|
touched_files,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1087,9 +1143,9 @@ def _exiftool_dict(
|
|||||||
IPTC:Keywords (may include album name, person name, or template)
|
IPTC:Keywords (may include album name, person name, or template)
|
||||||
XMP:Subject
|
XMP:Subject
|
||||||
XMP:PersonInImage
|
XMP:PersonInImage
|
||||||
|
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
|
||||||
EXIF:GPSLatitude, EXIF:GPSLongitude
|
EXIF:GPSLatitude, EXIF:GPSLongitude
|
||||||
EXIF:GPSPosition
|
EXIF:GPSPosition
|
||||||
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
|
|
||||||
EXIF:DateTimeOriginal
|
EXIF:DateTimeOriginal
|
||||||
EXIF:OffsetTimeOriginal
|
EXIF:OffsetTimeOriginal
|
||||||
EXIF:ModifyDate
|
EXIF:ModifyDate
|
||||||
@@ -1249,9 +1305,9 @@ def _exiftool_json_sidecar(
|
|||||||
IPTC:Keywords (may include album name, person name, or template)
|
IPTC:Keywords (may include album name, person name, or template)
|
||||||
XMP:Subject
|
XMP:Subject
|
||||||
XMP:PersonInImage
|
XMP:PersonInImage
|
||||||
|
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
|
||||||
EXIF:GPSLatitude, EXIF:GPSLongitude
|
EXIF:GPSLatitude, EXIF:GPSLongitude
|
||||||
EXIF:GPSPosition
|
EXIF:GPSPosition
|
||||||
EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
|
|
||||||
EXIF:DateTimeOriginal
|
EXIF:DateTimeOriginal
|
||||||
EXIF:OffsetTimeOriginal
|
EXIF:OffsetTimeOriginal
|
||||||
EXIF:ModifyDate
|
EXIF:ModifyDate
|
||||||
|
|||||||
@@ -4,6 +4,52 @@ 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"}}"""
|
||||||
|
XMP_DATA = """<!-- Created with osxphotos https://github.com/RhetTbull/osxphotos -->
|
||||||
|
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0">
|
||||||
|
<!-- mirrors Photos 5 "Export IPTC as XMP" option -->
|
||||||
|
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||||
|
<photoshop:SidecarForExtension>jpg</photoshop:SidecarForExtension>
|
||||||
|
<dc:description>Bride Wedding day</dc:description>
|
||||||
|
<dc:title></dc:title>
|
||||||
|
<!-- keywords and persons listed in <dc:subject> as Photos does -->
|
||||||
|
<dc:subject>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>wedding</rdf:li>
|
||||||
|
<rdf:li>Maria</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</dc:subject>
|
||||||
|
<photoshop:DateCreated>2019-04-15T14:40:24.086000-04:00</photoshop:DateCreated>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||||
|
<Iptc4xmpExt:PersonInImage>
|
||||||
|
<rdf:Bag>
|
||||||
|
<rdf:li>Maria</rdf:li>
|
||||||
|
</rdf:Bag>
|
||||||
|
</Iptc4xmpExt:PersonInImage>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||||
|
<digiKam:TagsList>
|
||||||
|
<rdf:Seq>
|
||||||
|
<rdf:li>wedding</rdf:li>
|
||||||
|
</rdf:Seq>
|
||||||
|
</digiKam:TagsList>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||||
|
<xmp:CreateDate>2019-04-15T14:40:24</xmp:CreateDate>
|
||||||
|
<xmp:ModifyDate>2019-04-15T14:40:24</xmp:ModifyDate>
|
||||||
|
</rdf:Description>
|
||||||
|
<rdf:Description rdf:about=""
|
||||||
|
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||||
|
</rdf:Description>
|
||||||
|
</rdf:RDF>
|
||||||
|
</x:xmpmeta>"""
|
||||||
|
|
||||||
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"}}"""
|
||||||
DATABASE_VERSION1 = "tests/export_db_version1.db"
|
DATABASE_VERSION1 = "tests/export_db_version1.db"
|
||||||
@@ -41,6 +87,10 @@ def test_export_db():
|
|||||||
assert db.get_stat_edited_for_file(filepath) == (10, 11, 12)
|
assert db.get_stat_edited_for_file(filepath) == (10, 11, 12)
|
||||||
db.set_stat_converted_for_file(filepath, (7, 8, 9))
|
db.set_stat_converted_for_file(filepath, (7, 8, 9))
|
||||||
assert db.get_stat_converted_for_file(filepath) == (7, 8, 9)
|
assert db.get_stat_converted_for_file(filepath) == (7, 8, 9)
|
||||||
|
db.set_exiftool_json_sidecar_for_file(filepath, EXIF_DATA)
|
||||||
|
assert db.get_exiftool_json_sidecar_for_file(filepath) == EXIF_DATA
|
||||||
|
db.set_xmp_sidecar_for_file(filepath, XMP_DATA)
|
||||||
|
assert db.get_xmp_sidecar_for_file(filepath) == XMP_DATA
|
||||||
|
|
||||||
# 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")
|
||||||
@@ -53,6 +103,8 @@ def test_export_db():
|
|||||||
(10, 11, 12),
|
(10, 11, 12),
|
||||||
INFO_DATA,
|
INFO_DATA,
|
||||||
EXIF_DATA,
|
EXIF_DATA,
|
||||||
|
EXIF_DATA,
|
||||||
|
XMP_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
|
||||||
@@ -61,6 +113,8 @@ def test_export_db():
|
|||||||
assert db.get_stat_exif_for_file(filepath2) == (4, 5, 6)
|
assert db.get_stat_exif_for_file(filepath2) == (4, 5, 6)
|
||||||
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 db.get_exiftool_json_sidecar_for_file(filepath2) == EXIF_DATA
|
||||||
|
assert db.get_xmp_sidecar_for_file(filepath2) == XMP_DATA
|
||||||
|
|
||||||
# close and re-open
|
# close and re-open
|
||||||
db.close()
|
db.close()
|
||||||
@@ -73,6 +127,8 @@ def test_export_db():
|
|||||||
assert db.get_stat_exif_for_file(filepath2) == (4, 5, 6)
|
assert db.get_stat_exif_for_file(filepath2) == (4, 5, 6)
|
||||||
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 db.get_exiftool_json_sidecar_for_file(filepath2) == EXIF_DATA
|
||||||
|
assert db.get_xmp_sidecar_for_file(filepath2) == XMP_DATA
|
||||||
|
|
||||||
# update data
|
# update data
|
||||||
db.set_uuid_for_file(filepath, "FUBAR")
|
db.set_uuid_for_file(filepath, "FUBAR")
|
||||||
@@ -109,6 +165,10 @@ 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
|
||||||
db.set_stat_edited_for_file(filepath, (10, 11, 12))
|
db.set_stat_edited_for_file(filepath, (10, 11, 12))
|
||||||
assert db.get_stat_edited_for_file(filepath) is None
|
assert db.get_stat_edited_for_file(filepath) is None
|
||||||
|
db.set_exiftool_json_sidecar_for_file(filepath, EXIF_DATA)
|
||||||
|
assert db.get_exiftool_json_sidecar_for_file(filepath) is None
|
||||||
|
db.set_xmp_sidecar_for_file(filepath, XMP_DATA)
|
||||||
|
assert db.get_xmp_sidecar_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")
|
||||||
@@ -121,6 +181,8 @@ def test_export_db_no_op():
|
|||||||
(10, 11, 12),
|
(10, 11, 12),
|
||||||
INFO_DATA,
|
INFO_DATA,
|
||||||
EXIF_DATA,
|
EXIF_DATA,
|
||||||
|
EXIF_DATA,
|
||||||
|
(16, 17, 18),
|
||||||
)
|
)
|
||||||
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
|
||||||
@@ -129,6 +191,8 @@ def test_export_db_no_op():
|
|||||||
assert db.get_stat_exif_for_file(filepath2) is None
|
assert db.get_stat_exif_for_file(filepath2) is None
|
||||||
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_exiftool_json_sidecar_for_file(filepath) is None
|
||||||
|
assert db.get_xmp_sidecar_for_file(filepath) is None
|
||||||
|
|
||||||
# update data
|
# update data
|
||||||
db.set_uuid_for_file(filepath, "FUBAR")
|
db.set_uuid_for_file(filepath, "FUBAR")
|
||||||
|
|||||||
Reference in New Issue
Block a user