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