diff --git a/README.md b/README.md
index b453d632..176e55d5 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@
+ [ScoreInfo](#scoreinfo)
+ [PersonInfo](#personinfo)
+ [FaceInfo](#faceinfo)
+ + [Raw Photos](#raw-photos)
+ [Template Substitutions](#template-substitutions)
+ [Utility Functions](#utility-functions)
* [Examples](#examples)
@@ -114,7 +115,7 @@ Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
for photos matching all options). If no query options are provided, all
photos will be exported. By default, all versions of all photos will be
exported including edited versions, live photo movies, burst photos, and
- associated RAW images. See --skip-edited, --skip-live, --skip-bursts, and
+ associated raw images. See --skip-edited, --skip-live, --skip-bursts, and
--skip-raw options to modify this behavior.
Options:
@@ -199,7 +200,7 @@ Options:
--not-selfie Search for photos that are not selfies.
--panorama Search for panorama photos.
--not-panorama Search for photos that are not panoramas.
- --has-raw Search for photos with both a jpeg and RAW
+ --has-raw Search for photos with both a jpeg and raw
version
--only-movies Search only for movies (default searches
both images and movies).
@@ -242,10 +243,10 @@ Options:
the library if a photo is a burst photo.
--skip-live Do not export the associated live video
component of a live photo.
- --skip-raw Do not export associated RAW images of a
- RAW/jpeg pair. Note: this does not skip RAW
- photos if the RAW photo does not have an
- associated jpeg image (e.g. the RAW file was
+ --skip-raw Do not export associated raw images of a
+ RAW+JPEG pair. Note: this does not skip raw
+ photos if the raw photo does not have an
+ associated jpeg image (e.g. the raw file was
imported to Photos without a jpeg preview).
--person-keyword Use person in image as keyword/tag when
exporting metadata.
@@ -279,6 +280,13 @@ Options:
renamed upon import. By default, photos are
exported with the the original name they had
before import.
+ --convert-to-jpeg Convert all non-jpeg images (e.g. raw, HEIC,
+ PNG, etc) to JPEG upon export. Only works
+ if your Mac has a GPU.
+ --jpeg-quality FLOAT RANGE Value in range 0.0 to 1.0 to use with
+ --convert-to-jpeg. A value of 1.0 specifies
+ best quality, a value of 0.0 specifies
+ maximum compression. Defaults to 1.0.
--sidecar FORMAT Create sidecar for each photo exported;
valid FORMAT values: xmp, json; --sidecar
json: create JSON sidecar useable by
@@ -347,7 +355,7 @@ If using --update, the exported library should be treated as a backup, not a
working copy where you intend to make changes.
Note: The number of files reported for export and the number actually exported
-may differ due to live photos, associated RAW images, and edited photos which
+may differ due to live photos, associated raw images, and edited photos which
are reported in the total photos exported.
Implementation note: To determine which files need to be updated, osxphotos
@@ -1084,6 +1092,18 @@ Returns the absolute path to the edited photo on disk as a string. If the photo
**Note**: will also return None if the edited photo is missing on disk.
+#### `path_raw`
+Returns the absolute path to the associated raw photo on disk as a string, if photo is part of a RAW+JPEG pair, otherwise returns None. See [notes on Raw Photos](#raw-photos).
+
+#### `has_raw`
+Returns True if photo has an associated raw image, otherwise False. (e.g. Photo is a RAW+JPEG pair). See also [is_raw](#israw) and [notes on Raw Photos](#raw-photos).
+
+#### `israw`
+Returns True if photo is a raw image. E.g. it was imported as a single raw image, not part of a RAW+JPEG pair. See also [has_raw](#has_raw) and .
+
+#### `raw_original`
+Returns True if associated raw image and the raw image is selected in Photos via "Use RAW as Original", otherwise returns False. See [notes on Raw Photos](#raw-photos).
+
#### `height`
Returns height of the photo in pixels. If image has been edited, returns height of the edited image, otherwise returns height of the original image. See also [original_height](#original_height).
@@ -1149,7 +1169,16 @@ Returns True if photo is a [cloud asset](#iscloudasset) and is synched to iCloud
**Note**: Applies to master (original) photo only. It's possible for the master to be in iCloud but a local edited version is not yet synched to iCloud. `incloud` provides status of only the master photo. osxphotos does not yet provide a means to determine if the edited version is in iCloud. If you need this feature, please open an [issue](https://github.com/RhetTbull/osxphotos/issues).
#### `uti`
-Returns Uniform Type Identifier (UTI) for the image, for example: 'public.jpeg' or 'com.apple.quicktime-movie'
+Returns Uniform Type Identifier (UTI) for the current version of the image, for example: 'public.jpeg' or 'com.apple. quicktime-movie'. If the image has been edited, `uti` will return the UTI for the edited image, otherwise it will return the UTI for the original image.
+
+#### `uti_original`
+Returns Uniform Type Identifier (UTI) for the original unedited image, for example: 'public.jpeg' or 'com.apple.quicktime-movie'.
+
+#### `uti_edited`
+Returns Uniform Type Identifier (UTI) for the edited image, for example: 'public.jpeg'. Returns None if the photo does not have adjustments.
+
+#### `uti_raw`
+Returns Uniform Type Identifier (UTI) for the associated raw image, if there is one; for example, 'com.canon.cr2-raw-image'. If the image is raw but not part of a RAW+JPEG pair, `uti_raw` returns None. In this case, use `uti`, or `uti_original`. See also [has_raw](#has_raw) and [notes on Raw Photos](#raw-photos).
#### `burst`
Returns True if photos is a burst image (e.g. part of a set of burst images), otherwise False.
@@ -1704,6 +1733,38 @@ Returns a dictionary representation of the FaceInfo instance.
#### `json()`
Returns a JSON representation of the FaceInfo instance.
+### Raw Photos
+Handling raw photos in `osxphotos` requires a bit of extra work. Raw photos in Photos can be imported in two different ways: 1) a single raw photo with no associated JPEG image is imported 2) a raw+JPEG pair is imported -- two separate images with same file stem (e.g. `IMG_0001.CR2` and `IMG_001.JPG`) are imported.
+
+The latter are treated by Photos as a single image. By default, Photos will treat these as a JPEG image. They are denoted in the Photos interface with a "J" icon superimposed on the image. In Photos, the user can select "Use RAW as original" in which case the "J" icon changes to an "R" icon and all subsequent edits will use the raw image as the original. To further complicate this, different versions of Photos handle these differently in their internal logic.
+
+`osxphotos` attempts to simplify the handling of these raw+JPEG pairs by providing a set of attributes for accessing both the JPEG and the raw version. For example, [PhotoInfo.has_raw](#has_raw) will be True if the photo has an associated raw image but False otherwise and [PhotoInfo.path_raw](#path_raw) provides the path to the associated raw image. Reference the following table for the various attributes useful for dealing with raw images. Given the different ways Photos deals with raw images I've struggled with how to represent these in a logical and consistent manner. If you have suggestions for a better interface, please open an [issue](https://github.com/RhetTbull/osxphotos/issues)!
+
+#### Raw-Related Attributes
+
+|`PhotoInfo` attribute|`IMG_0001.CR2` imported without raw+JPEG pair|`IMG_0001.CR2` + `IMG_0001.JPG` raw+JPEG pair, JPEG is original|`IMG_0001.CR2` + `IMG_0001.JPG` raw+jpeg pair, raw is original|
+|----------|----------|----------|----------|
+|[israw](#israw)| True | False | False |
+|[has_raw](#has_raw)| False | True | True |
+|[uti](#uti) | `com.canon.cr2-raw-image` | `public.jpeg` | `public.jpeg` |
+|[uti_raw](#uti_raw) | None | `com.canon.cr2-raw-image` | `com.canon.cr2-raw-image` |
+|[raw_original](#raw_original) | False | False | True |
+|[path](#path) | `/path/to/IMG_0001.CR2` | `/path/to/IMG_0001.JPG` | `/path/to/IMG_0001.JPG` |
+|[path_raw](#path_raw) | None | `/path/to/IMG_0001.CR2` | `/path/to/IMG_0001.CR2` |
+
+#### Example
+To get the path of every raw photo, whether it's a single raw photo or a raw+JPEG pair, one could do something like this:
+
+```python
+>>> import osxphotos
+>>> photosdb = osxphotos.PhotosDB()
+>>> photos = photosdb.photos()
+>>> all_raw = [p for p in photos if p.israw or p.has_raw]
+>>> for raw in all_raw:
+... path = raw.path if raw.israw else raw.path_raw
+... print(path)
+```
+
### Template Substitutions
The following substitutions are availabe for use with `PhotoInfo.render_template()`
@@ -1885,10 +1946,10 @@ Thank-you to the following people who have contributed to improving osxphotos!
## Known Bugs
-My goal is make osxphotos as reliable and comprehensive as possible. The test suite currently has over 600 tests--but there are still some [bugs](https://github.com/RhetTbull/osxphotos/issues?q=is%3Aissue+is%3Aopen+label%3Abug) or incomplete features lurking. If you find bugs please open an [issue](https://github.com/RhetTbull/osxphotos/issues). Notable issues include:
+My goal is make osxphotos as reliable and comprehensive as possible. The test suite currently has over 800 tests--but there are still some [bugs](https://github.com/RhetTbull/osxphotos/issues?q=is%3Aissue+is%3Aopen+label%3Abug) or incomplete features lurking. If you find bugs please open an [issue](https://github.com/RhetTbull/osxphotos/issues). Please consult the list of open bugs before deciding that you want to use this code on your Photos library. Notable issues include:
- Face coordinates (mouth, left eye, right eye) may not be correct for images where the head is tilted. See [Issue #196](https://github.com/RhetTbull/osxphotos/issues/196).
-- RAW images imported to Photos with an associated jpeg preview are not handled correctly by osxphotos. osxphotos query and export will operate on the jpeg preview instead of the RAW image as will `PhotoInfo.path`. If the user selects "Use RAW as original" in Photos, the RAW image will be exported or operated on but the jpeg will be ignored. See [Issue #101](https://github.com/RhetTbull/osxphotos/issues/101). Note: Beta version of fix for this bug is implemented in the current version of osxphotos.
+- Raw images imported to Photos with an associated jpeg preview are not handled correctly by osxphotos. osxphotos query and export will operate on the jpeg preview instead of the raw image as will `PhotoInfo.path`. If the user selects "Use RAW as original" in Photos, the raw image will be exported or operated on but the jpeg will be ignored. See [Issue #101](https://github.com/RhetTbull/osxphotos/issues/101). Note: Beta version of fix for this bug is implemented in the current version of osxphotos.
- The `--download-missing` option for `osxphotos export` does not work correctly with burst images. It will download the primary image but not the other burst images. See [Issue #75](https://github.com/RhetTbull/osxphotos/issues/75).
## Implementation Notes
@@ -1908,6 +1969,8 @@ For additional details about how osxphotos is implemented or if you would like t
- [Mako](https://www.makotemplates.org/)
- [bpylist2](https://pypi.org/project/bpylist2/)
- [pathvalidate](https://pypi.org/project/pathvalidate/)
+- [wurlitzer](https://pypi.org/project/wurlitzer/)
+
## Acknowledgements
This project was originally inspired by [photo-export](https://github.com/patrikhson/photo-export) by Patrick Fältström, Copyright (c) 2015 Patrik Fältström paf@frobbit.se
diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py
index f1777e9a..074b68f9 100644
--- a/osxphotos/__main__.py
+++ b/osxphotos/__main__.py
@@ -29,7 +29,7 @@ from ._constants import (
_UNKNOWN_PLACE,
UNICODE_FORMAT,
)
-from ._export_db import ExportDB, ExportDBInMemory
+from .export_db import ExportDB, ExportDBInMemory
from ._version import __version__
from .datetime_formatter import DateTimeFormatter
from .exiftool import get_exiftool_path
@@ -148,7 +148,7 @@ class ExportCommand(click.Command):
formatter.write("\n")
formatter.write_text(
"Note: The number of files reported for export and the number actually exported "
- + "may differ due to live photos, associated RAW images, and edited photos which are reported "
+ + "may differ due to live photos, associated raw images, and edited photos which are reported "
+ "in the total photos exported."
)
formatter.write("\n")
@@ -474,7 +474,7 @@ def query_options(f):
o(
"--has-raw",
is_flag=True,
- help="Search for photos with both a jpeg and RAW version",
+ help="Search for photos with both a jpeg and raw version",
),
o(
"--only-movies",
@@ -1183,9 +1183,9 @@ def query(
@click.option(
"--skip-raw",
is_flag=True,
- help="Do not export associated RAW images of a RAW/jpeg pair. "
- "Note: this does not skip RAW photos if the RAW photo does not have an associated jpeg image "
- "(e.g. the RAW file was imported to Photos without a jpeg preview).",
+ help="Do not export associated raw images of a RAW+JPEG pair. "
+ "Note: this does not skip raw photos if the raw photo does not have an associated jpeg image "
+ "(e.g. the raw file was imported to Photos without a jpeg preview).",
)
@click.option(
"--person-keyword",
@@ -1230,6 +1230,21 @@ def query(
"Note: Starting with Photos 5, all photos are renamed upon import. By default, "
"photos are exported with the the original name they had before import.",
)
+@click.option(
+ "--convert-to-jpeg",
+ is_flag=True,
+ help="Convert all non-jpeg images (e.g. raw, HEIC, PNG, etc) "
+ "to JPEG upon export. Only works if your Mac has a GPU.",
+)
+@click.option(
+ "--jpeg-quality",
+ type=click.FloatRange(0.0, 1.0),
+ default=1.0,
+ help="Value in range 0.0 to 1.0 to use with --convert-to-jpeg. "
+ "A value of 1.0 specifies best quality, "
+ "a value of 0.0 specifies maximum compression. "
+ "Defaults to 1.0."
+)
@click.option(
"--sidecar",
default=None,
@@ -1349,6 +1364,8 @@ def export(
keyword_template,
description_template,
current_name,
+ convert_to_jpeg,
+ jpeg_quality,
sidecar,
only_photos,
only_movies,
@@ -1392,13 +1409,13 @@ def export(
(e.g. search for photos matching all options).
If no query options are provided, all photos will be exported.
By default, all versions of all photos will be exported including edited
- versions, live photo movies, burst photos, and associated RAW images.
+ versions, live photo movies, burst photos, and associated raw images.
See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options
to modify this behavior.
"""
global VERBOSE
- VERBOSE = True if verbose_ else False
+ VERBOSE = bool(verbose_)
if not os.path.isdir(dest):
sys.exit(f"DEST {dest} must be valid path")
@@ -1424,6 +1441,7 @@ def export(
(any(place), no_place),
(deleted, deleted_only),
(skip_edited, skip_original_if_edited),
+ (export_as_hardlink, convert_to_jpeg),
]
if any(all(bb) for bb in exclusive):
click.echo("Incompatible export options", err=True)
@@ -1491,13 +1509,22 @@ def export(
if dry_run:
export_db = ExportDBInMemory(export_db_path)
- # echo = functools.partial(click.echo, err=True)
- # fileutil = FileUtilNoOp(verbose=echo)
fileutil = FileUtilNoOp
else:
export_db = ExportDB(export_db_path)
fileutil = FileUtil
+ if verbose_:
+ if export_db.was_created:
+ verbose(f"Created export database {export_db_path}")
+ else:
+ verbose(f"Using export database {export_db_path}")
+ upgraded = export_db.was_upgraded
+ if upgraded:
+ verbose(
+ f"Upgraded export database {export_db_path} from version {upgraded[0]} to {upgraded[1]}"
+ )
+
photos = _query(
db=db,
keyword=keyword,
@@ -1610,6 +1637,8 @@ def export(
touch_file=touch_file,
edited_suffix=edited_suffix,
use_photos_export=use_photos_export,
+ convert_to_jpeg=convert_to_jpeg,
+ jpeg_quality=jpeg_quality,
)
results_exported.extend(results.exported)
results_new.extend(results.new)
@@ -1618,6 +1647,12 @@ def export(
results_exif_updated.extend(results.exif_updated)
results_touched.extend(results.touched)
+ # if convert_to_jpeg and p.isphoto and p.uti != "public.jpeg":
+ # for photo_file in set(
+ # results.exported + results.updated + results.exif_updated
+ # ):
+ # verbose(f"Converting {photo_file} to jpeg")
+
else:
# show progress bar
with click.progressbar(photos) as bar:
@@ -1651,6 +1686,8 @@ def export(
touch_file=touch_file,
edited_suffix=edited_suffix,
use_photos_export=use_photos_export,
+ convert_to_jpeg=convert_to_jpeg,
+ jpeg_quality=jpeg_quality,
)
results_exported.extend(results.exported)
results_new.extend(results.new)
@@ -1658,6 +1695,7 @@ def export(
results_skipped.extend(results.skipped)
results_exif_updated.extend(results.exif_updated)
results_touched.extend(results.touched)
+
stop_time = time.perf_counter()
# print summary results
if update:
@@ -2020,7 +2058,7 @@ def _query(
photos = [p for p in photos if not p.shared]
if uti:
- photos = [p for p in photos if uti in p.uti]
+ photos = [p for p in photos if uti in p.uti_original]
if burst:
photos = [p for p in photos if p.burst]
@@ -2140,6 +2178,8 @@ def export_photo(
touch_file=None,
edited_suffix="_edited",
use_photos_export=False,
+ convert_to_jpeg=False,
+ jpeg_quality=1.0,
):
""" Helper function for export that does the actual export
@@ -2159,7 +2199,7 @@ def export_photo(
directory: template used to determine output directory
filename_template: template use to determine output file
no_extended_attributes: boolean; if True, exports photo without preserving extended attributes
- export_raw: boolean; if True exports RAW image associate with the photo
+ export_raw: boolean; if True exports raw image associate with the photo
export_edited: boolean; if True exports edited version of photo if there is one
skip_original_if_edited: boolean; if True does not export original if photo has been edited
album_keyword: boolean; if True, exports album names as keywords in metadata
@@ -2171,6 +2211,8 @@ def export_photo(
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.
Returns:
list of path(s) of exported photo or None if photo was missing
@@ -2179,7 +2221,7 @@ def export_photo(
ValueError on invalid filename_template
"""
global VERBOSE
- VERBOSE = True if verbose_ else False
+ VERBOSE = bool(verbose_)
if not download_missing:
if photo.ismissing:
@@ -2257,6 +2299,8 @@ def export_photo(
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
+ convert_to_jpeg=convert_to_jpeg,
+ jpeg_quality=jpeg_quality,
)
results_exported.extend(export_results.exported)
@@ -2316,6 +2360,8 @@ def export_photo(
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
+ convert_to_jpeg=convert_to_jpeg,
+ jpeg_quality=jpeg_quality,
)
results_exported.extend(export_results_edited.exported)
diff --git a/osxphotos/_version.py b/osxphotos/_version.py
index ba0d8927..a68d794e 100644
--- a/osxphotos/_version.py
+++ b/osxphotos/_version.py
@@ -1,3 +1,4 @@
""" version info """
-__version__ = "0.34.5"
+__version__ = "0.35.0"
+
diff --git a/osxphotos/_export_db.py b/osxphotos/export_db.py
similarity index 70%
rename from osxphotos/_export_db.py
rename to osxphotos/export_db.py
index 23726218..b4f8711a 100644
--- a/osxphotos/_export_db.py
+++ b/osxphotos/export_db.py
@@ -14,7 +14,7 @@ from sqlite3 import Error
from ._version import __version__
-OSXPHOTOS_EXPORTDB_VERSION = "1.0"
+OSXPHOTOS_EXPORTDB_VERSION = "2.0"
class ExportDB_ABC(ABC):
@@ -36,6 +36,22 @@ class ExportDB_ABC(ABC):
def get_stat_orig_for_file(self, filename):
pass
+ @abstractmethod
+ def set_stat_edited_for_file(self, filename, stats):
+ pass
+
+ @abstractmethod
+ def get_stat_edited_for_file(self, filename):
+ pass
+
+ @abstractmethod
+ def set_stat_converted_for_file(self, filename, stats):
+ pass
+
+ @abstractmethod
+ def get_stat_converted_for_file(self, filename):
+ pass
+
@abstractmethod
def set_stat_exif_for_file(self, filename, stats):
pass
@@ -61,13 +77,28 @@ class ExportDB_ABC(ABC):
pass
@abstractmethod
- def set_data(self, filename, uuid, orig_stat, exif_stat, info_json, exif_json):
+ def set_data(
+ self,
+ filename,
+ uuid,
+ orig_stat,
+ exif_stat,
+ converted_stat,
+ edited_stat,
+ info_json,
+ exif_json,
+ ):
pass
class ExportDBNoOp(ExportDB_ABC):
""" An ExportDB with NoOp methods """
+ def __init__(self):
+ self.was_created = True
+ self.was_upgraded = False
+ self.version = OSXPHOTOS_EXPORTDB_VERSION
+
def get_uuid_for_file(self, filename):
pass
@@ -80,6 +111,18 @@ class ExportDBNoOp(ExportDB_ABC):
def get_stat_orig_for_file(self, filename):
pass
+ def set_stat_edited_for_file(self, filename, stats):
+ pass
+
+ def get_stat_edited_for_file(self, filename):
+ pass
+
+ def set_stat_converted_for_file(self, filename, stats):
+ pass
+
+ def get_stat_converted_for_file(self, filename):
+ pass
+
def set_stat_exif_for_file(self, filename, stats):
pass
@@ -98,7 +141,17 @@ class ExportDBNoOp(ExportDB_ABC):
def set_exifdata_for_file(self, uuid, exifdata):
pass
- def set_data(self, filename, uuid, orig_stat, exif_stat, info_json, exif_json):
+ def set_data(
+ self,
+ filename,
+ uuid,
+ orig_stat,
+ exif_stat,
+ converted_stat,
+ edited_stat,
+ info_json,
+ exif_json,
+ ):
pass
@@ -122,7 +175,6 @@ class ExportDB(ExportDB_ABC):
returns None if filename not found in database
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
- logging.debug(f"get_uuid: {filename}")
conn = self._conn
try:
c = conn.cursor()
@@ -135,14 +187,12 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
uuid = None
- logging.debug(f"get_uuid: {uuid}")
return uuid
def set_uuid_for_file(self, filename, uuid):
""" set UUID of filename to uuid in the database """
filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower()
- logging.debug(f"set_uuid: {filename} {uuid}")
conn = self._conn
try:
c = conn.cursor()
@@ -162,7 +212,6 @@ class ExportDB(ExportDB_ABC):
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
- logging.debug(f"set_stat_orig_for_file: {filename} {stats}")
conn = self._conn
try:
c = conn.cursor()
@@ -199,9 +248,20 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
stats = (None, None, None)
- logging.debug(f"get_stat_orig_for_file: {stats}")
return stats
+ def set_stat_edited_for_file(self, filename, stats):
+ """ set stat info for edited version of image (in Photos' library)
+ filename: filename to set the stat info for
+ stat: a tuple of length 3: mode, size, mtime """
+ return self._set_stat_for_file("edited", filename, stats)
+
+ def get_stat_edited_for_file(self, filename):
+ """ get stat info for edited version of image (in Photos' library)
+ filename: filename to set the stat info for
+ stat: a tuple of length 3: mode, size, mtime """
+ return self._get_stat_for_file("edited", filename)
+
def set_stat_exif_for_file(self, filename, stats):
""" set stat info for filename (after exiftool has updated it)
filename: filename to set the stat info for
@@ -210,7 +270,6 @@ class ExportDB(ExportDB_ABC):
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
- logging.debug(f"set_stat_exif_for_file: {filename} {stats}")
conn = self._conn
try:
c = conn.cursor()
@@ -247,9 +306,20 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
stats = (None, None, None)
- logging.debug(f"get_stat_exif_for_file: {stats}")
return stats
+ def set_stat_converted_for_file(self, filename, stats):
+ """ set stat info for filename (after image converted to jpeg)
+ filename: filename to set the stat info for
+ stat: a tuple of length 3: mode, size, mtime """
+ return self._set_stat_for_file("converted", filename, stats)
+
+ def get_stat_converted_for_file(self, filename):
+ """ get stat info for filename (after jpeg conversion)
+ returns: tuple of (mode, size, mtime)
+ """
+ return self._get_stat_for_file("converted", filename)
+
def get_info_for_uuid(self, uuid):
""" returns the info JSON struct for a UUID """
conn = self._conn
@@ -262,7 +332,6 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
info = None
- logging.debug(f"get_info: {uuid}, {info}")
return info
def set_info_for_uuid(self, uuid, info):
@@ -278,8 +347,6 @@ class ExportDB(ExportDB_ABC):
except Error as e:
logging.warning(e)
- logging.debug(f"set_info: {uuid}, {info}")
-
def get_exifdata_for_file(self, filename):
""" returns the exifdata JSON struct for a file """
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
@@ -296,7 +363,6 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
exifdata = None
- logging.debug(f"get_exifdata: {filename}, {exifdata}")
return exifdata
def set_exifdata_for_file(self, filename, exifdata):
@@ -313,9 +379,17 @@ class ExportDB(ExportDB_ABC):
except Error as e:
logging.warning(e)
- logging.debug(f"set_exifdata: {filename}, {exifdata}")
-
- def set_data(self, filename, uuid, orig_stat, exif_stat, info_json, exif_json):
+ def set_data(
+ self,
+ filename,
+ uuid,
+ orig_stat,
+ exif_stat,
+ converted_stat,
+ edited_stat,
+ info_json,
+ exif_json,
+ ):
""" sets all the data for file and uuid at once
"""
filename = str(pathlib.Path(filename).relative_to(self._path))
@@ -339,6 +413,14 @@ class ExportDB(ExportDB_ABC):
+ "WHERE filepath_normalized = ?;",
(*exif_stat, filename_normalized),
)
+ c.execute(
+ "INSERT OR REPLACE INTO converted(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
+ (filename_normalized, *converted_stat),
+ )
+ c.execute(
+ "INSERT OR REPLACE INTO edited(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
+ (filename_normalized, *edited_stat),
+ )
c.execute(
"INSERT OR REPLACE INTO info(uuid, json_info) VALUES (?, ?);",
(uuid, info_json),
@@ -358,6 +440,37 @@ class ExportDB(ExportDB_ABC):
except Error as e:
logging.warning(e)
+ def _set_stat_for_file(self, table, filename, stats):
+ filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
+ if len(stats) != 3:
+ raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
+
+ conn = self._conn
+ c = conn.cursor()
+ c.execute(
+ f"INSERT OR REPLACE INTO {table}(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
+ (filename, *stats),
+ )
+ conn.commit()
+
+ def _get_stat_for_file(self, table, filename):
+ filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
+ conn = self._conn
+ c = conn.cursor()
+ c.execute(
+ f"SELECT mode, size, mtime FROM {table} WHERE filepath_normalized = ?",
+ (filename,),
+ )
+ results = c.fetchone()
+ if results:
+ stats = results[0:3]
+ mtime = int(stats[2]) if stats[2] is not None else None
+ stats = (stats[0], stats[1], mtime)
+ else:
+ stats = (None, None, None)
+
+ return stats
+
def _open_export_db(self, dbfile):
""" open export database and return a db connection
if dbfile does not exist, will create and initialize the database
@@ -365,15 +478,24 @@ class ExportDB(ExportDB_ABC):
"""
if not os.path.isfile(dbfile):
- logging.debug(f"dbfile {dbfile} doesn't exist, creating it")
conn = self._get_db_connection(dbfile)
if 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}")
else:
- logging.debug(f"dbfile {dbfile} exists, opening it")
conn = self._get_db_connection(dbfile)
+ self.was_created = False
+ version_info = self._get_database_version(conn)
+ if version_info[1] < OSXPHOTOS_EXPORTDB_VERSION:
+ self._create_db_tables(conn)
+ self.was_upgraded = (version_info[1], OSXPHOTOS_EXPORTDB_VERSION)
+ else:
+ self.was_upgraded = ()
+ self.version = OSXPHOTOS_EXPORTDB_VERSION
return conn
@@ -387,6 +509,13 @@ class ExportDB(ExportDB_ABC):
return conn
+ def _get_database_version(self, conn):
+ """ return tuple of (osxphotos, exportdb) versions for database connection conn """
+ version_info = conn.execute(
+ "SELECT osxphotos, exportdb, max(id) FROM version"
+ ).fetchone()
+ return (version_info[0], version_info[1])
+
def _create_db_tables(self, conn):
""" create (if not already created) the necessary db tables for the export database
conn: sqlite3 db connection
@@ -427,9 +556,25 @@ class ExportDB(ExportDB_ABC):
filepath_normalized TEXT NOT NULL,
json_exifdata JSON
); """,
- "sql_files_idx": """ CREATE UNIQUE INDEX idx_files_filepath_normalized on files (filepath_normalized); """,
- "sql_info_idx": """ CREATE UNIQUE INDEX idx_info_uuid on info (uuid); """,
- "sql_exifdata_idx": """ CREATE UNIQUE INDEX idx_exifdata_filename on exifdata (filepath_normalized); """,
+ "sql_edited_table": """ CREATE TABLE IF NOT EXISTS edited (
+ id INTEGER PRIMARY KEY,
+ filepath_normalized TEXT NOT NULL,
+ mode INTEGER,
+ size INTEGER,
+ mtime REAL
+ ); """,
+ "sql_converted_table": """ CREATE TABLE IF NOT EXISTS converted (
+ id INTEGER PRIMARY KEY,
+ filepath_normalized TEXT NOT NULL,
+ mode INTEGER,
+ size INTEGER,
+ mtime REAL
+ ); """,
+ "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);""",
}
try:
c = conn.cursor()
@@ -445,11 +590,10 @@ class ExportDB(ExportDB_ABC):
def __del__(self):
""" ensure the database connection is closed """
- if self._conn:
- try:
- self._conn.close()
- except Error as e:
- logging.warning(e)
+ try:
+ self._conn.close()
+ except:
+ pass
def _insert_run_info(self):
dt = datetime.datetime.utcnow().isoformat()
@@ -488,18 +632,18 @@ class ExportDBInMemory(ExportDB):
def _open_export_db(self, dbfile):
""" open export database and return a db connection
- if dbfile does not exist, will create and initialize the database
returns: connection to the database
"""
if not os.path.isfile(dbfile):
- logging.debug(f"dbfile {dbfile} doesn't exist, creating in memory version")
conn = self._get_db_connection()
if 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 in-memory database")
else:
- logging.debug(f"dbfile {dbfile} exists, opening it and copying to memory")
try:
conn = sqlite3.connect(dbfile)
except Error as e:
@@ -516,6 +660,14 @@ class ExportDBInMemory(ExportDB):
conn = sqlite3.connect(":memory:")
conn.cursor().executescript(tempfile.read())
conn.commit()
+ self.was_created = False
+ _, exportdb_ver = self._get_database_version(conn)
+ if exportdb_ver < OSXPHOTOS_EXPORTDB_VERSION:
+ self._create_db_tables(conn)
+ self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION)
+ else:
+ self.was_upgraded = ()
+ self.version = OSXPHOTOS_EXPORTDB_VERSION
return conn
diff --git a/osxphotos/fileutil.py b/osxphotos/fileutil.py
index f5609e09..d3fcf333 100644
--- a/osxphotos/fileutil.py
+++ b/osxphotos/fileutil.py
@@ -8,6 +8,7 @@ import subprocess
import sys
from abc import ABC, abstractmethod
+from .imageconverter import ImageConverter
class FileUtilABC(ABC):
""" Abstract base class for FileUtil """
@@ -47,6 +48,11 @@ class FileUtilABC(ABC):
def file_sig(cls, file1):
pass
+ @classmethod
+ @abstractmethod
+ def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
+ pass
+
class FileUtilMacOS(FileUtilABC):
""" Various file utilities """
@@ -163,6 +169,21 @@ class FileUtilMacOS(FileUtilABC):
def file_sig(cls, f1):
""" return os.stat signature for file f1 """
return cls._sig(os.stat(f1))
+
+ @classmethod
+ def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
+ """ converts image file src_file to jpeg format as dest_file
+
+ Args:
+ src_file: image file to convert
+ dest_file: destination path to write converted file to
+ compression quality: JPEG compression quality in range 0.0 <= compression_quality <= 1.0; default 1.0 (best quality)
+
+ Returns:
+ True if success, otherwise False
+ """
+ converter = ImageConverter()
+ return converter.write_jpeg(src_file, dest_file, compression_quality=compression_quality)
@staticmethod
def _sig(st):
@@ -173,7 +194,6 @@ class FileUtilMacOS(FileUtilABC):
# use int(st.st_mtime) because ditto does not copy fractional portion of mtime
return (stat.S_IFMT(st.st_mode), st.st_size, int(st.st_mtime))
-
class FileUtil(FileUtilMacOS):
""" Various file utilities """
@@ -221,3 +241,7 @@ class FileUtilNoOp(FileUtil):
def file_sig(cls, file1):
cls.verbose(f"file_sig: {file1}")
return (42, 42, 42)
+
+ @classmethod
+ def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
+ cls.verbose(f"convert_to_jpeg: {src_file}, {dest_file}, {compression_quality}")
diff --git a/osxphotos/imageconverter.py b/osxphotos/imageconverter.py
new file mode 100644
index 00000000..061dd3da
--- /dev/null
+++ b/osxphotos/imageconverter.py
@@ -0,0 +1,116 @@
+""" ImageConverter class
+ Convert an image to JPEG using CoreImage --
+ for example, RAW to JPEG. Only works if Mac equipped with GPU. """
+
+# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
+
+import logging
+import pathlib
+
+import Metal
+import Quartz
+from Cocoa import NSURL
+from Foundation import NSDictionary
+from py import path
+
+# needed to capture system-level stderr
+from wurlitzer import pipes
+
+
+class ImageConverter:
+ """ Convert images to jpeg. This class is a singleton
+ which will re-use the Core Image CIContext to avoid
+ creating a new context for every conversion. """
+
+ def __new__(cls, *args, **kwargs):
+ """ create new object or return instance of already created singleton """
+ if not hasattr(cls, "instance") or not cls.instance:
+ cls.instance = super().__new__(cls)
+
+ return cls.instance
+
+ def __init__(self):
+ """ return existing singleton or create a new one """
+
+ if hasattr(self, "context"):
+ return
+
+ """ initialize CIContext """
+ context_options = NSDictionary.dictionaryWithDictionary_(
+ {
+ "workingColorSpace": Quartz.CoreGraphics.kCGColorSpaceExtendedSRGB,
+ "workingFormat": Quartz.kCIFormatRGBAh,
+ }
+ )
+ mtldevice = Metal.MTLCreateSystemDefaultDevice()
+ self.context = Quartz.CIContext.contextWithMTLDevice_options_(
+ mtldevice, context_options
+ )
+
+ def write_jpeg(self, input_path, output_path, compression_quality=1.0):
+ """ convert image to jpeg and write image to output_path
+
+ Args:
+ input_path: path to input image (e.g. '/path/to/import/file.CR2') as str or pathlib.Path
+ output_path: path to exported jpeg (e.g. '/path/to/export/file.jpeg') as str or pathlib.Path
+ compression_quality: JPEG compression quality, float in range 0.0 to 1.0; default is 1.0 (best quality)
+
+ Return:
+ True if conversion successful, else False
+
+ Raises:
+ ValueError if compression quality not in range 0.0 to 1.0
+ FileNotFoundError if input_path doesn't exist
+ """
+
+ # accept input_path or output_path as pathlib.Path
+ if not isinstance(input_path, str):
+ input_path = str(input_path)
+
+ if not isinstance(output_path, str):
+ output_path = str(output_path)
+
+ if not pathlib.Path(input_path).is_file():
+ raise FileNotFoundError(f"could not find {input_path}")
+
+ if not (0.0 <= compression_quality <= 1.0):
+ raise ValueError(
+ "illegal value for compression_quality: {compression_quality}"
+ )
+
+ input_url = NSURL.fileURLWithPath_(input_path)
+ output_url = NSURL.fileURLWithPath_(output_path)
+
+ with pipes() as (out, err):
+ # capture stdout and stderr from system calls
+ # otherwise, Quartz.CIImage.imageWithContentsOfURL_
+ # prints to stderr something like:
+ # 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
+ # 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
+ input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
+
+ if input_image is None:
+ logging.debug(f"Could not create CIImage for {input_path}")
+ return False
+
+ output_colorspace = (
+ input_image.colorSpace()
+ if input_image.colorSpace()
+ else Quartz.CGColorSpaceCreateWithName(
+ Quartz.CoreGraphics.kCGColorSpaceSRGB
+ )
+ )
+ output_options = NSDictionary.dictionaryWithDictionary_(
+ {"kCGImageDestinationLossyCompressionQuality": compression_quality}
+ )
+ _, error = self.context.writeJPEGRepresentationOfImage_toURL_colorSpace_options_error_(
+ input_image, output_url, output_colorspace, output_options, None
+ )
+ if not error:
+ return True
+ else:
+ logging.debug(
+ "Error converting file {input_path} to jpeg at {output_path}: {error}"
+ )
+ return False
+
diff --git a/osxphotos/photoinfo/_photoinfo_export.py b/osxphotos/photoinfo/_photoinfo_export.py
index 0a7c16eb..bbee18b3 100644
--- a/osxphotos/photoinfo/_photoinfo_export.py
+++ b/osxphotos/photoinfo/_photoinfo_export.py
@@ -30,7 +30,7 @@ from .._constants import (
_UNKNOWN_PERSON,
_XMP_TEMPLATE_NAME,
)
-from .._export_db import ExportDBNoOp
+from ..export_db import ExportDBNoOp
from ..exiftool import ExifTool
from ..fileutil import FileUtil
from ..utils import dd_to_dms_str, findfiles
@@ -306,6 +306,8 @@ def export2(
fileutil=FileUtil,
dry_run=False,
touch_file=False,
+ convert_to_jpeg=False,
+ jpeg_quality=1.0,
):
""" export photo, like export but with update and dry_run options
dest: must be valid destination path or exception raised
@@ -313,10 +315,8 @@ def export2(
**NOTE**: if provided, user must ensure file extension (suffix) is correct.
For example, if photo is .CR2 file, edited image may be .jpeg.
If you provide an extension different than what the actual file is,
- export will print a warning but will export the photo using the
- incorrect file extension (unless use_photos_export is true, in which case export will
- use the extension provided by Photos upon export; in this case, an incorrect extension is
- silently ignored).
+ will export the photo using the incorrect file extension (unless use_photos_export is true,
+ in which case export will use the extension provided by Photos upon export.
e.g. to get the extension of the edited photo,
reference PhotoInfo.path_edited
edited: (boolean, default=False); if True will export the edited version of the photo
@@ -335,7 +335,6 @@ def export2(
timeout: (int, default=120) timeout in seconds used with use_photos_export
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
no_xattr: (boolean, default = False); if True, exports file without preserving extended attributes
- returns list of full paths to the exported files
use_albums_as_keywords: (boolean, default = False); if True, will include album names in keywords
when exporting metadata with exiftool or sidecar
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
@@ -349,6 +348,8 @@ def export2(
fileutil: (FileUtilABC); class that conforms to FileUtilABC with various file utilities
dry_run: (boolean, default=False); set to True to run in "dry run" mode
touch_file: (boolean, default=False); if True, sets file's modification time upon photo date
+ 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.
Returns: ExportResults namedtuple with fields: exported, new, updated, skipped
where each field is a list of file paths
@@ -357,6 +358,10 @@ def export2(
and no-op fileutil (e.g. ExportDBInMemory and FileUtilNoOp)
"""
+ # NOTE: This function is very complex and does a lot of things.
+ # Don't modify this code if you don't fully understand everything it does.
+ # TODO: This is a good candidate for refactoring.
+
# when called from export(), won't get an export_db, so use no-op version
if export_db is None:
export_db = ExportDBNoOp()
@@ -392,34 +397,41 @@ def export2(
raise TypeError(
"Too many positional arguments. Should be at most two: destination, filename."
)
- else:
- # verify destination is a valid path
- if dest is None:
- raise ValueError("Destination must not be None")
- elif not dry_run and not os.path.isdir(dest):
- raise FileNotFoundError("Invalid path passed to export")
- if filename and len(filename) == 1:
- # if filename passed, use it
- fname = filename[0]
- else:
- # no filename provided so use the default
- # if edited file requested, use filename but add _edited
- # need to use file extension from edited file as Photos saves a jpeg once edited
- if edited and not use_photos_export:
- # verify we have a valid path_edited and use that to get filename
- if not self.path_edited:
- raise FileNotFoundError(
- "edited=True but path_edited is none; hasadjustments: "
- f" {self.hasadjustments}"
- )
- edited_name = pathlib.Path(self.path_edited).name
- edited_suffix = pathlib.Path(edited_name).suffix
- fname = (
- pathlib.Path(self.filename).stem + edited_identifier + edited_suffix
+ # verify destination is a valid path
+ if dest is None:
+ raise ValueError("Destination must not be None")
+ elif not dry_run and not os.path.isdir(dest):
+ raise FileNotFoundError("Invalid path passed to export")
+
+ if filename and len(filename) == 1:
+ # if filename passed, use it
+ fname = filename[0]
+ else:
+ # no filename provided so use the default
+ # if edited file requested, use filename but add _edited
+ # need to use file extension from edited file as Photos saves a jpeg once edited
+ if edited and not use_photos_export:
+ # verify we have a valid path_edited and use that to get filename
+ if not self.path_edited:
+ raise FileNotFoundError(
+ "edited=True but path_edited is none; hasadjustments: "
+ f" {self.hasadjustments}"
)
- else:
- fname = self.filename
+ edited_name = pathlib.Path(self.path_edited).name
+ edited_suffix = pathlib.Path(edited_name).suffix
+ fname = pathlib.Path(self.filename).stem + edited_identifier + edited_suffix
+ else:
+ fname = self.filename
+
+ uti = self.uti if edited else self.uti_original
+ if convert_to_jpeg and self.isphoto and uti != "public.jpeg":
+ # not a jpeg but will convert to jpeg upon export so fix file extension
+ fname_new = pathlib.Path(fname)
+ fname = str(fname_new.parent / f"{fname_new.stem}.jpeg")
+ else:
+ # nothing to convert
+ convert_to_jpeg = False
# check destination path
dest = pathlib.Path(dest)
@@ -473,16 +485,12 @@ def export2(
raise FileNotFoundError(f"{src} does not appear to exist")
if not _check_export_suffix(src, dest, edited):
- logging.warning(
+ logging.debug(
f"Invalid destination suffix: {dest.suffix} for {self.path}, "
+ f"edited={edited}, path_edited={self.path_edited}, "
+ f"original_filename={self.original_filename}, filename={self.filename}"
)
- logging.debug(
- f"exporting {src} to {dest}, overwrite={overwrite}, increment={increment}, dest exists: {dest.exists()}"
- )
-
# found source now try to find right destination
if update and dest.exists():
# destination exists, check to see if destination is the right UUID
@@ -498,14 +506,13 @@ def export2(
self.uuid,
fileutil.file_sig(dest),
(None, None, None),
+ (None, None, None),
+ (None, None, None),
self.json(),
None,
)
if dest_uuid != self.uuid:
# not the right file, find the right one
- logging.debug(
- f"Need to find right photo: uuid={self.uuid}, dest={dest_uuid}, dest={dest}, path={self.path}"
- )
count = 1
glob_str = str(dest.parent / f"{dest.stem} (*{dest.suffix}")
dest_files = glob.glob(glob_str)
@@ -513,17 +520,11 @@ def export2(
for file_ in dest_files:
dest_uuid = export_db.get_uuid_for_file(file_)
if dest_uuid == self.uuid:
- logging.debug(
- f"Found matching file for uuid: {dest_uuid}, {file_}"
- )
dest = pathlib.Path(file_)
found_match = True
break
elif dest_uuid is None and fileutil.cmp(src, file_):
# files match, update the UUID
- logging.debug(
- f"Found matching file with blank uuid: {self.uuid}, {file_}"
- )
dest = pathlib.Path(file_)
found_match = True
export_db.set_data(
@@ -531,16 +532,14 @@ def export2(
self.uuid,
fileutil.file_sig(dest),
(None, None, None),
+ (None, None, None),
+ (None, None, None),
self.json(),
None,
)
break
if not found_match:
- logging.debug(
- f"Didn't find destination match for uuid {self.uuid} {dest}"
- )
-
# increment the destination file
count = 1
glob_str = str(dest.parent / f"{dest.stem}*")
@@ -551,7 +550,6 @@ def export2(
dest_new = f"{dest.stem} ({count})"
count += 1
dest = dest.parent / f"{dest_new}{dest.suffix}"
- logging.debug(f"New destination = {dest}, uuid = {self.uuid}")
# export the dest file
results = self._export_photo(
@@ -564,7 +562,10 @@ def export2(
export_as_hardlink,
exiftool,
touch_file,
- fileutil,
+ convert_to_jpeg,
+ fileutil=fileutil,
+ edited=edited,
+ jpeg_quality=jpeg_quality,
)
exported_files = results.exported
update_new_files = results.new
@@ -591,7 +592,9 @@ def export2(
export_as_hardlink,
exiftool,
touch_file,
- fileutil,
+ convert_to_jpeg,
+ fileutil=fileutil,
+ jpeg_quality=jpeg_quality,
)
exported_files.extend(results.exported)
update_new_files.extend(results.new)
@@ -618,7 +621,9 @@ def export2(
export_as_hardlink,
exiftool,
touch_file,
- fileutil,
+ convert_to_jpeg,
+ fileutil=fileutil,
+ jpeg_quality=jpeg_quality,
)
exported_files.extend(results.exported)
update_new_files.extend(results.new)
@@ -683,6 +688,7 @@ def export2(
f"Error exporting photo {self.uuid} to {dest} with use_photos_export"
)
+ # export metadata
if sidecar_json:
logging.debug("writing exiftool_json_sidecar")
sidecar_filename = dest.parent / pathlib.Path(f"{dest.stem}{dest.suffix}.json")
@@ -744,7 +750,6 @@ def export2(
if old_data is None or files_are_different:
# didn't have old data, assume we need to write it
# or files were different
- logging.debug(f"No exifdata for {exported_file}, writing it")
if not dry_run:
self._write_exif_data(
exported_file,
@@ -768,7 +773,6 @@ def export2(
exif_files_updated.append(exported_file)
elif exiftool and exif_files:
for exported_file in exif_files:
- logging.debug(f"Writing exif data to {exported_file}")
if not dry_run:
self._write_exif_data(
exported_file,
@@ -822,7 +826,10 @@ def _export_photo(
export_as_hardlink,
exiftool,
touch_file,
+ convert_to_jpeg,
fileutil=FileUtil,
+ edited=False,
+ jpeg_quality=1.0,
):
""" Helper function for export()
Does the actual copy or hardlink taking the appropriate
@@ -840,12 +847,21 @@ def _export_photo(
export_as_hardlink: bool
exiftool: bool
touch_file: bool
+ convert_to_jpeg: bool; if True, convert file to jpeg on export
fileutil: FileUtil class that conforms to fileutil.FileUtilABC
+ edited: bool; set to True if exporting edited version of photo
+ 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.
Returns:
ExportResults
+
+ Raises:
+ ValueError if export_as_hardlink and convert_to_jpeg both True
"""
+ if export_as_hardlink and convert_to_jpeg:
+ raise ValueError("export_as_hardlink and convert_to_jpeg cannot both be True")
+
exported_files = []
update_updated_files = []
update_new_files = []
@@ -854,40 +870,44 @@ def _export_photo(
dest_str = str(dest)
dest_exists = dest.exists()
- if export_as_hardlink:
- op_desc = "export_as_hardlink"
- else:
- op_desc = "export_by_copying"
+ op_desc = "export_as_hardlink" if export_as_hardlink else "export_by_copying"
- if not update:
- # not update, export the file
- logging.debug(f"Exporting file with {op_desc} {src} {dest}")
- exported_files.append(dest_str)
- if touch_file:
- sig = fileutil.file_sig(src)
- sig = (sig[0], sig[1], int(self.date.timestamp()))
- if not fileutil.cmp_file_sig(src, sig):
- touched_files.append(dest_str)
- else: # updating
- if not dest_exists:
- # update, destination doesn't exist (new file)
- logging.debug(f"Update: exporting new file with {op_desc} {src} {dest}")
- update_new_files.append(dest_str)
- if touch_file:
- touched_files.append(dest_str)
- else:
+ if update: # updating
+ cmp_touch, cmp_orig = False, False
+ if dest_exists:
# update, destination exists, but we might not need to replace it...
if exiftool:
sig_exif = export_db.get_stat_exif_for_file(dest_str)
cmp_orig = fileutil.cmp_file_sig(dest_str, sig_exif)
sig_exif = (sig_exif[0], sig_exif[1], int(self.date.timestamp()))
cmp_touch = fileutil.cmp_file_sig(dest_str, sig_exif)
+ elif convert_to_jpeg:
+ sig_converted = export_db.get_stat_converted_for_file(dest_str)
+ cmp_orig = fileutil.cmp_file_sig(dest_str, sig_converted)
+ sig_converted = (
+ sig_converted[0],
+ sig_converted[1],
+ int(self.date.timestamp()),
+ )
+ cmp_touch = fileutil.cmp_file_sig(dest_str, sig_converted)
else:
cmp_orig = fileutil.cmp(src, dest)
cmp_touch = fileutil.cmp(src, dest, mtime1=int(self.date.timestamp()))
sig_cmp = cmp_touch if touch_file else cmp_orig
+ if edited:
+ # requested edited version of photo
+ # need to see if edited version in Photos library has changed
+ # (e.g. it's been edited again)
+ sig_edited = export_db.get_stat_edited_for_file(dest_str)
+ cmp_edited = (
+ fileutil.cmp_file_sig(src, sig_edited)
+ if sig_edited != (None, None, None)
+ else False
+ )
+ sig_cmp = sig_cmp and cmp_edited
+
if (export_as_hardlink and dest.samefile(src)) or (
not export_as_hardlink and not dest.samefile(src) and sig_cmp
):
@@ -911,7 +931,24 @@ def _export_photo(
if touch_file:
touched_files.append(dest_str)
+ else:
+ # update, destination doesn't exist (new file)
+ logging.debug(f"Update: exporting new file with {op_desc} {src} {dest}")
+ update_new_files.append(dest_str)
+ if touch_file:
+ touched_files.append(dest_str)
+ else:
+ # not update, export the file
+ logging.debug(f"Exporting file with {op_desc} {src} {dest}")
+ exported_files.append(dest_str)
+ if touch_file:
+ sig = fileutil.file_sig(src)
+ sig = (sig[0], sig[1], int(self.date.timestamp()))
+ if not fileutil.cmp_file_sig(src, sig):
+ touched_files.append(dest_str)
if not update_skipped_files:
+ converted_stat = (None, None, None)
+ edited_stat = fileutil.file_sig(src) if edited else (None, None, None)
if dest_exists and (update or overwrite):
# need to remove the destination first
logging.debug(
@@ -920,6 +957,10 @@ def _export_photo(
fileutil.unlink(dest)
if export_as_hardlink:
fileutil.hardlink(src, dest)
+ elif convert_to_jpeg:
+ # use convert_to_jpeg to export the file
+ fileutil.convert_to_jpeg(src, dest_str, compression_quality=jpeg_quality)
+ converted_stat = fileutil.file_sig(dest_str)
else:
fileutil.copy(src, dest_str, norsrc=no_xattr)
@@ -928,6 +969,8 @@ def _export_photo(
self.uuid,
fileutil.file_sig(dest_str),
(None, None, None),
+ converted_stat,
+ edited_stat,
self.json(),
None,
)
diff --git a/osxphotos/photoinfo/photoinfo.py b/osxphotos/photoinfo/photoinfo.py
index c71f524f..d11a3501 100644
--- a/osxphotos/photoinfo/photoinfo.py
+++ b/osxphotos/photoinfo/photoinfo.py
@@ -20,6 +20,7 @@ from .._constants import (
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_VERSION,
+ _PHOTOS_5_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND,
@@ -67,11 +68,9 @@ class PhotoInfo:
@property
def filename(self):
""" filename of the picture """
- # sourcery off
- if self.has_raw and self.raw_original:
- # return name of the RAW file
- # TODO: not yet implemented
- return self._info["filename"]
+ if self._db._db_version <= _PHOTOS_4_VERSION and self.has_raw and self.raw_original:
+ # return the JPEG version as that's what Photos 5+ does
+ return self._info["raw_pair_info"]["filename"]
else:
return self._info["filename"]
@@ -79,7 +78,11 @@ class PhotoInfo:
def original_filename(self):
""" original filename of the picture
Photos 5 mangles filenames upon import """
- return self._info["originalFilename"]
+ if self._db._db_version <= _PHOTOS_4_VERSION and self.has_raw and self.raw_original:
+ # return the JPEG version as that's what Photos 5+ does
+ return self._info["raw_pair_info"]["originalFilename"]
+ else:
+ return self._info["originalFilename"]
@property
def date(self):
@@ -107,39 +110,67 @@ class PhotoInfo:
@property
def path(self):
""" absolute path on disk of the original picture """
+ try:
+ return self._path
+ except AttributeError:
+ self._path = None
+ photopath = None
+ if self._info["isMissing"] == 1:
+ return photopath # path would be meaningless until downloaded
- photopath = None
- if self._info["isMissing"] == 1:
- return photopath # path would be meaningless until downloaded
+ if self._db._db_version <= _PHOTOS_4_VERSION:
+ if self._info["has_raw"]:
+ # return the path to JPEG even if RAW is original
+ vol = (
+ self._db._dbvolumes[self._info["raw_pair_info"]["volumeId"]]
+ if self._info["raw_pair_info"]["volumeId"] is not None
+ else None
+ )
+ if vol is not None:
+ photopath = os.path.join(
+ "/Volumes", vol, self._info["raw_pair_info"]["imagePath"]
+ )
+ else:
+ photopath = os.path.join(
+ self._db._masters_path,
+ self._info["raw_pair_info"]["imagePath"],
+ )
+ else:
+ vol = self._info["volume"]
+ if vol is not None:
+ photopath = os.path.join(
+ "/Volumes", vol, self._info["imagePath"]
+ )
+ else:
+ photopath = os.path.join(
+ self._db._masters_path, self._info["imagePath"]
+ )
+ self._path = photopath
+ return photopath
- if self._db._db_version <= _PHOTOS_4_VERSION:
- vol = self._info["volume"]
- if vol is not None:
- photopath = os.path.join("/Volumes", vol, self._info["imagePath"])
+ if self._info["shared"]:
+ # shared photo
+ photopath = os.path.join(
+ self._db._library_path,
+ _PHOTOS_5_SHARED_PHOTO_PATH,
+ self._info["directory"],
+ self._info["filename"],
+ )
+ self._path = photopath
+ return photopath
+
+ if self._info["directory"].startswith("/"):
+ photopath = os.path.join(
+ self._info["directory"], self._info["filename"]
+ )
else:
photopath = os.path.join(
- self._db._masters_path, self._info["imagePath"]
+ self._db._masters_path,
+ self._info["directory"],
+ self._info["filename"],
)
+ self._path = photopath
return photopath
- # TODO: Is there a way to use applescript or PhotoKit to force the download in this
-
- if self._info["shared"]:
- # shared photo
- photopath = os.path.join(
- self._db._library_path,
- _PHOTOS_5_SHARED_PHOTO_PATH,
- self._info["directory"],
- self._info["filename"],
- )
- return photopath
-
- if self._info["directory"].startswith("/"):
- photopath = os.path.join(self._info["directory"], self._info["filename"])
- else:
- photopath = os.path.join(
- self._db._masters_path, self._info["directory"], self._info["filename"]
- )
- return photopath
@property
def path_edited(self):
@@ -149,109 +180,132 @@ class PhotoInfo:
# TODO: break this code into a _path_edited_4 and _path_edited_5
# version to simplify the big if/then; same for path_live_photo
- photopath = None
-
- if self._db._db_version <= _PHOTOS_4_VERSION:
- if self._info["hasAdjustments"]:
- edit_id = self._info["edit_resource_id"]
- if edit_id is not None:
- library = self._db._library_path
- folder_id, file_id = _get_resource_loc(edit_id)
- # todo: is this always true or do we need to search file file_id under folder_id
- # figure out what kind it is and build filename
- filename = None
- if self._info["type"] == _PHOTO_TYPE:
- # it's a photo
- filename = f"fullsizeoutput_{file_id}.jpeg"
- elif self._info["type"] == _MOVIE_TYPE:
- # it's a movie
- filename = f"fullsizeoutput_{file_id}.mov"
- else:
- # don't know what it is!
- logging.debug(f"WARNING: unknown type {self._info['type']}")
- return None
-
- # photopath appears to usually be in "00" subfolder but
- # could be elsewhere--I haven't figured out this logic yet
- # first see if it's in 00
- photopath = os.path.join(
- library,
- "resources",
- "media",
- "version",
- folder_id,
- "00",
- filename,
- )
-
- if not os.path.isfile(photopath):
- rootdir = os.path.join(
- library, "resources", "media", "version", folder_id
- )
-
- for dirname, _, filelist in os.walk(rootdir):
- if filename in filelist:
- photopath = os.path.join(dirname, filename)
- break
-
- # check again to see if we found a valid file
- if not os.path.isfile(photopath):
- logging.debug(
- f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
- )
- photopath = None
- else:
- logging.debug(
- f"{self.uuid} hasAdjustments but edit_resource_id is None"
- )
- photopath = None
+ try:
+ return self._path_edited
+ except AttributeError:
+ if self._db._db_version <= _PHOTOS_4_VERSION:
+ self._path_edited = self._path_edited_4()
+ return self._path_edited
else:
+ self._path_edited = self._path_edited_5()
+ return self._path_edited
+
+ def _path_edited_5(self):
+ """ return path_edited for Photos >= 5 """
+ # In Photos 5.0 / Catalina / MacOS 10.15:
+ # edited photos appear to always be converted to .jpeg and stored in
+ # library_name/resources/renders/X/UUID_1_201_a.jpeg
+ # where X = first letter of UUID
+ # and UUID = UUID of image
+ # this seems to be true even for photos not copied to Photos library and
+ # where original format was not jpg/jpeg
+ # if more than one edit, previous edit is stored as UUID_p.jpeg
+ #
+ # In Photos 6.0 / Big Sur, the edited image is a .heic if the photo isn't a jpeg,
+ # otherwise it's a jpeg. It could also be a jpeg if photo library upgraded from earlier
+ # version.
+
+ if self._db._db_version < _PHOTOS_5_VERSION:
+ raise RuntimeError("Wrong database format!")
+
+ if self._info["hasAdjustments"]:
+ library = self._db._library_path
+ directory = self._uuid[0] # first char of uuid
+ filename = None
+ if self._info["type"] == _PHOTO_TYPE:
+ # it's a photo
+ if self._db._photos_ver == 5:
+ filename = f"{self._uuid}_1_201_a.jpeg"
+ else:
+ # could be a heic or a jpeg
+ if self.uti == "public.heic":
+ filename = f"{self._uuid}_1_201_a.heic"
+ else:
+ filename = f"{self._uuid}_1_201_a.jpeg"
+ elif self._info["type"] == _MOVIE_TYPE:
+ # it's a movie
+ filename = f"{self._uuid}_2_0_a.mov"
+ else:
+ # don't know what it is!
+ logging.debug(f"WARNING: unknown type {self._info['type']}")
+ return None
+
+ photopath = os.path.join(
+ library, "resources", "renders", directory, filename
+ )
+
+ if not os.path.isfile(photopath):
+ logging.debug(
+ f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
+ )
photopath = None
-
- # if self._info["isMissing"] == 1:
- # photopath = None # path would be meaningless until downloaded
else:
- # in Photos 5.0 / Catalina / MacOS 10.15:
- # edited photos appear to always be converted to .jpeg and stored in
- # library_name/resources/renders/X/UUID_1_201_a.jpeg
- # where X = first letter of UUID
- # and UUID = UUID of image
- # this seems to be true even for photos not copied to Photos library and
- # where original format was not jpg/jpeg
- # if more than one edit, previous edit is stored as UUID_p.jpeg
+ photopath = None
- if self._info["hasAdjustments"]:
+ # TODO: might be possible for original/master to be missing but edit to still be there
+ # if self._info["isMissing"] == 1:
+ # photopath = None # path would be meaningless until downloaded
+
+ # logging.debug(photopath)
+
+ return photopath
+
+ def _path_edited_4(self):
+ """ return path_edited for Photos <= 4 """
+
+ if self._db._db_version > _PHOTOS_4_VERSION:
+ raise RuntimeError("Wrong database format!")
+
+ photopath = None
+ if self._info["hasAdjustments"]:
+ edit_id = self._info["edit_resource_id"]
+ if edit_id is not None:
library = self._db._library_path
- directory = self._uuid[0] # first char of uuid
+ folder_id, file_id = _get_resource_loc(edit_id)
+ # todo: is this always true or do we need to search file file_id under folder_id
+ # figure out what kind it is and build filename
filename = None
if self._info["type"] == _PHOTO_TYPE:
# it's a photo
- filename = f"{self._uuid}_1_201_a.jpeg"
+ filename = f"fullsizeoutput_{file_id}.jpeg"
elif self._info["type"] == _MOVIE_TYPE:
# it's a movie
- filename = f"{self._uuid}_2_0_a.mov"
+ filename = f"fullsizeoutput_{file_id}.mov"
else:
# don't know what it is!
logging.debug(f"WARNING: unknown type {self._info['type']}")
return None
+ # photopath appears to usually be in "00" subfolder but
+ # could be elsewhere--I haven't figured out this logic yet
+ # first see if it's in 00
photopath = os.path.join(
- library, "resources", "renders", directory, filename
+ library, "resources", "media", "version", folder_id, "00", filename,
)
+ if not os.path.isfile(photopath):
+ rootdir = os.path.join(
+ library, "resources", "media", "version", folder_id
+ )
+
+ for dirname, _, filelist in os.walk(rootdir):
+ if filename in filelist:
+ photopath = os.path.join(dirname, filename)
+ break
+
+ # check again to see if we found a valid file
if not os.path.isfile(photopath):
logging.debug(
- f"edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
+ f"MISSING PATH: edited file for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
+ logging.debug(
+ f"{self.uuid} hasAdjustments but edit_resource_id is None"
+ )
photopath = None
-
- # TODO: might be possible for original/master to be missing but edit to still be there
- # if self._info["isMissing"] == 1:
- # photopath = None # path would be meaningless until downloaded
-
- # logging.debug(photopath)
+ else:
+ photopath = None
return photopath
@@ -473,7 +527,37 @@ class PhotoInfo:
""" Returns Uniform Type Identifier (UTI) for the image
for example: public.jpeg or com.apple.quicktime-movie
"""
- return self._info["UTI"]
+ if self._db._db_version <= _PHOTOS_4_VERSION:
+ if self.hasadjustments:
+ return self._info["UTI_edited"]
+ elif self.has_raw and self.raw_original:
+ # return UTI of the non-raw image to match Photos 5+ behavior
+ return self._info["raw_pair_info"]["UTI"]
+ else:
+ return self._info["UTI"]
+ else:
+ return self._info["UTI"]
+
+ @property
+ def uti_original(self):
+ """ Returns Uniform Type Identifier (UTI) for the original image
+ for example: public.jpeg or com.apple.quicktime-movie
+ """
+ if self._db._db_version <= _PHOTOS_4_VERSION and self._info["has_raw"]:
+ return self._info["raw_pair_info"]["UTI"]
+ else:
+ return self._info["UTI_original"]
+
+ @property
+ def uti_edited(self):
+ """ Returns Uniform Type Identifier (UTI) for the edited image
+ if the photo has been edited, otherwise None;
+ for example: public.jpeg
+ """
+ if self._db._db_version >= _PHOTOS_5_VERSION:
+ return self.uti if self.hasadjustments else None
+ else:
+ return self._info["UTI_edited"]
@property
def uti_raw(self):
@@ -664,12 +748,17 @@ class PhotoInfo:
@property
def has_raw(self):
- """ returns True if photo has an associated RAW image, otherwise False """
+ """ returns True if photo has an associated raw image (that is, it's a RAW+JPEG pair), otherwise False """
return self._info["has_raw"]
+ @property
+ def israw(self):
+ """ returns True if photo is a raw image. For images with an associated RAW+JPEG pair, see has_raw """
+ return "raw-image" in self.uti_original
+
@property
def raw_original(self):
- """ returns True if associated RAW image and the RAW image is selected in Photos
+ """ returns True if associated raw image and the raw image is selected in Photos
via "Use RAW as Original "
otherwise returns False """
return self._info["raw_is_original"]
diff --git a/osxphotos/photosdb/photosdb.py b/osxphotos/photosdb/photosdb.py
index 0b21e3e6..a8164d67 100644
--- a/osxphotos/photosdb/photosdb.py
+++ b/osxphotos/photosdb/photosdb.py
@@ -846,7 +846,8 @@ class PhotosDB:
RKMaster.height,
RKMaster.width,
RKMaster.orientation,
- RKMaster.fileSize
+ RKMaster.fileSize,
+ RKVersion.subType
FROM RKVersion, RKMaster
WHERE RKVersion.masterUuid = RKMaster.uuid"""
)
@@ -873,7 +874,8 @@ class PhotosDB:
RKMaster.height,
RKMaster.width,
RKMaster.orientation,
- RKMaster.originalFileSize
+ RKMaster.originalFileSize,
+ RKVersion.subType
FROM RKVersion, RKMaster
WHERE RKVersion.masterUuid = RKMaster.uuid"""
)
@@ -919,6 +921,7 @@ class PhotosDB:
# 37 RKMaster.width,
# 38 RKMaster.orientation,
# 39 RKMaster.originalFileSize
+ # 40 RKVersion.subType
for row in c:
uuid = row[0]
@@ -989,6 +992,13 @@ class PhotosDB:
self._dbphotos[uuid]["UTI"] = row[22]
+ # The UTI in RKMaster will always be UTI of the original
+ # Unlike Photos 5 which changes the UTI to match latest edit
+ self._dbphotos[uuid]["UTI_original"] = row[22]
+
+ # UTI edited will be read from RKModelResource
+ self._dbphotos[uuid]["UTI_edited"] = None
+
# handle burst photos
# if burst photo, determine whether or not it's a selected burst photo
self._dbphotos[uuid]["burstUUID"] = row[23]
@@ -1055,11 +1065,6 @@ class PhotosDB:
self._dbphotos[uuid]["cloudAvailable"] = None
self._dbphotos[uuid]["incloud"] = None
- # TODO: NOT YET USED -- PLACEHOLDER for RAW processing (currently only in _process_database5)
- # original resource choice (e.g. RAW or jpeg)
- self._dbphotos[uuid]["original_resource_choice"] = None
- self._dbphotos[uuid]["raw_is_original"] = None
-
# associated RAW image info
self._dbphotos[uuid]["has_raw"] = True if row[25] == 7 else False
self._dbphotos[uuid]["UTI_raw"] = None
@@ -1071,6 +1076,25 @@ class PhotosDB:
self._dbphotos[uuid]["non_raw_master_uuid"] = row[30]
self._dbphotos[uuid]["alt_master_uuid"] = row[31]
+ # original resource choice (e.g. RAW or jpeg)
+ # In Photos 5+, original_resource_choice set from:
+ # ZADDITIONALASSETATTRIBUTES.ZORIGINALRESOURCECHOICE
+ # = 0 if jpeg is selected as "original" in Photos (the default)
+ # = 1 if RAW is selected as "original" in Photos
+ # RKVersion.subType, RAW always appears to be 16
+ # 4 = mov
+ # 16 = RAW
+ # 32 = JPEG
+ # 64 = TIFF
+ # 2048 = PNG
+ # 32768 = HIEC
+ self._dbphotos[uuid]["original_resource_choice"] = (
+ 1 if row[40] == 16 and self._dbphotos[uuid]["has_raw"] else 0
+ )
+ self._dbphotos[uuid]["raw_is_original"] = bool(
+ self._dbphotos[uuid]["original_resource_choice"]
+ )
+
# recently deleted items
self._dbphotos[uuid]["intrash"] = True if row[32] == 1 else False
@@ -1100,7 +1124,8 @@ class PhotosDB:
RKMaster.modelID,
RKMaster.fileSize,
RKMaster.isTrulyRaw,
- RKMaster.alternateMasterUuid
+ RKMaster.alternateMasterUuid,
+ RKMaster.filename
FROM RKMaster
"""
)
@@ -1116,6 +1141,7 @@ class PhotosDB:
# 7 RKMaster.fileSize,
# 8 RKMaster.isTrulyRaw,
# 9 RKMaster.alternateMasterUuid
+ # 10 RKMaster.filename
for row in c:
uuid = row[0]
@@ -1130,6 +1156,7 @@ class PhotosDB:
info["fileSize"] = row[7]
info["isTrulyRAW"] = row[8]
info["alternateMasterUuid"] = row[9]
+ info["filename"] = row[10]
self._dbphotos_master[uuid] = info
# get details needed to find path of the edited photos
@@ -1159,7 +1186,6 @@ class PhotosDB:
if (
row[1] != "UNADJUSTEDNONRAW"
and row[1] != "UNADJUSTED"
- # and row[4] == "public.jpeg"
and row[6] == 2
):
if "edit_resource_id" in self._dbphotos[uuid]:
@@ -1173,6 +1199,7 @@ class PhotosDB:
# should we return all edits or just most recent one?
# For now, return most recent edit
self._dbphotos[uuid]["edit_resource_id"] = row[2]
+ self._dbphotos[uuid]["UTI_edited"] = row[4]
# get details on external edits
c.execute(
@@ -1245,7 +1272,7 @@ class PhotosDB:
)
# Order of results
- # 0 RKMaster.uuid,
+ # 0 RKVersion.uuid,
# 1 RKMaster.cloudLibraryState,
# 2 RKCloudResource.available,
# 3 RKCloudResource.status
@@ -1329,6 +1356,13 @@ class PhotosDB:
if info["has_raw"]:
raw_uuid = info["raw_master_uuid"]
info["raw_info"] = self._dbphotos_master[raw_uuid]
+ info["UTI_raw"] = self._dbphotos_master[raw_uuid]["UTI"]
+ non_raw_uuid = info["non_raw_master_uuid"]
+ info["raw_pair_info"] = self._dbphotos_master[non_raw_uuid]
+ else:
+ info["raw_info"] = None
+ info["UTI_raw"] = None
+ info["raw_pair_info"] = None
# done with the database connection
conn.close()
@@ -2062,36 +2096,43 @@ class PhotosDB:
# determine if a photo is missing in Photos 5
# Get info on remote/local availability for photos in shared albums
+ # Also get UTI of original image (zdatastoresubtype = 1)
c.execute(
f""" SELECT
{asset_table}.ZUUID,
ZINTERNALRESOURCE.ZLOCALAVAILABILITY,
- ZINTERNALRESOURCE.ZREMOTEAVAILABILITY
+ ZINTERNALRESOURCE.ZREMOTEAVAILABILITY,
+ ZINTERNALRESOURCE.ZDATASTORESUBTYPE,
+ ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER,
+ ZUNIFORMTYPEIDENTIFIER.ZIDENTIFIER
FROM {asset_table}
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
+ JOIN ZUNIFORMTYPEIDENTIFIER ON ZUNIFORMTYPEIDENTIFIER.Z_PK = ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER
WHERE ZDATASTORESUBTYPE = 1 OR ZDATASTORESUBTYPE = 3 """
)
+ # Order of results:
+ # 0 {asset_table}.ZUUID,
+ # 1 ZINTERNALRESOURCE.ZLOCALAVAILABILITY,
+ # 2 ZINTERNALRESOURCE.ZREMOTEAVAILABILITY,
+ # 3 ZINTERNALRESOURCE.ZDATASTORESUBTYPE,
+ # 4 ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER,
+ # 5 ZUNIFORMTYPEIDENTIFIER.ZIDENTIFIER
+
for row in c:
uuid = row[0]
if uuid in self._dbphotos:
- # and self._dbphotos[uuid]["isMissing"] is None:
self._dbphotos[uuid]["localAvailability"] = row[1]
self._dbphotos[uuid]["remoteAvailability"] = row[2]
-
- # old = self._dbphotos[uuid]["isMissing"]
+ if row[3] == 1:
+ self._dbphotos[uuid]["UTI_original"] = row[5]
if row[1] != 1:
self._dbphotos[uuid]["isMissing"] = 1
else:
self._dbphotos[uuid]["isMissing"] = 0
- # if old is not None and old != self._dbphotos[uuid]["isMissing"]:
- # logging.warning(
- # f"{uuid} isMissing changed: {old} {self._dbphotos[uuid]['isMissing']}"
- # )
-
# get information on local/remote availability
c.execute(
f""" SELECT {asset_table}.ZUUID,
@@ -2108,18 +2149,11 @@ class PhotosDB:
self._dbphotos[uuid]["localAvailability"] = row[1]
self._dbphotos[uuid]["remoteAvailability"] = row[2]
- # old = self._dbphotos[uuid]["isMissing"]
-
if row[1] != 1:
self._dbphotos[uuid]["isMissing"] = 1
else:
self._dbphotos[uuid]["isMissing"] = 0
- # if old is not None and old != self._dbphotos[uuid]["isMissing"]:
- # logging.warning(
- # f"{uuid} isMissing changed: {old} {self._dbphotos[uuid]['isMissing']}"
- # )
-
# get information about cloud sync state
c.execute(
f""" SELECT
diff --git a/requirements.txt b/requirements.txt
index 3f3da02a..6077770b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -202,5 +202,6 @@ virtualenv==20.0.30
wcwidth==0.1.9
webencodings==0.5.1
wrapt==1.11.1
+wurlitzer==2.0.1
yarl==1.4.2
-zipp==0.5.2
+zipp==0.5.2
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 14d730db..c95e7994 100755
--- a/setup.py
+++ b/setup.py
@@ -78,6 +78,7 @@ setup(
"bpylist2==3.0.2",
"pathvalidate==2.2.1",
"dataclasses==0.7;python_version<'3.7'",
+ "wurlitzer>=2.0.1",
],
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
include_package_data=True,
diff --git a/tests/Test-10.14.6.photoslibrary/Masters/2020/06/27/20200627-160436/IMG_1064.jpeg b/tests/Test-10.14.6.photoslibrary/Masters/2020/06/27/20200627-160436/IMG_1064.jpeg
deleted file mode 100644
index 829a2345..00000000
Binary files a/tests/Test-10.14.6.photoslibrary/Masters/2020/06/27/20200627-160436/IMG_1064.jpeg and /dev/null differ
diff --git a/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041506/IMG_1997.JPG b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041506/IMG_1997.JPG
new file mode 100755
index 00000000..143e9532
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041506/IMG_1997.JPG differ
diff --git a/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041506/IMG_1997.cr2 b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041506/IMG_1997.cr2
new file mode 100755
index 00000000..f2adc62f
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041506/IMG_1997.cr2 differ
diff --git a/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041514/IMG_1994.JPG b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041514/IMG_1994.JPG
new file mode 100755
index 00000000..c1872812
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041514/IMG_1994.JPG differ
diff --git a/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041514/IMG_1994.cr2 b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041514/IMG_1994.cr2
new file mode 100755
index 00000000..6cd12fb4
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041514/IMG_1994.cr2 differ
diff --git a/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041520/IMG_1693.tif b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041520/IMG_1693.tif
new file mode 100644
index 00000000..69654411
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041520/IMG_1693.tif differ
diff --git a/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041542/DSC03584.dng b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041542/DSC03584.dng
new file mode 100755
index 00000000..1f96242c
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041542/DSC03584.dng differ
diff --git a/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041653/IMG_3092.heic b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041653/IMG_3092.heic
new file mode 100644
index 00000000..8f103814
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041653/IMG_3092.heic differ
diff --git a/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041858/IMG_3984.jpeg b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041858/IMG_3984.jpeg
new file mode 100644
index 00000000..ee8fcafb
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/Masters/2020/10/05/20201005-041858/IMG_3984.jpeg differ
diff --git a/tests/Test-10.14.6.photoslibrary/database/RKVersion_searchIndexText.skindex b/tests/Test-10.14.6.photoslibrary/database/RKVersion_searchIndexText.skindex
index 0befdb5e..2ae017ba 100644
Binary files a/tests/Test-10.14.6.photoslibrary/database/RKVersion_searchIndexText.skindex and b/tests/Test-10.14.6.photoslibrary/database/RKVersion_searchIndexText.skindex differ
diff --git a/tests/Test-10.14.6.photoslibrary/database/photos.db b/tests/Test-10.14.6.photoslibrary/database/photos.db
index 3b75f66f..d6d1d11d 100644
Binary files a/tests/Test-10.14.6.photoslibrary/database/photos.db and b/tests/Test-10.14.6.photoslibrary/database/photos.db differ
diff --git a/tests/Test-10.14.6.photoslibrary/private/com.apple.Photos/appPrivateData.plist b/tests/Test-10.14.6.photoslibrary/private/com.apple.Photos/appPrivateData.plist
index c4268fcf..56af9a8f 100644
--- a/tests/Test-10.14.6.photoslibrary/private/com.apple.Photos/appPrivateData.plist
+++ b/tests/Test-10.14.6.photoslibrary/private/com.apple.Photos/appPrivateData.plist
@@ -36,7 +36,7 @@
other
0
photos
- 6
+ 11
videos
0
diff --git a/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotoAnalysisServicePreferences.plist b/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotoAnalysisServicePreferences.plist
index d92dd897..ed56bc8a 100644
--- a/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotoAnalysisServicePreferences.plist
+++ b/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotoAnalysisServicePreferences.plist
@@ -3,8 +3,8 @@
PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate
- 2020-07-27T03:16:28Z
+ 2020-10-09T16:14:42Z
PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate
- 2020-07-27T12:35:43Z
+ 2020-10-10T05:21:03Z
diff --git a/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotosGraph/liveupdate-photosgraph.graphdb b/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotosGraph/liveupdate-photosgraph.graphdb
index 229d7b37..a654cafc 100644
Binary files a/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotosGraph/liveupdate-photosgraph.graphdb and b/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotosGraph/liveupdate-photosgraph.graphdb differ
diff --git a/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotosGraph/photosgraph.graphdb b/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotosGraph/photosgraph.graphdb
index 4147fb50..ecb7f869 100644
Binary files a/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotosGraph/photosgraph.graphdb and b/tests/Test-10.14.6.photoslibrary/private/com.apple.photoanalysisd/GraphService/PhotosGraph/photosgraph.graphdb differ
diff --git a/tests/Test-10.14.6.photoslibrary/private/com.apple.photomodel/appPrivateData.plist b/tests/Test-10.14.6.photoslibrary/private/com.apple.photomodel/appPrivateData.plist
index 46de063a..d56d365c 100644
--- a/tests/Test-10.14.6.photoslibrary/private/com.apple.photomodel/appPrivateData.plist
+++ b/tests/Test-10.14.6.photoslibrary/private/com.apple.photomodel/appPrivateData.plist
@@ -5,7 +5,7 @@
LithiumMessageTracer
LastReportedDate
- 2020-07-27T03:18:40Z
+ 2020-10-04T23:49:39Z
diff --git a/tests/Test-10.14.6.photoslibrary/resources/media/master/00/00/fullsizeoutput_10.jpeg b/tests/Test-10.14.6.photoslibrary/resources/media/master/00/00/fullsizeoutput_10.jpeg
new file mode 100644
index 00000000..348e66d3
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/media/master/00/00/fullsizeoutput_10.jpeg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/media/master/00/00/fullsizeoutput_e.jpeg b/tests/Test-10.14.6.photoslibrary/resources/media/master/00/00/fullsizeoutput_e.jpeg
new file mode 100644
index 00000000..d101f635
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/media/master/00/00/fullsizeoutput_e.jpeg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/media/master/00/00/fullsizeoutput_f.jpeg b/tests/Test-10.14.6.photoslibrary/resources/media/master/00/00/fullsizeoutput_f.jpeg
new file mode 100644
index 00000000..a29aa4e6
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/media/master/00/00/fullsizeoutput_f.jpeg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/media/version/00/00/fullsizeoutput_11.jpeg b/tests/Test-10.14.6.photoslibrary/resources/media/version/00/00/fullsizeoutput_11.jpeg
new file mode 100644
index 00000000..cbac3357
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/media/version/00/00/fullsizeoutput_11.jpeg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/moments/analysismetadata.plist b/tests/Test-10.14.6.photoslibrary/resources/moments/analysismetadata.plist
index da42fc31..284a9438 100644
--- a/tests/Test-10.14.6.photoslibrary/resources/moments/analysismetadata.plist
+++ b/tests/Test-10.14.6.photoslibrary/resources/moments/analysismetadata.plist
@@ -11,6 +11,6 @@
PLLastRevGeoForcedProviderOutOfDateCheckVersionKey
1
PLLastRevGeoVerFileFetchDateKey
- 2020-07-27T03:16:25Z
+ 2020-10-04T23:43:17Z
diff --git a/tests/Test-10.14.6.photoslibrary/resources/moments/historicalmarker.plist b/tests/Test-10.14.6.photoslibrary/resources/moments/historicalmarker.plist
index c9df3009..7505f349 100644
--- a/tests/Test-10.14.6.photoslibrary/resources/moments/historicalmarker.plist
+++ b/tests/Test-10.14.6.photoslibrary/resources/moments/historicalmarker.plist
@@ -3,7 +3,7 @@
LastHistoryRowId
- 707
+ 948
LibraryBuildTag
D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E
LibrarySchemaVersion
diff --git a/tests/Test-10.14.6.photoslibrary/resources/moments/needsanalysis b/tests/Test-10.14.6.photoslibrary/resources/moments/needsanalysis
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/8/UNADJUSTEDNONRAW_mini_8.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/8/UNADJUSTEDNONRAW_mini_8.jpg
deleted file mode 100644
index c720f1ff..00000000
Binary files a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/8/UNADJUSTEDNONRAW_mini_8.jpg and /dev/null differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/8/UNADJUSTEDNONRAW_thumb_8.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/8/UNADJUSTEDNONRAW_thumb_8.jpg
deleted file mode 100644
index 2a88f74d..00000000
Binary files a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/8/UNADJUSTEDNONRAW_thumb_8.jpg and /dev/null differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/9/UNADJUSTEDNONRAW_mini_9.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/9/UNADJUSTEDNONRAW_mini_9.jpg
new file mode 100644
index 00000000..f89eb2b5
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/9/UNADJUSTEDNONRAW_mini_9.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/9/UNADJUSTEDNONRAW_thumb_9.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/9/UNADJUSTEDNONRAW_thumb_9.jpg
new file mode 100644
index 00000000..5ddefee6
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/9/UNADJUSTEDNONRAW_thumb_9.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/a/UNADJUSTEDRAW_mini_a.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/a/UNADJUSTEDRAW_mini_a.jpg
new file mode 100644
index 00000000..9a30c450
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/a/UNADJUSTEDRAW_mini_a.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/a/UNADJUSTEDRAW_thumb_a.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/a/UNADJUSTEDRAW_thumb_a.jpg
new file mode 100644
index 00000000..ba6d7521
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/a/UNADJUSTEDRAW_thumb_a.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/b/UNADJUSTEDNONRAW_mini_b.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/b/UNADJUSTEDNONRAW_mini_b.jpg
new file mode 100644
index 00000000..58d242ca
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/b/UNADJUSTEDNONRAW_mini_b.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/b/UNADJUSTEDNONRAW_thumb_b.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/b/UNADJUSTEDNONRAW_thumb_b.jpg
new file mode 100644
index 00000000..c722a245
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/b/UNADJUSTEDNONRAW_thumb_b.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/c/UNADJUSTEDRAW_largepv_c.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/c/UNADJUSTEDRAW_largepv_c.jpg
new file mode 100644
index 00000000..98a979ea
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/c/UNADJUSTEDRAW_largepv_c.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/c/UNADJUSTEDRAW_mini_c.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/c/UNADJUSTEDRAW_mini_c.jpg
new file mode 100644
index 00000000..4edcb011
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/c/UNADJUSTEDRAW_mini_c.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/c/UNADJUSTEDRAW_thumb_c.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/c/UNADJUSTEDRAW_thumb_c.jpg
new file mode 100644
index 00000000..ecb59e1a
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/c/UNADJUSTEDRAW_thumb_c.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/d/kALjMq4oSXWM4XefUXj44Q_mini_d.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/d/kALjMq4oSXWM4XefUXj44Q_mini_d.jpg
new file mode 100644
index 00000000..99dcfae2
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/d/kALjMq4oSXWM4XefUXj44Q_mini_d.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/d/kALjMq4oSXWM4XefUXj44Q_thumb_d.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/d/kALjMq4oSXWM4XefUXj44Q_thumb_d.jpg
new file mode 100644
index 00000000..8ba6e932
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/d/kALjMq4oSXWM4XefUXj44Q_thumb_d.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/e/UNADJUSTEDNONRAW_mini_e.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/e/UNADJUSTEDNONRAW_mini_e.jpg
new file mode 100644
index 00000000..3f1db849
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/e/UNADJUSTEDNONRAW_mini_e.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/e/UNADJUSTEDNONRAW_thumb_e.jpg b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/e/UNADJUSTEDNONRAW_thumb_e.jpg
new file mode 100644
index 00000000..68983ca4
Binary files /dev/null and b/tests/Test-10.14.6.photoslibrary/resources/proxies/derivatives/00/00/e/UNADJUSTEDNONRAW_thumb_e.jpg differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/Info.plist b/tests/Test-10.14.6.photoslibrary/resources/recovery/Info.plist
index f85d3a18..facca9d6 100644
--- a/tests/Test-10.14.6.photoslibrary/resources/recovery/Info.plist
+++ b/tests/Test-10.14.6.photoslibrary/resources/recovery/Info.plist
@@ -9,7 +9,7 @@
HistoricalMarker
LastHistoryRowId
- 707
+ 948
LibraryBuildTag
D8C4AAA1-3AB6-4A65-BEBD-99CC3E5D433E
LibrarySchemaVersion
@@ -24,7 +24,7 @@
SnapshotCompletedDate
2019-07-27T13:16:43Z
SnapshotLastValidated
- 2020-07-27T03:18:40Z
+ 2020-10-10T05:22:36Z
SnapshotTables
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKAdjustmentData/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKAdjustmentData/0000000000.lij
index 64aa5fb3..7b0ea696 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKAdjustmentData/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKAdjustmentData/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKAlbum/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKAlbum/0000000000.lij
index 5f848d52..aba74b21 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKAlbum/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKAlbum/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKImageProxyState/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKImageProxyState/0000000000.lij
index b5b9516b..022ef058 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKImageProxyState/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKImageProxyState/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKImportGroup/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKImportGroup/0000000000.lij
index d5bf8f68..c31ef1f3 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKImportGroup/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKImportGroup/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKMaster/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKMaster/0000000000.lij
index f1557467..7a5bacc4 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKMaster/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKMaster/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKModelResource/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKModelResource/0000000000.lij
index 2b43df90..21018411 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKModelResource/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKModelResource/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKPlaceForVersion/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKPlaceForVersion/0000000000.lij
index c3aba0e9..bbb5f30d 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKPlaceForVersion/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKPlaceForVersion/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKSceneInVersion/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKSceneInVersion/0000000000.lij
index fa462206..7df1ad0a 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKSceneInVersion/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKSceneInVersion/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKVersion/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKVersion/0000000000.lij
index 26652c8e..fc3febb5 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKVersion/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKVersion/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKVersionAnalysisState/0000000000.lij b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKVersionAnalysisState/0000000000.lij
index 9f6eaa3e..557b6a8a 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/recovery/RKVersionAnalysisState/0000000000.lij and b/tests/Test-10.14.6.photoslibrary/resources/recovery/RKVersionAnalysisState/0000000000.lij differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/segments/Thumb64Segment_0.data b/tests/Test-10.14.6.photoslibrary/resources/segments/Thumb64Segment_0.data
index 6e55d71d..b5318d68 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/segments/Thumb64Segment_0.data and b/tests/Test-10.14.6.photoslibrary/resources/segments/Thumb64Segment_0.data differ
diff --git a/tests/Test-10.14.6.photoslibrary/resources/segments/ThumbJPGSegment_0.data b/tests/Test-10.14.6.photoslibrary/resources/segments/ThumbJPGSegment_0.data
index 8fb8e44d..cb0b5f6c 100644
Binary files a/tests/Test-10.14.6.photoslibrary/resources/segments/ThumbJPGSegment_0.data and b/tests/Test-10.14.6.photoslibrary/resources/segments/ThumbJPGSegment_0.data differ
diff --git a/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite-shm b/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite-shm
index 4823f4bd..3d8e0438 100644
Binary files a/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite-shm and b/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite-shm differ
diff --git a/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite-wal b/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite-wal
index 3d96f6a7..88518648 100644
Binary files a/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite-wal and b/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite-wal differ
diff --git a/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite.lock b/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite.lock
index 2342dda3..480501c1 100644
--- a/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite.lock
+++ b/tests/Test-10.15.6.photoslibrary/database/Photos.sqlite.lock
@@ -7,7 +7,7 @@
hostuuid
9575E48B-8D5F-5654-ABAC-4431B1167324
pid
- 80508
+ 2964
processname
photolibraryd
uid
diff --git a/tests/Test-10.15.6.photoslibrary/database/search/psi.sqlite b/tests/Test-10.15.6.photoslibrary/database/search/psi.sqlite
index ac583031..f3c95ffd 100644
Binary files a/tests/Test-10.15.6.photoslibrary/database/search/psi.sqlite and b/tests/Test-10.15.6.photoslibrary/database/search/psi.sqlite differ
diff --git a/tests/Test-10.15.6.photoslibrary/database/search/searchMetadata.plist b/tests/Test-10.15.6.photoslibrary/database/search/searchMetadata.plist
new file mode 100644
index 00000000..3eec4180
--- /dev/null
+++ b/tests/Test-10.15.6.photoslibrary/database/search/searchMetadata.plist
@@ -0,0 +1,188 @@
+
+
+
+
+ BlacklistedMeaningsByMeaning
+
+ MePersonUUID
+ 39488755-78C0-40B2-B378-EDA280E1823C
+ SceneWhitelist
+
+ Graduation
+ Aquarium
+ Food
+ Ice Skating
+ Mountain
+ Cliff
+ Basketball
+ Tennis
+ Jewelry
+ Cheese
+ Softball
+ Football
+ Circus
+ Jet Ski
+ Playground
+ Carousel
+ Paint Ball
+ Windsurfing
+ Sailboat
+ Sunbathing
+ Dam
+ Fireplace
+ Flower
+ Scuba
+ Hiking
+ Cetacean
+ Pier
+ Bowling
+ Snowboarding
+ Zoo
+ Snowmobile
+ Theater
+ Boat
+ Casino
+ Car
+ Diving
+ Cycling
+ Musical Instrument
+ Board Game
+ Castle
+ Sunset Sunrise
+ Martial Arts
+ Motocross
+ Submarine
+ Cat
+ Snow
+ Kiteboarding
+ Squash
+ Geyser
+ Music
+ Archery
+ Desert
+ Blackjack
+ Fireworks
+ Sportscar
+ Feline
+ Soccer
+ Museum
+ Baby
+ Fencing
+ Railroad
+ Nascar
+ Sky Surfing
+ Bird
+ Games
+ Baseball
+ Dressage
+ Snorkeling
+ Pyramid
+ Kite
+ Rowboat
+ Golf
+ Watersports
+ Lightning
+ Canyon
+ Auditorium
+ Night Sky
+ Karaoke
+ Skiing
+ Parade
+ Forest
+ Hot Air Balloon
+ Dragon Parade
+ Easter Egg
+ Monument
+ Jungle
+ Thanksgiving
+ Jockey Horse
+ Stadium
+ Airplane
+ Ballet
+ Yoga
+ Coral Reef
+ Skating
+ Wrestling
+ Bicycle
+ Tattoo
+ Amusement Park
+ Canoe
+ Cheerleading
+ Ping Pong
+ Fishing
+ Magic
+ Reptile
+ Winter Sport
+ Waterfall
+ Train
+ Bonsai
+ Surfing
+ Dog
+ Cake
+ Sledding
+ Sandcastle
+ Glacier
+ Lighthouse
+ Equestrian
+ Rafting
+ Shore
+ Hockey
+ Santa Claus
+ Formula One Car
+ Sport
+ Vehicle
+ Boxing
+ Rollerskating
+ Underwater
+ Orchestra
+ Carnival
+ Rocket
+ Skateboarding
+ Helicopter
+ Performance
+ Oktoberfest
+ Water Polo
+ Skate Park
+ Animal
+ Nightclub
+ String Instrument
+ Dinosaur
+ Gymnastics
+ Cricket
+ Volcano
+ Lake
+ Aurora
+ Dancing
+ Concert
+ Rock Climbing
+ Hang Glider
+ Rodeo
+ Fish
+ Art
+ Motorcycle
+ Volleyball
+ Wake Boarding
+ Badminton
+ Motor Sport
+ Sumo
+ Parasailing
+ Skydiving
+ Kickboxing
+ Pinata
+ Foosball
+ Go Kart
+ Poker
+ Kayak
+ Swimming
+ Atv
+ Beach
+ Dartboard
+ Athletics
+ Camping
+ Tornado
+ Billiards
+ Rugby
+ Airshow
+
+
+
diff --git a/tests/Test-10.15.6.photoslibrary/database/search/zeroKeywords.data b/tests/Test-10.15.6.photoslibrary/database/search/zeroKeywords.data
index a4dcac95..5d03620c 100644
Binary files a/tests/Test-10.15.6.photoslibrary/database/search/zeroKeywords.data and b/tests/Test-10.15.6.photoslibrary/database/search/zeroKeywords.data differ
diff --git a/tests/Test-10.15.6.photoslibrary/originals/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.heic b/tests/Test-10.15.6.photoslibrary/originals/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.heic
new file mode 100644
index 00000000..8f103814
Binary files /dev/null and b/tests/Test-10.15.6.photoslibrary/originals/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.heic differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm
index 811342b4..ddf44042 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-wal b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-wal
index a7b2e264..f3f296d2 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-wal and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-wal differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm
index c1559a14..a6e01a6d 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-wal b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-wal
index 13bbed76..e84ab701 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-wal and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-wal differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm
index 224e42df..5fd01404 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal
index 27ca9feb..fd9a5f60 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm
index baa82d09..0edff070 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-wal b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-wal
index 4b43a04c..465a7269 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-wal and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-wal differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm
index 8f27ed5e..69218fab 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-wal b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-wal
index 6f88eabe..47c8e394 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-wal and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-wal differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-shm b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-shm
index 02fdeb9f..3126e34e 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-shm and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-shm differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-wal b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-wal
index 05d5fb6f..9ef5f028 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-wal and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-wal differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm
index ebd79c90..dda17c2e 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-wal b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-wal
index d2e08b8f..4e26de8e 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-wal and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-wal differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb
index 6cdd833f..f6b8a764 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb differ
diff --git a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist
index 46a0d6b4..f35dce33 100644
Binary files a/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist and b/tests/Test-10.15.6.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist differ
diff --git a/tests/Test-10.15.6.photoslibrary/resources/derivatives/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_105_c.jpeg b/tests/Test-10.15.6.photoslibrary/resources/derivatives/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_105_c.jpeg
new file mode 100644
index 00000000..1aed5919
Binary files /dev/null and b/tests/Test-10.15.6.photoslibrary/resources/derivatives/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_105_c.jpeg differ
diff --git a/tests/Test-10.15.6.photoslibrary/resources/derivatives/masters/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_4_5005_c.jpeg b/tests/Test-10.15.6.photoslibrary/resources/derivatives/masters/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_4_5005_c.jpeg
new file mode 100644
index 00000000..ced2af2b
Binary files /dev/null and b/tests/Test-10.15.6.photoslibrary/resources/derivatives/masters/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_4_5005_c.jpeg differ
diff --git a/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/3305.ithmb b/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/3305.ithmb
index 08e0011f..acd4974d 100644
Binary files a/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/3305.ithmb and b/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/3305.ithmb differ
diff --git a/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/4031.ithmb b/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/4031.ithmb
index f3eaf950..47c751f8 100644
Binary files a/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/4031.ithmb and b/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/4031.ithmb differ
diff --git a/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/4132.ithmb b/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/4132.ithmb
index 595ff63c..eeef704f 100644
Binary files a/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/4132.ithmb and b/tests/Test-10.15.6.photoslibrary/resources/derivatives/thumbs/4132.ithmb differ
diff --git a/tests/Test-10.15.6.photoslibrary/resources/journals/Asset-change.plj b/tests/Test-10.15.6.photoslibrary/resources/journals/Asset-change.plj
index a0f1aa26..7d4df25e 100644
Binary files a/tests/Test-10.15.6.photoslibrary/resources/journals/Asset-change.plj and b/tests/Test-10.15.6.photoslibrary/resources/journals/Asset-change.plj differ
diff --git a/tests/Test-10.15.6.photoslibrary/resources/journals/HistoryToken.plist b/tests/Test-10.15.6.photoslibrary/resources/journals/HistoryToken.plist
index f0162126..46dcaaa9 100644
Binary files a/tests/Test-10.15.6.photoslibrary/resources/journals/HistoryToken.plist and b/tests/Test-10.15.6.photoslibrary/resources/journals/HistoryToken.plist differ
diff --git a/tests/Test-10.15.6.photoslibrary/resources/journals/ImportSession-change.plj b/tests/Test-10.15.6.photoslibrary/resources/journals/ImportSession-change.plj
index 9f5e1c56..20fe3a9c 100644
Binary files a/tests/Test-10.15.6.photoslibrary/resources/journals/ImportSession-change.plj and b/tests/Test-10.15.6.photoslibrary/resources/journals/ImportSession-change.plj differ
diff --git a/tests/Test-10.15.6.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.plist b/tests/Test-10.15.6.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.plist
new file mode 100644
index 00000000..7f47da2b
--- /dev/null
+++ b/tests/Test-10.15.6.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.plist
@@ -0,0 +1,68 @@
+
+
+
+
+ adjustmentBaseVersion
+ 0
+ adjustmentData
+
+ bVdbt6I6s/0vvtqjFygo9BjfA0jCRbklXMSzzgMCEi6CChp0j/3fv7i6++zeYxzekqrM
+ mlUJqZm/Zo/iNlR9Z3anfvbjr9nxXrW5cz8fi9vsx4yX9fVy9m2WXi7RT7/35Ir/zn3n
+ FxwzDBkpzikqHtVPI/9tdmnT8dTfzszTxfvZ399m52JM83RM3/jndBiLW1zlI5n9WHIL
+ gS2oLkVbdcU/Edi67wJD/+lsFFVJxtkPgVsuvs36W1V0Yzr+DMfQ37HS8f8WMwZpXt+H
+ 8czchtmP//nr//EouvTYFvnsx3i7FyyLYhyrrhzeBPvTiQ13fZa2u59xuW+/Jg02bt9z
+ w9dk1V3u42+f70tuvRTE5YqTFsJaWCzk36vAdOmH+634A0ht06zx+qr7E129vaG6YviJ
+ PrxzHMYq+2J14RbvIBy3loQlv1wvREHmeHnFrVn9RO7LJkvcUl7w4kpaLdaCyEmsEvex
+ j9L2/g7+fSGJK+6PjxXz+CcRhr7k5fVK5FmlRVla8Gvx22zsu7RFaVcWv4II4mrNCbK0
+ XItr8b19C/HLshQXssDz65Ww5kRJWiyZSZbeJlH+98eIfZXRTi9ss2F02ebc5Yijxo8V
+ cY/4xolVJ4Ywudob92LB8hir5zvkE1JvgrUlg6rhDsSMLdjynhnpbZzy+RjHcpq1/GK/
+ bLXPD2JRrQ29qMkTC2ZehJQJqgczBiTRIwfEqrUHbZLEG9200tZJpkcJl0p5DG80zMWI
+ jw5DtOAPR/4y7Y3oVKhnTcONE1ExZkSiSIMx4nMj4sgew5zhtgy3CeLmZnrp8Pj8CNei
+ xg/eQUVjeD6EKYyu0YMbhk1txacpa1tRgE9wwKrKgJJjpDYZCITQAPkeR8RigAfcttuY
+ G7MA8va+ikp0As6mlFu3Q/gUoT7SL1HzLD8/tlyYIdJwode7pmqd0Jwxo5yjvezUUOUE
+ w9+Al23cjMcvwEYsgwX0kpLTtdjnH1r0+REZ9qLcwCESrA0/xSVMpZASE0PVDKMJ7d24
+ xxGqtnirmknzi2HD9o6fGCB/+gIkrFZ+6MSttXJ0O8yRyjuysqeT24GnHwBiI63J9ghl
+ IYzKGD+9wkqz6hBeqi+C3Jsgwwt/4bWw0eLEUUvxHnkCcsnWAkhBAJkYdH6Y86KUJCGK
+ JieCk7/nUL6H57L9/GiWnx/4If/DMOSL34jh3nZzVRy9a8k2i87lbUKvPWy5uHOON15c
+ smNEru9j2fb7GEU3a6SXWJuf8F1Z7cGl+6oh96uGPGy9Zd8fcB6asZkUakf0c58TMu+N
+ LqltHB9Mvdk/VLDDnx/WlgEc9aaMb2qxRSmxV83rEOSyy3GfH3kwd3yeN4JwJwTlpBDt
+ SZGLQAzigawnvzVfwwsFSLc5++6hcQX6iKU2FRBSDyiSq718hMgOgoHiO9lKxsU/ZhN9
+ wCf1/FI4aTGHX8o81Sj1gRo+tSm5eDXv+lCRAu0AHKe/NuGTQsghjTI8AnyAjB7ECk7I
+ WG91++JXycJ8+ce0lG9vHJ0KWCO9o1EOQ2wWzrNh2cibMBjt3VK0H68rgmqIMwTP8JjF
+ dPJ9/eR4bavWYRzeDvjQmedLdFSjMeoUJ/O5nYbtdIvBYOK+tmtFsl9iVkBRNLqmfFZr
+ 3xZ7TyPTEWyoZ6J0bbhNuKaCqfmZV0SObLh5nMF2Ap3i6j41NMSd7Sdd7rTsZlcNtTVu
+ KPSNnezp46SuNtY2qhZqBOGoYKp8fpy0S+8YPpezBD2xXJw0P3E2SOati+nllF9qiAHj
+ QXK4kPNrR9nVw93FRNrfnbpsCkVPaNlVr7PBm4cL6XBwOvdz+vkha35jb8nuvD3ZrkQX
+ CHQJzlWTVYjDpkKRpQ0SrsetC5KXr+vKwVRWtZ5uIsHOq8qab+V2c/Pl0QjtQ4Bea20O
+ HID8AJp0f1V3V7D1Ix01pbHxcwtxnYYEZ/tswTYsL2jjS5oAzwRe3YPs1BX7W/VDSGXY
+ eVoq+Ve1c1w7QxOinV7beYLoSUd9qFdgsGrl5uCGtzT/tCu5LSS03W2Gp/ZSPFhKTti3
+ 50q/27uLVHebTW5t7UOrggGoEvbIujZwxppq8rA0s949wWv7oi+kOdAO/Qmp4MWuhr1O
+ aLcFGQYIyCDIcM3rXmpFcTNVrtHTop+aC6uTq5Usr6ddO1hY2zX39GplbgYh8YOL60VA
+ xJTfAJ/bmxv7YWHhmpLIsVoq5Eg+b2HWHJ6I1w9c4D1dYsDEBlQ6ANXOLWyejbonXuUo
+ tiYN3mbgdkHTOCV31NTMhqg8G7gZ3Ocrt2ylSDefH54phN0VR56xFEKiOhttEXoWla9g
+ GhLzaXdmFRIL+3frRZ++Fml2wN1dRkwjmW0SP2QnqfMCS9gJTf2o8oft9Pe2ekLrPJAr
+ GR/gVR7bqS2tjrvUFbiaVVlvcXizlsM8qHnHacKXX40LrTUxK4wD94q9oVKug+HU1AXr
+ EEZZnXFU7m7vi3cVgFvmZdO4hDolNVks4bbv7CqnEGRhR6UH2Er+hqienvWOOtlYa0CY
+ KkLI2lzsTTI1UnB+TUUOeeUYofain5W0RrA2rCbKpic2Oy6oSP4ErY136h5bbpKskRID
+ f0CDzxkQJa5H7AMUwkOppore0nLCgQbTJiabuIWHZJ+TQ6MJprP35UpbjPmSiqxh9k7k
+ f36gnZzh+3QyIWz8TSnt2REPdZSreiSchoncYZaMdaUlhkmPFakAEH10RFELDROHPh8A
+ WHqpssg0QcETlRvY00tHnjtD58ixlBONlqzXcp4e9aTE7hqKwr4kDxX24f5MElk3KWvj
+ DgFLwbPRloCicScq3rUnEwkErqA03LhNTHYXn//8eO7mxrV0NwpnaOFwHav702I9a0QW
+ gfs+SCen1zgBF6qrAtEOEQJzgAWP3aY7dqtGkMo+uJh+Sp4LbSEgf4IL4Ax+rQjsVwxd
+ rzKsrelHKZWu4JywfZYo6EIvpLwI9uEwIEhABPwUNUd219iaMj+CvRA2yOJ1ScnuvnzS
+ VCFI6Txjt6ejIkfVRzt7YdcwhSQIETiBqNk36iSB3g865Ny1rnesUqCgHZwExTWQbJw8
+ vSAooHVXFqWmhE7tz2PtOTgq5SyjbRB4dgiukoKtb8BB8Xa+vNX3wGPdEmvL0juVXKkt
+ +/8w6d++Bbjyp3xd/ku8chyTnpRUY/FLvvJv+fqH/H2/RPifwvgfSfxLIn+bEaY/t8Xz
+ bZa41VJgsp3pdlFeSdL679+KfNN34409Qf4Q6ZikeU/fCp15VTl7YlSn6uuphM/pbQz6
+ rpj9/b9//xc=
+
+ adjustmentEditorBundleID
+ com.apple.Photos
+ adjustmentFormatIdentifier
+ com.apple.photo
+ adjustmentFormatVersion
+ 1.5
+ adjustmentTimestamp
+ 2020-10-03T22:54:20Z
+
+
diff --git a/tests/Test-10.15.6.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_201_a.jpeg b/tests/Test-10.15.6.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_201_a.jpeg
new file mode 100644
index 00000000..b8b15f53
Binary files /dev/null and b/tests/Test-10.15.6.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_201_a.jpeg differ
diff --git a/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite b/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite
index d4cc056d..6c0e53ae 100644
Binary files a/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite and b/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite differ
diff --git a/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite-shm b/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite-shm
index 74770d55..dd767aea 100644
Binary files a/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite-shm and b/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite-shm differ
diff --git a/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite-wal b/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite-wal
index f8160ace..fa66655e 100644
Binary files a/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite-wal and b/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite-wal differ
diff --git a/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite.lock b/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite.lock
index 65649889..62ec7355 100644
--- a/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite.lock
+++ b/tests/Test-10.16.0.photoslibrary/database/Photos.sqlite.lock
@@ -7,7 +7,7 @@
hostuuid
9575E48B-8D5F-5654-ABAC-4431B1167324
pid
- 434
+ 561
processname
photolibraryd
uid
diff --git a/tests/Test-10.16.0.photoslibrary/database/search/psi.sqlite b/tests/Test-10.16.0.photoslibrary/database/search/psi.sqlite
index 3f06de8f..f3c95ffd 100644
Binary files a/tests/Test-10.16.0.photoslibrary/database/search/psi.sqlite and b/tests/Test-10.16.0.photoslibrary/database/search/psi.sqlite differ
diff --git a/tests/Test-10.16.0.photoslibrary/database/search/synonymsProcess.plist b/tests/Test-10.16.0.photoslibrary/database/search/synonymsProcess.plist
index 58e48cc1..f5ebcace 100644
Binary files a/tests/Test-10.16.0.photoslibrary/database/search/synonymsProcess.plist and b/tests/Test-10.16.0.photoslibrary/database/search/synonymsProcess.plist differ
diff --git a/tests/Test-10.16.0.photoslibrary/originals/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.heic b/tests/Test-10.16.0.photoslibrary/originals/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.heic
new file mode 100644
index 00000000..8f103814
Binary files /dev/null and b/tests/Test-10.16.0.photoslibrary/originals/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.heic differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite
index b2a22026..ab2ed09a 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm
index 5a588562..fe9ac284 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite
index fa6011dd..ef3166c4 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm
index e1f50db6..fe9ac284 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite
index 9512bfd3..c6a5cd4b 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm
index c6d19902..820f26f6 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite
index b07c8c9a..6fdfa43e 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm
index 4ef6a565..fe9ac284 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite
index 4ad6beb0..46823bb3 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm
index 8470135e..fe9ac284 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite
index e934fdda..a63daabb 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite
index 35d2bb8e..1cf3b570 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm
index 3299391e..8f63440e 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb
index 9a0064df..bc594a18 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb-shm b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb-shm
index 9292bfe0..fe9ac284 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb-shm and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb-shm differ
diff --git a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist
index 8eb87588..3d028a03 100644
Binary files a/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist and b/tests/Test-10.16.0.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/derivatives/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_105_c.jpeg b/tests/Test-10.16.0.photoslibrary/resources/derivatives/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_105_c.jpeg
new file mode 100644
index 00000000..ed80532d
Binary files /dev/null and b/tests/Test-10.16.0.photoslibrary/resources/derivatives/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_105_c.jpeg differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/derivatives/masters/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_4_5005_c.jpeg b/tests/Test-10.16.0.photoslibrary/resources/derivatives/masters/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_4_5005_c.jpeg
new file mode 100644
index 00000000..8b5467f6
Binary files /dev/null and b/tests/Test-10.16.0.photoslibrary/resources/derivatives/masters/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_4_5005_c.jpeg differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/3305.ithmb b/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/3305.ithmb
index 08e0011f..7ac044fd 100644
Binary files a/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/3305.ithmb and b/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/3305.ithmb differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/4031.ithmb b/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/4031.ithmb
index f3eaf950..d13bd530 100644
Binary files a/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/4031.ithmb and b/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/4031.ithmb differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/4132.ithmb b/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/4132.ithmb
index 595ff63c..d3210033 100644
Binary files a/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/4132.ithmb and b/tests/Test-10.16.0.photoslibrary/resources/derivatives/thumbs/4132.ithmb differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/journals/Album-change.plj b/tests/Test-10.16.0.photoslibrary/resources/journals/Album-change.plj
index b20737cd..c34ef490 100644
Binary files a/tests/Test-10.16.0.photoslibrary/resources/journals/Album-change.plj and b/tests/Test-10.16.0.photoslibrary/resources/journals/Album-change.plj differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/journals/Asset-change.plj b/tests/Test-10.16.0.photoslibrary/resources/journals/Asset-change.plj
index 2a28aff6..e77f0bf4 100644
Binary files a/tests/Test-10.16.0.photoslibrary/resources/journals/Asset-change.plj and b/tests/Test-10.16.0.photoslibrary/resources/journals/Asset-change.plj differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/journals/Folder-change.plj b/tests/Test-10.16.0.photoslibrary/resources/journals/Folder-change.plj
index 2a0e7fde..8ff6b386 100644
Binary files a/tests/Test-10.16.0.photoslibrary/resources/journals/Folder-change.plj and b/tests/Test-10.16.0.photoslibrary/resources/journals/Folder-change.plj differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/journals/HistoryToken.plist b/tests/Test-10.16.0.photoslibrary/resources/journals/HistoryToken.plist
index f0162126..deb6efcb 100644
Binary files a/tests/Test-10.16.0.photoslibrary/resources/journals/HistoryToken.plist and b/tests/Test-10.16.0.photoslibrary/resources/journals/HistoryToken.plist differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/journals/ImportSession-change.plj b/tests/Test-10.16.0.photoslibrary/resources/journals/ImportSession-change.plj
index 9f5e1c56..20fe3a9c 100644
Binary files a/tests/Test-10.16.0.photoslibrary/resources/journals/ImportSession-change.plj and b/tests/Test-10.16.0.photoslibrary/resources/journals/ImportSession-change.plj differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/journals/MigrationHistory-change.plj b/tests/Test-10.16.0.photoslibrary/resources/journals/MigrationHistory-change.plj
index 8d3b6eb4..3f36895c 100644
Binary files a/tests/Test-10.16.0.photoslibrary/resources/journals/MigrationHistory-change.plj and b/tests/Test-10.16.0.photoslibrary/resources/journals/MigrationHistory-change.plj differ
diff --git a/tests/Test-10.16.0.photoslibrary/resources/journals/MigrationHistory.plist b/tests/Test-10.16.0.photoslibrary/resources/journals/MigrationHistory.plist
index c5c87982..4ec37c7f 100644
--- a/tests/Test-10.16.0.photoslibrary/resources/journals/MigrationHistory.plist
+++ b/tests/Test-10.16.0.photoslibrary/resources/journals/MigrationHistory.plist
@@ -5,7 +5,7 @@
currentPayloadVersion
1
snapshotDate
- 2020-08-09T15:25:21Z
+ 2020-10-02T13:39:59Z
snapshotPayloadVersion
1
diff --git a/tests/Test-10.16.0.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.plist b/tests/Test-10.16.0.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.plist
new file mode 100644
index 00000000..080450e0
--- /dev/null
+++ b/tests/Test-10.16.0.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266.plist
@@ -0,0 +1,68 @@
+
+
+
+
+ adjustmentBaseVersion
+ 0
+ adjustmentData
+
+ bVfbkqM6sv0Xv1ZHl8BgQ0fMAyCJm8EgbsZT84ABczVggwvsHfvfj1zdfXZPxPCGMrVy
+ KSVlLv21+sxvY9V3enfuVz/+Wp3uVZvZ98spv61+rFgg8WuBS1ffVskwhD9d6fiaAd+Z
+ 7wwDqGFMy/ySkPyz+mlkvq2GNpnO/e1CPffeYfX3t9Uln5IsmZJXiEsyTvktqrKpXP1Y
+ A5ajE6ohb6su/ycCnfedo+g/nbW8Kspp9YMDa/bbqr9VeTcl089wFP0VK5n+fzJlkGT1
+ fZwu1G1c/fj3X//DI++SU5tnqx/T7Z7TVeTTVHXF+CLYn8/0d9enSbv7GRd8+zWo0f/2
+ NTZ+DVbdcJ9++3xnhA0QNzzgOLAFPMMKv2ehZejH+y3/A0huk7Rx+qr7E12+vaC6fPyJ
+ Pr7WOE5V+sVqAOwrCABbgVsz6y3LcyJgxA3Y0vzx4MsmCmAtsgy/ETbsluMBZZDcpz5M
+ 2vsr+HdW4Dfgj48m8/QnEYq+ZsTthmdopnlRYJkt/2019V3SkqQr8l9BOH6z2fKABZQF
+ y1KPgeW/TGueFTmG2W44mgBBYNfUJAovEy/+90eZfeXRSga62xgPZgaGkxc2biTxB8I0
+ diTbEcbxYCnmYODiFMmXO2bislb8rTGhqgFhqUcGbhlHD9U2SphsiiIxSVuGPaxb+PG+
+ 4Bm2gRM2WWzg1AmJNKPlqEeojNXQRpFsHFAYx5Gi6kbS2vHyWeC1VJyC2xxkYsi0xzFk
+ meOJGZaDFp5z+QKh19jhzEe4jYMQ4ogwmRaC8uDhjOK2FLfxo+amO8n8+fEebDewHZ2j
+ TKbgcgwSHF7DT+42emczOi9p2/IcfqAjmUUKFCeh3KTI53wNZQcvLA0KePTa1ozAlPqY
+ sQ5VWJAzspVCbO2OeOeQ9KE6hM2j+Hg3QZCSsgGB0+912TiTN8psBhasrUSTxdjDvwEH
+ M2qm0xdgwxc+i524ACqMXOYTko/3ULPYQsFjyBkKs3gFPkn+XOoelvUgXMhhH/UkJJXp
+ mbIeN78YNnTvmIUCMucvwJLmyg3sqDU2tmoFGZEZW5QOi7zvUOX6qLRc2MQHIqcBDovQ
+ ezhnw0yrYzBUXwTBiyDFC37htbiBUWzLBXMPHY7siWkgSSCI6B7q3CBjeCGOAzdc7JBO
+ i0CZHfClaD/em/XHu/cp/sMwYPLfiMHBsjOZn5xrQTdrfhPNeL72uAVRZ59GzK/pMSqv
+ r2PZ9lFEyM2Y5iGCb2fvLm0OaOi+cgh+5ZDBrdP13dHLAj3S41zuSvXSZ2X51mtdXFte
+ dNTYJvqUpZ338W6bFOCkNkV4kxOTJKW1aZ5HPxP3DPp4z/034rJY84Md5xWzUMLHTPYy
+ ilA0ltvFbfTn+CQ+TW2j3x152KA+pEtbcoxnB87CHj5dl5Q7jNLZuy+mqA3uKV3mO37M
+ jutyZxgB7ym9JXCeHSQHD7jEg+MzexdLgvcMkW331yZ4LBgDFxYUr0TUTxtQJHlxOdUG
+ a/VuFbD60zolhXiDxeyqBUcg6W04Aw97em5XjbgHohLAydqteWuGV4KWwEsJvuBdGi6y
+ 66pn22lbuQ6i4Hb0jp0eDeFJDqewk+zUBTvoWUdTQTdd6WvLlwSrZtIc87zWNedHtXWt
+ oXdguZyQMjs6ibbavgkECWjQTZ08tEVtn0UpbhfUSXtVmjVIwMV6zGvzGd+sqpktCG65
+ qljxYf48yxvFMMOKlUOEJ4nM0sd7AY+9rbkgg8Xo8AV7fp1qhfCMMehONjNrSCRb9UbB
+ blzg1vrH+64e73slEw53uy6aSFL1ueiq50Vr9eNQdp5/vvYf7xRahFJjmWTXmWfLFmaW
+ oC72MlmnOQKeLs0u3eOt50/mHgVPh1Wloy7xtZooIWdlVWW8mWIr31xx0gLr6JOnAN+Q
+ jWTXw/p8uMq7G9y4gUqaQlOszJDnC127bT5aZAbFQBRLgBy+lPi6P4qkruh9VY/BLOLO
+ gYngXuXa3luFu5C5U2sri8l8pliB6kk3o5auttcwBrTyXQFMTObWfIwP+BAcXAh20LeX
+ Sr1bu0GoOkXJDNM6tjIakSx4TrmuNS9tdlXwaUC93j3Q03zOTwJtbAU0nCw9UakfVDJf
+ DJB6iCARwdSrGdVJjDBqlmqv9XPeL8VA82TDAlzgQ69tj9taPlgcT3jT/aB0/WHvhBLn
+ zYyMXHDQFetueNyQlKFttDOXEfFi4rQ5PmxGPQLPqcxSZWILzcIRyVZqePpF8/vSqayP
+ dwsKo6OMYOcXte3OJ2WJd5gUF80rxv3jmRmWlCfK1tG5oLt6xNHWXFDKtgLZYG/M4hUt
+ Y6xX1kWvgtLwKJHn/HBhCCnhaa/on7BMLZ24PpSlzvENbsc19efj+GnZ49RWd2xcxuJa
+ Dp+oLk7t0hZGB4a6QoNepbWpBDdjPYp+zdh2EDzdamJhq3v0Qtr4IFnKLGQYjOemTmiP
+ 0Irq4oXF7pYeCNn46FY46TKtsTqXdcmusTlerCqbadkIulm4o53k0mazV9PRlhcK2qAg
+ kbiANrrQWcRZM9EFyHmGGekUyu2gXqSkJnKtGU2YLg9P74BfldkDtRbZyb5n7NN46woR
+ cnsyFkDDcrB3iH7EXHAs5ERSw7lYPAfipIlKJWrxMY6yMmwgp9sHV6zgZcrWM09bZm+H
+ 7sc72YkpLW8nHePGUSgwPeKBKh9lNeTO41LecZpOdQVjTZ+TqswR4l33RMIWrXUvcBkP
+ 4cJJJDaFnEQeEt/gfh668rHTVFCeCjGGRWErLnDUsC8Lb7/FPHcoypuM++BwKWORXm7a
+ yI0SdZxjyWaJ8mZPce7wQWVCKW+wMF6B4pW7o8t8vD92H+/atdgrrwIVjMNU3R8GbVtT
+ aZT40PvJYvQQAJLLtNTzVkBk9IYUQAs6LYSkCXEhunTX3aR8sJDlXLfELBRHt5Y4ndal
+ vVNppqm7QTILVxTFdKOFGXWBE8wMjw7BbSS4RFhyEtIktNjQs/F2QhoXNMRg1FFK7654
+ hjLwkvkthS7ttsSQ1clNn5Wp6VzsBwSdUdgcGnkRUO/6HbHv8NDbhst9onC0YxLVSHC9
+ +LEL/Bwbd5ctoOTatfsWwQc9QgXQtbYhqOoI3sQZnd8gQ3J2rmiqB+TQhunBdeGcC1DA
+ df8vKv/blwiX/pSw6w3L/SlhN99Wc1lN+S8Jy7wk7B8S+PUaYX6K439k8S+Z/G1VUglq
+ 5o+XWQCbNbd5+bBUxLP85u/fqlzpu+lGnyF/CHWvTLJ+fql06lVl9JlRnauvF5N3SW6T
+ 33f56u///P1/
+
+ adjustmentEditorBundleID
+ com.apple.Photos
+ adjustmentFormatIdentifier
+ com.apple.photo
+ adjustmentFormatVersion
+ 1.5
+ adjustmentTimestamp
+ 2020-10-04T06:00:14Z
+
+
diff --git a/tests/Test-10.16.0.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_201_a.heic b/tests/Test-10.16.0.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_201_a.heic
new file mode 100644
index 00000000..c7b00d6c
Binary files /dev/null and b/tests/Test-10.16.0.photoslibrary/resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_201_a.heic differ
diff --git a/tests/export_db_version1.db b/tests/export_db_version1.db
new file mode 100644
index 00000000..3bf0bdf8
Binary files /dev/null and b/tests/export_db_version1.db differ
diff --git a/tests/test-images/DSC03584.dng b/tests/test-images/DSC03584.dng
new file mode 100755
index 00000000..1f96242c
Binary files /dev/null and b/tests/test-images/DSC03584.dng differ
diff --git a/tests/test-images/IMG_1994.JPG b/tests/test-images/IMG_1994.JPG
new file mode 100755
index 00000000..c1872812
Binary files /dev/null and b/tests/test-images/IMG_1994.JPG differ
diff --git a/tests/test-images/IMG_1994.cr2 b/tests/test-images/IMG_1994.cr2
new file mode 100755
index 00000000..6cd12fb4
Binary files /dev/null and b/tests/test-images/IMG_1994.cr2 differ
diff --git a/tests/test-images/IMG_1997.JPG b/tests/test-images/IMG_1997.JPG
new file mode 100755
index 00000000..143e9532
Binary files /dev/null and b/tests/test-images/IMG_1997.JPG differ
diff --git a/tests/test-images/IMG_1997.cr2 b/tests/test-images/IMG_1997.cr2
new file mode 100755
index 00000000..f2adc62f
Binary files /dev/null and b/tests/test-images/IMG_1997.cr2 differ
diff --git a/tests/test-images/IMG_3092.heic b/tests/test-images/IMG_3092.heic
new file mode 100644
index 00000000..8f103814
Binary files /dev/null and b/tests/test-images/IMG_3092.heic differ
diff --git a/tests/test_bigsur_10_16_0.py b/tests/test_bigsur_10_16_0.py
index 11906c95..58aaee93 100644
--- a/tests/test_bigsur_10_16_0.py
+++ b/tests/test_bigsur_10_16_0.py
@@ -1,5 +1,7 @@
import pytest
+from collections import namedtuple
+
from osxphotos._constants import _UNKNOWN_PERSON
@@ -7,8 +9,8 @@ PHOTOS_DB = "tests/Test-10.16.0.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-10.16.0.photoslibrary/database/photos.db"
PHOTOS_LIBRARY_PATH = "/Test-10.16.0.photoslibrary"
-PHOTOS_DB_LEN = 15
-PHOTOS_NOT_IN_TRASH_LEN = 13
+PHOTOS_DB_LEN = 16
+PHOTOS_NOT_IN_TRASH_LEN = 14
PHOTOS_IN_TRASH_LEN = 2
KEYWORDS = [
@@ -31,6 +33,8 @@ ALBUMS = [
"Raw",
"I have a deleted twin", # there's an empty album with same name that has been deleted
"EmptyAlbum",
+ "2018-10 - Sponsion, Museum, Frühstück, Römermuseum",
+ "2019-10/11 Paris Clermont",
]
KEYWORDS_DICT = {
"Kids": 4,
@@ -51,6 +55,8 @@ ALBUM_DICT = {
"Raw": 4,
"I have a deleted twin": 1,
"EmptyAlbum": 0,
+ "2018-10 - Sponsion, Museum, Frühstück, Römermuseum": 1,
+ "2019-10/11 Paris Clermont": 1,
} # Note: there are 2 albums named "Test Album" for testing duplicate album names
UUID_DICT = {
@@ -87,6 +93,82 @@ ALBUM_SORT_ORDER = [
]
ALBUM_KEY_PHOTO = "D79B8D77-BFFC-460B-9312-034F2877D35B"
+UTI_DICT = {
+ "8846E3E6-8AC8-4857-8448-E3D025784410": "public.tiff",
+ "7783E8E6-9CAC-40F3-BE22-81FB7051C266": "public.heic",
+ "1EB2B765-0765-43BA-A90C-0D0580E6172C": "public.jpeg",
+ "4D521201-92AC-43E5-8F7C-59BC41C37A96": "public.jpeg",
+}
+
+UTI_ORIGINAL_DICT = {
+ "8846E3E6-8AC8-4857-8448-E3D025784410": "public.tiff",
+ "7783E8E6-9CAC-40F3-BE22-81FB7051C266": "public.heic",
+ "1EB2B765-0765-43BA-A90C-0D0580E6172C": "public.jpeg",
+ "4D521201-92AC-43E5-8F7C-59BC41C37A96": "public.jpeg",
+}
+
+RawInfo = namedtuple(
+ "RawInfo",
+ [
+ "comment",
+ "original_filename",
+ "has_raw",
+ "israw",
+ "raw_original",
+ "uti",
+ "uti_original",
+ "uti_raw",
+ ],
+)
+RAW_DICT = {
+ "D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068": RawInfo(
+ "raw image, no jpeg pair",
+ "DSC03584.dng",
+ False,
+ True,
+ False,
+ "com.adobe.raw-image",
+ "com.adobe.raw-image",
+ None,
+ ),
+ "A92D9C26-3A50-4197-9388-CB5F7DB9FA91": RawInfo(
+ "raw+jpeg, jpeg original",
+ "IMG_1994.JPG",
+ True,
+ False,
+ False,
+ "public.jpeg",
+ "public.jpeg",
+ "com.canon.cr2-raw-image",
+ ),
+ "4D521201-92AC-43E5-8F7C-59BC41C37A96": RawInfo(
+ "raw+jpeg, raw original",
+ "IMG_1997.JPG",
+ True,
+ False,
+ True,
+ "public.jpeg",
+ "public.jpeg",
+ "com.canon.cr2-raw-image",
+ ),
+ "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51": RawInfo(
+ "jpeg, no raw",
+ "wedding.jpg",
+ False,
+ False,
+ False,
+ "public.jpeg",
+ "public.jpeg",
+ None,
+ ),
+}
+
+# HEIC image that's been edited in Big Sur, resulting edit is .HEIC
+UUID_HEIC_EDITED = "7783E8E6-9CAC-40F3-BE22-81FB7051C266"
+PATH_HEIC_EDITED = (
+ "resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_201_a.heic"
+)
+
def test_init1():
# test named argument
@@ -425,7 +507,7 @@ def test_external_edit2():
assert p.external_edit == False
-def test_path_edited1():
+def test_path_edited_jpeg():
# test a valid edited path
import os.path
import osxphotos
@@ -441,6 +523,17 @@ def test_path_edited1():
assert os.path.exists(path)
+def test_path_edited_heic():
+ # test a valid edited path for .heic image
+ import pathlib
+ import osxphotos
+
+ photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+ photo = photosdb.get_photo(UUID_HEIC_EDITED)
+ assert photo.path_edited.endswith(PATH_HEIC_EDITED)
+ assert pathlib.Path(photo.path_edited).is_file()
+
+
def test_path_edited2():
# test an invalid edited path
import osxphotos
@@ -617,9 +710,7 @@ def test_get_db_connection():
assert isinstance(conn, sqlite3.Connection)
assert isinstance(cursor, sqlite3.Cursor)
- results = conn.execute(
- "SELECT ZUUID FROM ZASSET WHERE ZFAVORITE = 1;"
- ).fetchall()
+ results = conn.execute("SELECT ZUUID FROM ZASSET WHERE ZFAVORITE = 1;").fetchall()
assert len(results) == 1
assert results[0][0] == "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51" # uuid
@@ -1032,7 +1123,7 @@ def test_from_to_date():
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28))
- assert len(photos) == 6
+ assert len(photos) == 7
photos = photosdb.photos(to_date=dt.datetime(2018, 10, 28))
assert len(photos) == 7
@@ -1044,11 +1135,13 @@ def test_from_to_date():
def test_date_invalid():
- """ Test date is invalid """
+ """ Test date is invalid """
from datetime import datetime, timedelta, timezone
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+ # UUID_DICT["date_invalid"] has an invalid date that's
+ # been manually adjusted in the database
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
assert len(photos) == 1
p = photos[0]
@@ -1063,7 +1156,37 @@ def test_date_modified_invalid():
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+ # UUID_DICT["date_invalid"] has an invalid modified date that's
+ # been manually adjusted in the database
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
assert len(photos) == 1
p = photos[0]
assert p.date_modified is None
+
+
+def test_uti():
+ """ test uti """
+ import osxphotos
+
+ photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+
+ for uuid, uti in UTI_DICT.items():
+ photo = photosdb.get_photo(uuid)
+ assert photo.uti == uti
+ assert photo.uti_original == UTI_ORIGINAL_DICT[uuid]
+
+
+def test_raw():
+ """ Test various raw properties """
+ import osxphotos
+
+ photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+
+ for uuid, rawinfo in RAW_DICT.items():
+ photo = photosdb.get_photo(uuid)
+ assert photo.original_filename == rawinfo.original_filename
+ assert photo.has_raw == rawinfo.has_raw
+ assert photo.israw == rawinfo.israw
+ assert photo.uti == rawinfo.uti
+ assert photo.uti_original == rawinfo.uti_original
+ assert photo.uti_raw == rawinfo.uti_raw
diff --git a/tests/test_catalina_10_15_6.py b/tests/test_catalina_10_15_6.py
index 21a9c035..b592c223 100644
--- a/tests/test_catalina_10_15_6.py
+++ b/tests/test_catalina_10_15_6.py
@@ -9,9 +9,10 @@ PHOTOS_DB = "tests/Test-10.15.6.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-10.15.6.photoslibrary/database/photos.db"
PHOTOS_LIBRARY_PATH = "/Test-10.15.6.photoslibrary"
-PHOTOS_DB_LEN = 15
-PHOTOS_NOT_IN_TRASH_LEN = 13
+PHOTOS_DB_LEN = 16
+PHOTOS_NOT_IN_TRASH_LEN = 14
PHOTOS_IN_TRASH_LEN = 2
+PHOTOS_DB_IMPORT_SESSIONS = 11
KEYWORDS = [
"Kids",
@@ -94,6 +95,19 @@ ALBUM_SORT_ORDER = [
]
ALBUM_KEY_PHOTO = "D79B8D77-BFFC-460B-9312-034F2877D35B"
+UTI_DICT = {
+ "8846E3E6-8AC8-4857-8448-E3D025784410": "public.tiff",
+ "7783E8E6-9CAC-40F3-BE22-81FB7051C266": "public.jpeg",
+ "1EB2B765-0765-43BA-A90C-0D0580E6172C": "public.jpeg",
+}
+
+
+UTI_ORIGINAL_DICT = {
+ "8846E3E6-8AC8-4857-8448-E3D025784410": "public.tiff",
+ "7783E8E6-9CAC-40F3-BE22-81FB7051C266": "public.heic",
+ "1EB2B765-0765-43BA-A90C-0D0580E6172C": "public.jpeg",
+}
+
def test_init1():
# test named argument
@@ -1046,7 +1060,7 @@ def test_from_to_date():
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28))
- assert len(photos) == 6
+ assert len(photos) == 7
photos = photosdb.photos(to_date=dt.datetime(2018, 10, 28))
assert len(photos) == 7
@@ -1134,7 +1148,7 @@ def test_import_session_count():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
import_sessions = photosdb.import_info
- assert len(import_sessions) == 10
+ assert len(import_sessions) == PHOTOS_DB_IMPORT_SESSIONS
def test_import_session_photo():
@@ -1177,3 +1191,15 @@ def test_import_session_photo():
)
assert len(import_session.photos) == 1
+
+def test_uti():
+ """ test uti """
+ import osxphotos
+
+ photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+
+ for uuid, uti in UTI_DICT.items():
+ photo = photosdb.get_photo(uuid)
+ assert photo.uti == uti
+ assert photo.uti_original == UTI_ORIGINAL_DICT[uuid]
+
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 91e7e3d4..49c4d7be 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -93,6 +93,47 @@ CLI_EXPORT_FILENAMES_CURRENT = [
"F12384F6-CD17-4151-ACBA-AE0E3688539E.jpeg",
]
+CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = [
+ "DSC03584.jpeg",
+ "IMG_1693.jpeg",
+ "IMG_1994.JPG",
+ "IMG_1994.cr2",
+ "IMG_1997.JPG",
+ "IMG_1997.cr2",
+ "IMG_3092.jpeg",
+ "IMG_3092_edited.jpeg",
+ "IMG_4547.jpg",
+ "Pumkins1.jpg",
+ "Pumkins2.jpg",
+ "Pumpkins3.jpg",
+ "St James Park.jpg",
+ "St James Park_edited.jpeg",
+ "Tulips.jpg",
+ "Tulips_edited.jpeg",
+ "wedding.jpg",
+ "wedding_edited.jpeg",
+]
+
+CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = [
+ "DSC03584.jpeg",
+ "IMG_1693.jpeg",
+ "IMG_1994.JPG",
+ "IMG_1997.JPG",
+ "IMG_3092.jpeg",
+ "IMG_3092_edited.jpeg",
+ "IMG_4547.jpg",
+ "Pumkins1.jpg",
+ "Pumkins2.jpg",
+ "Pumpkins3.jpg",
+ "St James Park.jpg",
+ "St James Park_edited.jpeg",
+ "Tulips.jpg",
+ "Tulips_edited.jpeg",
+ "wedding.jpg",
+ "wedding_edited.jpeg",
+]
+
+CLI_EXPORT_CONVERT_TO_JPEG_LARGE_FILE = "DSC03584.jpeg"
CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1 = [
"2019/April/wedding.jpg",
@@ -235,7 +276,7 @@ CLI_TEMPLATE_SIDECAR_FILENAME = "Pumkins1.jpg.json"
CLI_UUID_DICT_14_6 = {"intrash": "3tljdX43R8+k6peNHVrJNQ"}
-PHOTOS_NOT_IN_TRASH_LEN_14_6 = 7
+PHOTOS_NOT_IN_TRASH_LEN_14_6 = 12
PHOTOS_IN_TRASH_LEN_14_6 = 1
PHOTOS_MISSING_14_6 = 1
@@ -243,6 +284,10 @@ PHOTOS_NOT_IN_TRASH_LEN_15_5 = 13
PHOTOS_IN_TRASH_LEN_15_5 = 2
PHOTOS_MISSING_15_5 = 2
+PHOTOS_NOT_IN_TRASH_LEN_15_6 = 14
+PHOTOS_IN_TRASH_LEN_15_6 = 2
+PHOTOS_MISSING_15_6 = 1
+
CLI_PLACES_JSON = """{"places": {"_UNKNOWN_": 1, "Maui, Wailea, Hawai'i, United States": 1, "Washington, District of Columbia, United States": 1}}"""
CLI_EXIFTOOL = {
@@ -821,6 +866,97 @@ def test_export_edited_suffix():
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_EDITED_SUFFIX)
+@pytest.mark.skipif(
+ "OSXPHOTOS_TEST_CONVERT" not in os.environ,
+ reason="Skip if running in Github actions, no GPU.",
+)
+def test_export_convert_to_jpeg():
+ """ test --convert-to-jpeg """
+ import glob
+ import os
+ import os.path
+ import pathlib
+ from osxphotos.__main__ import export
+
+ runner = CliRunner()
+ cwd = os.getcwd()
+ # pylint: disable=not-context-manager
+ with runner.isolated_filesystem():
+ result = runner.invoke(
+ export, [os.path.join(cwd, PHOTOS_DB_15_6), ".", "-V", "--convert-to-jpeg"]
+ )
+ assert result.exit_code == 0
+ files = glob.glob("*")
+ assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG)
+ large_file = pathlib.Path(CLI_EXPORT_CONVERT_TO_JPEG_LARGE_FILE)
+ assert large_file.stat().st_size > 10000000
+
+
+@pytest.mark.skipif(
+ "OSXPHOTOS_TEST_CONVERT" not in os.environ,
+ reason="Skip if running in Github actions, no GPU.",
+)
+def test_export_convert_to_jpeg_quality():
+ """ test --convert-to-jpeg --jpeg-quality """
+ import glob
+ import os
+ import os.path
+ import pathlib
+ from osxphotos.__main__ import export
+
+ runner = CliRunner()
+ cwd = os.getcwd()
+ # pylint: disable=not-context-manager
+ with runner.isolated_filesystem():
+ result = runner.invoke(
+ export,
+ [
+ os.path.join(cwd, PHOTOS_DB_15_6),
+ ".",
+ "-V",
+ "--convert-to-jpeg",
+ "--jpeg-quality",
+ "0.2",
+ ],
+ )
+ assert result.exit_code == 0
+ files = glob.glob("*")
+ assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG)
+ large_file = pathlib.Path(CLI_EXPORT_CONVERT_TO_JPEG_LARGE_FILE)
+ assert large_file.stat().st_size < 1000000
+
+
+@pytest.mark.skipif(
+ "OSXPHOTOS_TEST_CONVERT" not in os.environ,
+ reason="Skip if running in Github actions, no GPU.",
+)
+def test_export_convert_to_jpeg_skip_raw():
+ """ test --convert-to-jpeg """
+ import glob
+ import os
+ import os.path
+ import pathlib
+ from osxphotos.__main__ import export
+
+ runner = CliRunner()
+ cwd = os.getcwd()
+ # pylint: disable=not-context-manager
+ with runner.isolated_filesystem():
+ result = runner.invoke(
+ export,
+ [
+ os.path.join(cwd, PHOTOS_DB_15_6),
+ ".",
+ "-V",
+ "--convert-to-jpeg",
+ "--skip-raw",
+ ],
+ )
+ assert result.exit_code == 0
+ files = glob.glob("*")
+ assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW)
+
+
def test_query_date_1():
""" Test --from-date and --to-date """
import json
@@ -1339,11 +1475,11 @@ def test_query_deleted_2():
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
- query, ["--json", "--db", os.path.join(cwd, PHOTOS_DB_14_6), "--deleted"]
+ query, ["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_6), "--deleted"]
)
assert result.exit_code == 0
json_got = json.loads(result.output)
- assert len(json_got) == PHOTOS_NOT_IN_TRASH_LEN_14_6 + PHOTOS_IN_TRASH_LEN_14_6
+ assert len(json_got) == PHOTOS_NOT_IN_TRASH_LEN_15_6 + PHOTOS_IN_TRASH_LEN_15_6
def test_query_deleted_3():
@@ -1376,11 +1512,11 @@ def test_query_deleted_4():
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
- query, ["--json", "--db", os.path.join(cwd, PHOTOS_DB_14_6), "--deleted-only"]
+ query, ["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_6), "--deleted-only"]
)
assert result.exit_code == 0
json_got = json.loads(result.output)
- assert len(json_got) == PHOTOS_IN_TRASH_LEN_14_6
+ assert len(json_got) == PHOTOS_IN_TRASH_LEN_15_6
assert json_got[0]["intrash"]
@@ -2636,8 +2772,12 @@ def test_export_update_no_db():
export, [os.path.join(cwd, CLI_PHOTOS_DB), ".", "--update"]
)
assert result.exit_code == 0
+
+ # unedited files will be skipped because their signatures will compare but
+ # edited files will be re-exported because there won't be an edited signature
+ # in the database
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 8 photos, updated EXIF data: 0 photos"
+ "Exported: 0 photos, updated: 2 photos, skipped: 6 photos, updated EXIF data: 0 photos"
in result.output
)
assert os.path.isfile(OSXPHOTOS_EXPORT_DB)
@@ -2811,7 +2951,7 @@ def test_export_touch_files():
)
assert result.exit_code == 0
- assert "Exported: 16 photos, touched date: 14 photos" in result.output
+ assert "Exported: 18 photos, touched date: 16 photos" in result.output
for fname, mtime in zip(CLI_EXPORT_BY_DATE, CLI_EXPORT_BY_DATE_TOUCH_TIMES):
st = os.stat(fname)
@@ -2843,7 +2983,7 @@ def test_export_touch_files_update():
)
assert result.exit_code == 0
- assert "Exported: 16 photos" in result.output
+ assert "Exported: 18 photos" in result.output
assert not pathlib.Path(CLI_EXPORT_BY_DATE[0]).is_file()
@@ -2853,7 +2993,7 @@ def test_export_touch_files_update():
)
assert result.exit_code == 0
- assert "Exported: 16 photos" in result.output
+ assert "Exported: 18 photos" in result.output
assert pathlib.Path(CLI_EXPORT_BY_DATE[0]).is_file()
@@ -2865,7 +3005,7 @@ def test_export_touch_files_update():
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 16 photos, updated EXIF data: 0 photos"
+ "Exported: 0 photos, updated: 0 photos, skipped: 18 photos, updated EXIF data: 0 photos"
in result.output
)
@@ -2883,7 +3023,7 @@ def test_export_touch_files_update():
)
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 16 photos, updated EXIF data: 0 photos, touched date: 14 photos"
+ "Exported: 0 photos, updated: 0 photos, skipped: 18 photos, updated EXIF data: 0 photos, touched date: 16 photos"
in result.output
)
@@ -2906,7 +3046,7 @@ def test_export_touch_files_update():
)
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 16 photos, updated EXIF data: 0 photos, touched date: 14 photos"
+ "Exported: 0 photos, updated: 0 photos, skipped: 18 photos, updated EXIF data: 0 photos, touched date: 16 photos"
in result.output
)
@@ -2932,7 +3072,7 @@ def test_export_touch_files_update():
)
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 1 photo, skipped: 15 photos, updated EXIF data: 0 photos, touched date: 1 photo"
+ "Exported: 0 photos, updated: 1 photo, skipped: 17 photos, updated EXIF data: 0 photos, touched date: 1 photo"
in result.output
)
@@ -2948,7 +3088,7 @@ def test_export_touch_files_update():
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 16 photos, updated EXIF data: 0 photos"
+ "Exported: 0 photos, updated: 0 photos, skipped: 18 photos, updated EXIF data: 0 photos"
in result.output
)
@@ -2980,7 +3120,7 @@ def test_export_touch_files_exiftool_update():
)
assert result.exit_code == 0
- assert "Exported: 16 photos" in result.output
+ assert "Exported: 18 photos" in result.output
assert not pathlib.Path(CLI_EXPORT_BY_DATE[0]).is_file()
@@ -2990,7 +3130,7 @@ def test_export_touch_files_exiftool_update():
)
assert result.exit_code == 0
- assert "Exported: 16 photos" in result.output
+ assert "Exported: 18 photos" in result.output
assert pathlib.Path(CLI_EXPORT_BY_DATE[0]).is_file()
@@ -3002,7 +3142,7 @@ def test_export_touch_files_exiftool_update():
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 16 photos, updated EXIF data: 0 photos"
+ "Exported: 0 photos, updated: 0 photos, skipped: 18 photos, updated EXIF data: 0 photos"
in result.output
)
@@ -3021,7 +3161,7 @@ def test_export_touch_files_exiftool_update():
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 16 photos, skipped: 0 photos, updated EXIF data: 16 photos"
+ "Exported: 0 photos, updated: 18 photos, skipped: 0 photos, updated EXIF data: 18 photos"
in result.output
)
@@ -3039,7 +3179,7 @@ def test_export_touch_files_exiftool_update():
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 16 photos, skipped: 0 photos, updated EXIF data: 16 photos"
+ "Exported: 0 photos, updated: 18 photos, skipped: 0 photos, updated EXIF data: 18 photos"
in result.output
)
@@ -3058,7 +3198,7 @@ def test_export_touch_files_exiftool_update():
)
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 16 photos, updated EXIF data: 0 photos, touched date: 14 photos"
+ "Exported: 0 photos, updated: 0 photos, skipped: 18 photos, updated EXIF data: 0 photos, touched date: 18 photos"
in result.output
)
@@ -3076,7 +3216,7 @@ def test_export_touch_files_exiftool_update():
)
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 16 photos, updated EXIF data: 0 photos, touched date: 14 photos"
+ "Exported: 0 photos, updated: 0 photos, skipped: 18 photos, updated EXIF data: 0 photos, touched date: 18 photos"
in result.output
)
@@ -3101,7 +3241,7 @@ def test_export_touch_files_exiftool_update():
)
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 1 photo, skipped: 15 photos, updated EXIF data: 1 photo, touched date: 1 photo"
+ "Exported: 0 photos, updated: 1 photo, skipped: 17 photos, updated EXIF data: 1 photo, touched date: 1 photo"
in result.output
)
@@ -3123,7 +3263,7 @@ def test_export_touch_files_exiftool_update():
)
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 16 photos, updated EXIF data: 0 photos, touched date: 0 photos"
+ "Exported: 0 photos, updated: 0 photos, skipped: 18 photos, updated EXIF data: 0 photos, touched date: 0 photos"
in result.output
)
@@ -3141,7 +3281,7 @@ def test_export_touch_files_exiftool_update():
assert result.exit_code == 0
assert (
- "Exported: 0 photos, updated: 0 photos, skipped: 16 photos, updated EXIF data: 0 photos"
+ "Exported: 0 photos, updated: 0 photos, skipped: 18 photos, updated EXIF data: 0 photos"
in result.output
)
diff --git a/tests/test_export_convert_to_jpeg.py b/tests/test_export_convert_to_jpeg.py
new file mode 100644
index 00000000..e992b15e
--- /dev/null
+++ b/tests/test_export_convert_to_jpeg.py
@@ -0,0 +1,62 @@
+import os
+import pytest
+
+from osxphotos._constants import _UNKNOWN_PERSON
+
+skip_test = "OSXPHOTOS_TEST_CONVERT" not in os.environ
+pytestmark = pytest.mark.skipif(
+ skip_test, reason="Skip if running on GitHub actions, no GPU."
+)
+
+PHOTOS_DB = "tests/Test-10.15.6.photoslibrary"
+
+UUID_DICT = {
+ "raw": "D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068",
+ "heic": "7783E8E6-9CAC-40F3-BE22-81FB7051C266",
+}
+
+NAMES_DICT = {
+ "raw": "D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068.jpeg",
+ "heic": "7783E8E6-9CAC-40F3-BE22-81FB7051C266.jpeg",
+}
+
+
+@pytest.fixture(scope="module")
+def photosdb():
+ import osxphotos
+
+ return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+
+
+def test_export_convert_raw_to_jpeg(photosdb):
+ # test export with convert_to_jpeg
+ import pathlib
+ import tempfile
+
+ tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ dest = tempdir.name
+ photos = photosdb.photos(uuid=[UUID_DICT["raw"]])
+
+ results = photos[0].export2(dest, convert_to_jpeg=True)
+ got_dest = pathlib.Path(results.exported[0])
+
+ assert got_dest.is_file()
+ assert got_dest.suffix == ".jpeg"
+ assert got_dest.name == NAMES_DICT["raw"]
+
+
+def test_export_convert_heic_to_jpeg(photosdb):
+ # test export with convert_to_jpeg
+ import pathlib
+ import tempfile
+
+ tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ dest = tempdir.name
+ photos = photosdb.photos(uuid=[UUID_DICT["heic"]])
+
+ results = photos[0].export2(dest, convert_to_jpeg=True)
+ got_dest = pathlib.Path(results.exported[0])
+
+ assert got_dest.is_file()
+ assert got_dest.suffix == ".jpeg"
+ assert got_dest.name == NAMES_DICT["heic"]
diff --git a/tests/test_export_db.py b/tests/test_export_db.py
index b1c7569e..df48cbbc 100644
--- a/tests/test_export_db.py
+++ b/tests/test_export_db.py
@@ -6,18 +6,22 @@ EXIF_DATA = """[{"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphoto
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"}}"""
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"}}"""
+DATABASE_VERSION1 = "tests/export_db_version1.db"
def test_export_db():
""" test ExportDB """
import os
import tempfile
- from osxphotos._export_db import ExportDB
+ from osxphotos.export_db import ExportDB, OSXPHOTOS_EXPORTDB_VERSION
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
dbname = os.path.join(tempdir.name, ".osxphotos_export.db")
db = ExportDB(dbname)
assert os.path.isfile(dbname)
+ assert db.was_created
+ assert not db.was_upgraded
+ assert db.version == OSXPHOTOS_EXPORTDB_VERSION
filepath = os.path.join(tempdir.name, "test.JPG")
filepath_lower = os.path.join(tempdir.name, "test.jpg")
@@ -33,24 +37,42 @@ def test_export_db():
assert db.get_stat_orig_for_file(filepath) == (1, 2, 3)
db.set_stat_exif_for_file(filepath, (4, 5, 6))
assert db.get_stat_exif_for_file(filepath) == (4, 5, 6)
+ db.set_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))
+ assert db.get_stat_converted_for_file(filepath) == (7, 8, 9)
# test set_data which sets all at the same time
filepath2 = os.path.join(tempdir.name, "test2.jpg")
- db.set_data(filepath2, "BAR-FOO", (1, 2, 3), (4, 5, 6), INFO_DATA, EXIF_DATA)
+ db.set_data(
+ filepath2,
+ "BAR-FOO",
+ (1, 2, 3),
+ (4, 5, 6),
+ (7, 8, 9),
+ (10, 11, 12),
+ INFO_DATA,
+ EXIF_DATA,
+ )
assert db.get_uuid_for_file(filepath2) == "BAR-FOO"
assert db.get_info_for_uuid("BAR-FOO") == INFO_DATA
assert db.get_exifdata_for_file(filepath2) == EXIF_DATA
assert db.get_stat_orig_for_file(filepath2) == (1, 2, 3)
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_edited_for_file(filepath2) == (10, 11, 12)
# close and re-open
db.close()
db = ExportDB(dbname)
+ assert not db.was_created
assert db.get_uuid_for_file(filepath2) == "BAR-FOO"
assert db.get_info_for_uuid("BAR-FOO") == INFO_DATA
assert db.get_exifdata_for_file(filepath2) == EXIF_DATA
assert db.get_stat_orig_for_file(filepath2) == (1, 2, 3)
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_edited_for_file(filepath2) == (10, 11, 12)
# update data
db.set_uuid_for_file(filepath, "FUBAR")
@@ -61,10 +83,13 @@ def test_export_db_no_op():
""" test ExportDBNoOp """
import os
import tempfile
- from osxphotos._export_db import ExportDBNoOp
+ from osxphotos.export_db import ExportDBNoOp, OSXPHOTOS_EXPORTDB_VERSION
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
db = ExportDBNoOp()
+ assert db.was_created
+ assert not db.was_upgraded
+ assert db.version == OSXPHOTOS_EXPORTDB_VERSION
filepath = os.path.join(tempdir.name, "test.JPG")
filepath_lower = os.path.join(tempdir.name, "test.jpg")
@@ -80,15 +105,30 @@ def test_export_db_no_op():
assert db.get_stat_orig_for_file(filepath) is None
db.set_stat_exif_for_file(filepath, (4, 5, 6))
assert db.get_stat_exif_for_file(filepath) is None
+ db.set_stat_converted_for_file(filepath, (7, 8, 9))
+ assert db.get_stat_converted_for_file(filepath) is None
+ db.set_stat_edited_for_file(filepath, (10, 11, 12))
+ assert db.get_stat_edited_for_file(filepath) is None
# test set_data which sets all at the same time
filepath2 = os.path.join(tempdir.name, "test2.jpg")
- db.set_data(filepath2, "BAR-FOO", (1, 2, 3), (4, 5, 6), INFO_DATA, EXIF_DATA)
+ db.set_data(
+ filepath2,
+ "BAR-FOO",
+ (1, 2, 3),
+ (4, 5, 6),
+ (7, 8, 9),
+ (10, 11, 12),
+ INFO_DATA,
+ EXIF_DATA,
+ )
assert db.get_uuid_for_file(filepath2) is None
assert db.get_info_for_uuid("BAR-FOO") is None
assert db.get_exifdata_for_file(filepath2) is None
assert db.get_stat_orig_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_edited_for_file(filepath) is None
# update data
db.set_uuid_for_file(filepath, "FUBAR")
@@ -99,7 +139,11 @@ def test_export_db_in_memory():
""" test ExportDBInMemory """
import os
import tempfile
- from osxphotos._export_db import ExportDB, ExportDBInMemory
+ from osxphotos.export_db import (
+ ExportDB,
+ ExportDBInMemory,
+ OSXPHOTOS_EXPORTDB_VERSION,
+ )
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
dbname = os.path.join(tempdir.name, ".osxphotos_export.db")
@@ -114,10 +158,15 @@ def test_export_db_in_memory():
db.set_exifdata_for_file(filepath, EXIF_DATA)
db.set_stat_orig_for_file(filepath, (1, 2, 3))
db.set_stat_exif_for_file(filepath, (4, 5, 6))
+ db.set_stat_converted_for_file(filepath, (7, 8, 9))
+ db.set_stat_edited_for_file(filepath, (10, 11, 12))
db.close()
dbram = ExportDBInMemory(dbname)
+ assert not dbram.was_created
+ assert not dbram.was_upgraded
+ assert dbram.version == OSXPHOTOS_EXPORTDB_VERSION
# verify values as expected
assert dbram.get_uuid_for_file(filepath_lower) == "FOO-BAR"
@@ -125,6 +174,8 @@ def test_export_db_in_memory():
assert dbram.get_exifdata_for_file(filepath) == EXIF_DATA
assert dbram.get_stat_orig_for_file(filepath) == (1, 2, 3)
assert dbram.get_stat_exif_for_file(filepath) == (4, 5, 6)
+ assert dbram.get_stat_converted_for_file(filepath) == (7, 8, 9)
+ assert dbram.get_stat_edited_for_file(filepath) == (10, 11, 12)
# change a value
dbram.set_uuid_for_file(filepath, "FUBAR")
@@ -132,12 +183,16 @@ def test_export_db_in_memory():
dbram.set_exifdata_for_file(filepath, EXIF_DATA2)
dbram.set_stat_orig_for_file(filepath, (7, 8, 9))
dbram.set_stat_exif_for_file(filepath, (10, 11, 12))
+ dbram.set_stat_converted_for_file(filepath, (1, 2, 3))
+ dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
assert dbram.get_exifdata_for_file(filepath) == EXIF_DATA2
assert dbram.get_stat_orig_for_file(filepath) == (7, 8, 9)
assert dbram.get_stat_exif_for_file(filepath) == (10, 11, 12)
+ assert dbram.get_stat_converted_for_file(filepath) == (1, 2, 3)
+ assert dbram.get_stat_edited_for_file(filepath) == (4, 5, 6)
dbram.close()
@@ -148,5 +203,42 @@ def test_export_db_in_memory():
assert db.get_exifdata_for_file(filepath) == EXIF_DATA
assert db.get_stat_orig_for_file(filepath) == (1, 2, 3)
assert db.get_stat_exif_for_file(filepath) == (4, 5, 6)
+ assert db.get_stat_converted_for_file(filepath) == (7, 8, 9)
+ assert db.get_stat_edited_for_file(filepath) == (10, 11, 12)
assert db.get_info_for_uuid("FUBAR") is None
+
+
+def test_export_db_in_memory_nofile():
+ """ test ExportDBInMemory with no dbfile """
+ import os
+ import tempfile
+ from osxphotos.export_db import ExportDBInMemory, OSXPHOTOS_EXPORTDB_VERSION
+
+ tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ filepath = os.path.join(tempdir.name, "test.JPG")
+ filepath_lower = os.path.join(tempdir.name, "test.jpg")
+
+ dbram = ExportDBInMemory(os.path.join(tempdir.name, "NOT_A_DATABASE_FILE.db"))
+ assert dbram.was_created
+ assert not dbram.was_upgraded
+ assert dbram.version == OSXPHOTOS_EXPORTDB_VERSION
+
+ # change a value
+ dbram.set_uuid_for_file(filepath, "FUBAR")
+ dbram.set_info_for_uuid("FUBAR", INFO_DATA2)
+ dbram.set_exifdata_for_file(filepath, EXIF_DATA2)
+ dbram.set_stat_orig_for_file(filepath, (7, 8, 9))
+ dbram.set_stat_exif_for_file(filepath, (10, 11, 12))
+ dbram.set_stat_converted_for_file(filepath, (1, 2, 3))
+ dbram.set_stat_edited_for_file(filepath, (4, 5, 6))
+
+ assert dbram.get_uuid_for_file(filepath_lower) == "FUBAR"
+ assert dbram.get_info_for_uuid("FUBAR") == INFO_DATA2
+ assert dbram.get_exifdata_for_file(filepath) == EXIF_DATA2
+ assert dbram.get_stat_orig_for_file(filepath) == (7, 8, 9)
+ assert dbram.get_stat_exif_for_file(filepath) == (10, 11, 12)
+ assert dbram.get_stat_converted_for_file(filepath) == (1, 2, 3)
+ assert dbram.get_stat_edited_for_file(filepath) == (4, 5, 6)
+
+ dbram.close()
diff --git a/tests/test_fileutil.py b/tests/test_fileutil.py
index c50db763..474d04b5 100644
--- a/tests/test_fileutil.py
+++ b/tests/test_fileutil.py
@@ -1,7 +1,11 @@
""" test FileUtil """
+import os
import pytest
+TEST_HEIC = "tests/test-images/IMG_3092.heic"
+TEST_RAW = "tests/test-images/DSC03584.dng"
+
def test_copy_file_valid():
# copy file with valid src, dest
@@ -22,8 +26,8 @@ def test_copy_file_invalid():
from osxphotos.fileutil import FileUtil
temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
- src = "tests/test-images/wedding_DOES_NOT_EXIST.jpg"
with pytest.raises(Exception) as e:
+ src = "tests/test-images/wedding_DOES_NOT_EXIST.jpg"
assert FileUtil.copy(src, temp_dir.name)
assert e.type == FileNotFoundError
@@ -67,3 +71,39 @@ def test_unlink_file():
assert os.path.isfile(dest)
FileUtil.unlink(dest)
assert not os.path.isfile(dest)
+
+
+@pytest.mark.skipif(
+ "OSXPHOTOS_TEST_CONVERT" not in os.environ,
+ reason="Skip if running in Github actions, no GPU.",
+)
+def test_convert_to_jpeg():
+ """ test convert_to_jpeg """
+ import pathlib
+ import tempfile
+ from osxphotos.fileutil import FileUtil
+
+ temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ with temp_dir:
+ imgfile = pathlib.Path(TEST_HEIC)
+ outfile = pathlib.Path(temp_dir.name) / f"{imgfile.stem}.jpeg"
+ assert FileUtil.convert_to_jpeg(imgfile, outfile)
+ assert outfile.is_file()
+
+@pytest.mark.skipif(
+ "OSXPHOTOS_TEST_CONVERT" not in os.environ,
+ reason="Skip if running in Github actions, no GPU.",
+)
+def test_convert_to_jpeg_quality():
+ """ test convert_to_jpeg with compression_quality """
+ import pathlib
+ import tempfile
+ from osxphotos.fileutil import FileUtil
+
+ temp_dir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ with temp_dir:
+ imgfile = pathlib.Path(TEST_RAW)
+ outfile = pathlib.Path(temp_dir.name) / f"{imgfile.stem}.jpeg"
+ assert FileUtil.convert_to_jpeg(imgfile, outfile, compression_quality=0.1)
+ assert outfile.is_file()
+ assert outfile.stat().st_size < 1000000
diff --git a/tests/test_image_converter.py b/tests/test_image_converter.py
new file mode 100644
index 00000000..566accf3
--- /dev/null
+++ b/tests/test_image_converter.py
@@ -0,0 +1,115 @@
+""" test ImageConverter """
+
+import os
+
+import pytest
+
+skip_test = "OSXPHOTOS_TEST_CONVERT" not in os.environ
+pytestmark = pytest.mark.skipif(
+ skip_test, reason="Skip if running on GitHub actions, no GPU."
+)
+
+
+TEST_HEIC = "tests/test-images/IMG_3092.heic"
+TEST_RAW = "tests/test-images/IMG_0476_2.CR2"
+TEST_JPEG = "tests/test-images/IMG_3984.jpeg"
+TEST_IMAGES = [TEST_HEIC, TEST_RAW, TEST_JPEG]
+TEST_NOT_AN_IMAGE = "tests/README.md"
+TEST_IMAGE_DOES_NOT_EXIST = "tests/test-images/NOT-A-FILE.heic"
+
+
+def test_image_converter_singleton():
+ """ test that ImageConverter is a singleton """
+ from osxphotos.imageconverter import ImageConverter
+
+ convert1 = ImageConverter()
+ convert2 = ImageConverter()
+
+ assert convert1 == convert2
+
+
+def test_image_converter():
+ """ test conversion of different image types """
+ import pathlib
+ import tempfile
+ from osxphotos.imageconverter import ImageConverter
+
+ converter = ImageConverter()
+ tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ with tempdir:
+ for imgfile in TEST_IMAGES:
+ imgfile = pathlib.Path(imgfile)
+ outfile = pathlib.Path(tempdir.name) / f"{imgfile.stem}.jpeg"
+ outfile2 = pathlib.Path(tempdir.name) / f"{imgfile.stem}_2.jpeg"
+
+ # call write_jpeg with both pathlib.Path and str arguments
+ assert converter.write_jpeg(imgfile, outfile)
+ assert converter.write_jpeg(str(imgfile), str(outfile2))
+ assert outfile.is_file()
+ assert outfile2.is_file()
+
+
+def test_image_converter_compression_quality():
+ """ test conversion of different image types with custom compression quality """
+ import pathlib
+ import tempfile
+ from osxphotos.imageconverter import ImageConverter
+
+ converter = ImageConverter()
+ tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ with tempdir:
+ for imgfile in TEST_IMAGES:
+ imgfile = pathlib.Path(imgfile)
+ outfile = pathlib.Path(tempdir.name) / f"{imgfile.stem}.jpeg"
+
+ # call write_jpeg with both pathlib.Path and str arguments
+ assert converter.write_jpeg(imgfile, outfile, compression_quality=0.1)
+ assert outfile.is_file()
+ assert outfile.stat().st_size < 1000000
+
+
+def test_image_converter_bad_compression_quality():
+ """ test illegal compression quality """
+ import pathlib
+ import tempfile
+ from osxphotos.imageconverter import ImageConverter
+
+ converter = ImageConverter()
+ tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ with tempdir:
+ imgfile = pathlib.Path(TEST_HEIC)
+ outfile = pathlib.Path(tempdir.name) / f"{imgfile.stem}.jpeg"
+ with pytest.raises(ValueError):
+ converter.write_jpeg(imgfile, outfile, compression_quality=2.0)
+ with pytest.raises(ValueError):
+ converter.write_jpeg(imgfile, outfile, compression_quality=-1.0)
+
+
+def test_image_converter_bad_file():
+ """ Try to convert a file that's not an image """
+ import pathlib
+ import tempfile
+ from osxphotos.imageconverter import ImageConverter
+
+ converter = ImageConverter()
+ tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ with tempdir:
+ imgfile = pathlib.Path(TEST_NOT_AN_IMAGE)
+ outfile = pathlib.Path(tempdir.name) / f"{imgfile.stem}.jpeg"
+ assert not converter.write_jpeg(imgfile, outfile)
+
+
+def test_image_converter_missing_file():
+ """ Try to convert a file that's not an image """
+ import pathlib
+ import tempfile
+ from osxphotos.imageconverter import ImageConverter
+
+ converter = ImageConverter()
+ tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
+ with tempdir:
+ imgfile = pathlib.Path(TEST_IMAGE_DOES_NOT_EXIST)
+ outfile = pathlib.Path(tempdir.name) / f"{imgfile.stem}.jpeg"
+ with pytest.raises(FileNotFoundError):
+ converter.write_jpeg(imgfile, outfile)
+
diff --git a/tests/test_mojave_10_14_6.py b/tests/test_mojave_10_14_6.py
index 508af580..967abad8 100644
--- a/tests/test_mojave_10_14_6.py
+++ b/tests/test_mojave_10_14_6.py
@@ -1,3 +1,4 @@
+import pytest
from osxphotos._constants import _UNKNOWN_PERSON
@@ -41,9 +42,46 @@ UUID_DICT = {
"favorite": "6bxcNnzRQKGnK4uPrCJ9UQ",
"not_favorite": "8SOE9s0XQVGsuq4ONohTng",
"date_invalid": "YZFCPY24TUySvpu7owiqxA",
- "intrash": "3tljdX43R8+k6peNHVrJNQ",
+ "hidden": "od0fmC7NQx+ayVr+%i06XA",
+ "not_hidden": "6bxcNnzRQKGnK4uPrCJ9UQ",
+ "intrash": "td4yIljYS8aRAgzlsRRDtQ",
"not_intrash": "6bxcNnzRQKGnK4uPrCJ9UQ",
+ "location": "3Jn73XpSQQCluzRBMWRsMA",
+ "no_location": "YZFCPY24TUySvpu7owiqxA",
"has_adjustments": "6bxcNnzRQKGnK4uPrCJ9UQ",
+ "no_adjustments": "15uNd7%8RguTEgNPKHfTWw",
+ "external_edits": "3Jn73XpSQQCluzRBMWRsMA",
+ "no_external_edits": "6bxcNnzRQKGnK4uPrCJ9UQ",
+ "raw": "DZAgPwQNTWiM+T5cX3WMqA",
+ "jpeg+raw": "AcxIpfolT3KU2Ge84VG3yQ",
+ "raw+jpeg": "oTiMG6OfSP6d%nUTEOfvMg",
+ "heic": "GdJJPQX0RP63mcdKFj%sfQ",
+}
+
+RAW_PATH_DICT = {
+ "jpeg+raw": "/Masters/2020/10/05/20201005-041506/IMG_1997.cr2",
+ "raw+jpeg": "Masters/2020/10/05/20201005-041514/IMG_1994.cr2",
+}
+UUID_UTI_DICT = {
+ "DZAgPwQNTWiM+T5cX3WMqA": [
+ "com.adobe.raw-image",
+ None,
+ "com.adobe.raw-image",
+ None,
+ ],
+ "AcxIpfolT3KU2Ge84VG3yQ": [
+ "public.jpeg",
+ "com.canon.cr2-raw-image",
+ "public.jpeg",
+ None,
+ ],
+ "oTiMG6OfSP6d%nUTEOfvMg": [
+ "public.jpeg",
+ "com.canon.cr2-raw-image",
+ "public.jpeg",
+ None,
+ ],
+ "GdJJPQX0RP63mcdKFj%sfQ": ["public.jpeg", None, "public.heic", "public.jpeg"],
}
ALBUM_SORT_ORDER = [
@@ -53,92 +91,76 @@ ALBUM_SORT_ORDER = [
]
ALBUM_KEY_PHOTO = "15uNd7%8RguTEgNPKHfTWw"
-PHOTOS_DB_LEN = 8
-PHOTOS_NOT_IN_TRASH_LEN = 7
+PHOTOS_DB_LEN = 13
+PHOTOS_NOT_IN_TRASH_LEN = 12
PHOTOS_IN_TRASH_LEN = 1
-def test_init():
+@pytest.fixture(scope="module")
+def photosdb():
+ import osxphotos
+
+ return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+
+
+def test_init(photosdb):
import osxphotos
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert isinstance(photosdb, osxphotos.PhotosDB)
-def test_db_version():
+def test_db_version(photosdb):
import osxphotos
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert photosdb.db_version in osxphotos._constants._TESTED_DB_VERSIONS
assert photosdb.db_version == "4025"
-def test_db_len():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_db_len(photosdb):
# assert photosdb.db_version in osxphotos._TESTED_DB_VERSIONS
assert len(photosdb) == PHOTOS_DB_LEN
-def test_persons():
- import osxphotos
+def test_persons(photosdb):
import collections
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert "Katie" in photosdb.persons
assert collections.Counter(PERSONS) == collections.Counter(photosdb.persons)
-def test_keywords():
- import osxphotos
+def test_keywords(photosdb):
import collections
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert "wedding" in photosdb.keywords
assert collections.Counter(KEYWORDS) == collections.Counter(photosdb.keywords)
-def test_album_names():
- import osxphotos
+def test_album_names(photosdb):
import collections
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
assert "Pumpkin Farm" in photosdb.albums
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.albums)
-def test_keywords_dict():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_keywords_dict(photosdb):
keywords = photosdb.keywords_as_dict
assert keywords["wedding"] == 2
assert keywords == KEYWORDS_DICT
-def test_persons_as_dict():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_persons_as_dict(photosdb):
persons = photosdb.persons_as_dict
assert persons["Maria"] == 1
assert persons == PERSONS_DICT
-def test_albums_as_dict():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_albums_as_dict(photosdb):
albums = photosdb.albums_as_dict
assert albums["Pumpkin Farm"] == 3
assert albums == ALBUM_DICT
-def test_album_sort_order():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_album_sort_order(photosdb):
album = [a for a in photosdb.album_info if a.title == "Pumpkin Farm"][0]
photos = album.photos
@@ -146,11 +168,9 @@ def test_album_sort_order():
assert uuids == ALBUM_SORT_ORDER
-def test_attributes():
+def test_attributes(photosdb):
import datetime
- import osxphotos
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=["15uNd7%8RguTEgNPKHfTWw"])
assert len(photos) == 1
p = photos[0]
@@ -172,12 +192,10 @@ def test_attributes():
assert p.ismissing == False
-def test_attributes_2():
+def test_attributes_2(photosdb):
""" Test attributes including height, width, etc """
import datetime
- import osxphotos
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
assert len(photos) == 1
p = photos[0]
@@ -210,10 +228,7 @@ def test_attributes_2():
assert p.original_filesize == 460483
-def test_missing():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_missing(photosdb):
photos = photosdb.photos(uuid=["od0fmC7NQx+ayVr+%i06XA"])
assert len(photos) == 1
p = photos[0]
@@ -221,53 +236,37 @@ def test_missing():
assert p.ismissing
-def test_favorite():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_favorite(photosdb):
photos = photosdb.photos(uuid=[UUID_DICT["favorite"]])
assert len(photos) == 1
p = photos[0]
assert p.favorite == True
-def test_not_favorite():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_not_favorite(photosdb):
photos = photosdb.photos(uuid=["od0fmC7NQx+ayVr+%i06XA"])
assert len(photos) == 1
p = photos[0]
assert p.favorite == False
-def test_hidden():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["od0fmC7NQx+ayVr+%i06XA"])
+def test_hidden(photosdb):
+ photos = photosdb.photos(uuid=[UUID_DICT["hidden"]])
assert len(photos) == 1
p = photos[0]
assert p.hidden == True
-def test_not_hidden():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["6bxcNnzRQKGnK4uPrCJ9UQ"])
+def test_not_hidden(photosdb):
+ photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
assert len(photos) == 1
p = photos[0]
assert p.hidden == False
-def test_location_1():
+def test_location_1(photosdb):
# test photo with lat/lon info
- import osxphotos
- import pytest
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["3Jn73XpSQQCluzRBMWRsMA"])
+ photos = photosdb.photos(uuid=[UUID_DICT["location"]])
assert len(photos) == 1
p = photos[0]
lat, lon = p.location
@@ -275,12 +274,9 @@ def test_location_1():
assert lon == pytest.approx(-0.1318055)
-def test_location_2():
+def test_location_2(photosdb):
# test photo with no location info
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["YZFCPY24TUySvpu7owiqxA"])
+ photos = photosdb.photos(uuid=[UUID_DICT["no_location"]])
assert len(photos) == 1
p = photos[0]
lat, lon = p.location
@@ -288,59 +284,45 @@ def test_location_2():
assert lon is None
-def test_hasadjustments1():
+def test_hasadjustments1(photosdb):
# test hasadjustments == True
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["6bxcNnzRQKGnK4uPrCJ9UQ"])
+ photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
assert len(photos) == 1
p = photos[0]
assert p.hasadjustments == True
-def test_hasadjustments2():
+def test_hasadjustments2(photosdb):
# test hasadjustments == False
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["15uNd7%8RguTEgNPKHfTWw"])
+ photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
assert len(photos) == 1
p = photos[0]
assert p.hasadjustments == False
-def test_external_edit1():
+def test_external_edit1(photosdb):
# test image has been edited in external editor
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["3Jn73XpSQQCluzRBMWRsMA"])
+ photos = photosdb.photos(uuid=[UUID_DICT["external_edits"]])
assert len(photos) == 1
p = photos[0]
assert p.external_edit == True
-def test_external_edit2():
+def test_external_edit2(photosdb):
# test image has not been edited in external editor
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["6bxcNnzRQKGnK4uPrCJ9UQ"])
+ photos = photosdb.photos(uuid=[UUID_DICT["no_external_edits"]])
assert len(photos) == 1
p = photos[0]
assert p.external_edit == False
-def test_path_edited1():
+def test_path_edited1(photosdb):
# test a valid edited path
import os.path
- import osxphotos
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["6bxcNnzRQKGnK4uPrCJ9UQ"])
+ photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
assert len(photos) == 1
p = photos[0]
path = p.path_edited
@@ -348,95 +330,64 @@ def test_path_edited1():
assert os.path.exists(path)
-def test_path_edited2():
+def test_path_edited2(photosdb):
# test an invalid edited path
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
- photos = photosdb.photos(uuid=["15uNd7%8RguTEgNPKHfTWw"])
+ photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
assert len(photos) == 1
p = photos[0]
path = p.path_edited
assert path is None
-def test_count():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_count(photosdb):
photos = photosdb.photos()
assert len(photos) == PHOTOS_NOT_IN_TRASH_LEN
-def test_photos_intrash_1():
+def test_photos_intrash_1(photosdb):
""" test PhotosDB.photos(intrash=True) """
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(intrash=True)
assert len(photos) == PHOTOS_IN_TRASH_LEN
-def test_photos_intrash_2():
+def test_photos_intrash_2(photosdb):
""" test PhotosDB.photos(intrash=True) """
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(intrash=True)
for p in photos:
assert p.intrash
-def test_photos_intrash_2():
+def test_photos_not_intrash(photosdb):
""" test PhotosDB.photos(intrash=False) """
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(intrash=False)
for p in photos:
assert not p.intrash
-def test_photoinfo_intrash_1():
+def test_photoinfo_intrash_1(photosdb):
""" Test PhotoInfo.intrash """
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
p = photosdb.photos(uuid=[UUID_DICT["intrash"]], intrash=True)[0]
assert p.intrash
-def test_photoinfo_intrash_2():
+def test_photoinfo_intrash_2(photosdb):
""" Test PhotoInfo.intrash and intrash=default"""
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
p = photosdb.photos(uuid=[UUID_DICT["intrash"]])
assert not p
-def test_photoinfo_not_intrash():
+def test_photoinfo_not_intrash(photosdb):
""" Test PhotoInfo.intrash """
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
p = photosdb.photos(uuid=[UUID_DICT["not_intrash"]])[0]
assert not p.intrash
-def test_keyword_2():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_keyword_2(photosdb):
photos = photosdb.photos(keywords=["wedding"])
assert len(photos) == 2
-def test_keyword_not_in_album():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
-
+def test_keyword_not_in_album(photosdb):
# find all photos with keyword "Kids" not in the album "Pumpkin Farm"
photos1 = photosdb.photos(albums=["Pumpkin Farm"])
photos2 = photosdb.photos(keywords=["Kids"])
@@ -445,18 +396,12 @@ def test_keyword_not_in_album():
assert photos3[0].uuid == "od0fmC7NQx+ayVr+%i06XA"
-def test_get_db_path():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_get_db_path(photosdb):
db_path = photosdb.db_path
assert db_path.endswith(PHOTOS_DB_PATH)
-def test_get_library_path():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
+def test_get_library_path(photosdb):
lib_path = photosdb.library_path
assert lib_path.endswith(PHOTOS_LIBRARY_PATH)
@@ -475,7 +420,7 @@ def test_photosdb_repr():
def test_photosinfo_repr():
import osxphotos
- import datetime
+ import datetime # needed for eval to work
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["favorite"]])
@@ -487,48 +432,34 @@ def test_photosinfo_repr():
}
-def test_multi_uuid():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(PHOTOS_DB)
+def test_multi_uuid(photosdb):
photos = photosdb.photos(uuid=[UUID_DICT["favorite"], UUID_DICT["not_favorite"]])
assert len(photos) == 2
-def test_multi_keyword():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(PHOTOS_DB)
+def test_multi_keyword(photosdb):
photos = photosdb.photos(keywords=["Kids", "wedding"])
assert len(photos) == 6
-def test_multi_album():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(PHOTOS_DB)
+def test_multi_album(photosdb):
photos = photosdb.photos(albums=["Pumpkin Farm", "Test Album"])
assert len(photos) == 3
-def test_multi_person():
- import osxphotos
-
- photosdb = osxphotos.PhotosDB(PHOTOS_DB)
+def test_multi_person(photosdb):
photos = photosdb.photos(persons=["Katie", "Suzy"])
assert len(photos) == 3
-def test_date_invalid():
+def test_date_invalid(photosdb):
""" Test date is invalid """
from datetime import datetime, timedelta, timezone
- import osxphotos
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
assert len(photos) == 1
p = photos[0]
@@ -537,13 +468,40 @@ def test_date_invalid():
assert p.date == datetime(1970, 1, 1).astimezone(tz=tz)
-def test_date_modified_invalid():
+def test_date_modified_invalid(photosdb):
""" Test date modified is invalid """
- from datetime import datetime, timedelta, timezone
- import osxphotos
- photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
assert len(photos) == 1
p = photos[0]
assert p.date_modified is None
+
+
+def test_uti(photosdb):
+ for uuid, utis in UUID_UTI_DICT.items():
+ photo = photosdb.get_photo(uuid)
+ assert photo.uti == utis[0]
+ assert photo.uti_raw == utis[1]
+ assert photo.uti_original == utis[2]
+ assert photo.uti_edited == utis[3]
+
+
+def test_raw(photosdb):
+ photo = photosdb.get_photo(UUID_DICT["raw"])
+ # assert photo.israw
+ assert not photo.has_raw
+ assert photo.uti_raw == None
+ assert photo.uti == "com.adobe.raw-image"
+ assert photo.path_raw is None
+
+ photo = photosdb.get_photo(UUID_DICT["jpeg+raw"])
+ assert photo.has_raw
+ assert photo.path_raw.endswith(RAW_PATH_DICT["jpeg+raw"])
+
+ photo = photosdb.get_photo(UUID_DICT["raw+jpeg"])
+ assert photo.has_raw
+ assert photo.path_raw.endswith(RAW_PATH_DICT["raw+jpeg"])
+
+ photo = photosdb.get_photo(UUID_DICT["heic"])
+ assert not photo.has_raw
+ assert photo.path_raw is None
diff --git a/tests/test_shared_mojave_10_14_6.py b/tests/test_shared_mojave_10_14_6.py
index b78e592b..3dbd0803 100644
--- a/tests/test_shared_mojave_10_14_6.py
+++ b/tests/test_shared_mojave_10_14_6.py
@@ -1,8 +1,6 @@
import pytest
-# TODO: put some of this code into a pre-function
-
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-10.14.6.photoslibrary/database/photos.db"
PHOTOS_LIBRARY_PATH = "/Test-10.14.6.photoslibrary"
@@ -16,6 +14,10 @@ ALBUM_DICT = {
}
+PHOTOS_DB_LEN = 13
+PHOTOS_NOT_IN_TRASH_LEN = 12
+PHOTOS_IN_TRASH_LEN = 1
+
def test_album_names():
import osxphotos
@@ -69,4 +71,4 @@ def test_not_shared():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photos = [p for p in photosdb.photos() if not p.shared]
- assert len(photos) == 7
+ assert len(photos) == PHOTOS_NOT_IN_TRASH_LEN