diff --git a/README.md b/README.md index d71c9317..2290c95c 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,14 @@ ## What is osxphotos? -OSXPhotos provides the ability to interact with and query Apple's Photos.app library database on MacOS. Using this module you can query the Photos database for information about the photos stored in a Photos library on your Mac--for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. +OSXPhotos provides the ability to interact with and query Apple's Photos.app library database on MacOS. Using this module you can query the Photos database for information about the photos stored in a Photos library on your Mac--for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos. **NOTE**: OSXPhotos currently only supports image files -- e.g. it does not handle movies. ## Supported operating systems -Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0 and MacOS 10.14.5, 10.14.6 / Photos 4.0. Requires python >= 3.6 +Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0, MacOS 10.14.5, 10.14.6 / Photos 4.0, MacOS 10.15.1 / Photos 5.0. Requires python >= 3.6 -**NOTE**: Alpha support for Mac OS 10.15.0 / Photos 5.0. Photos 5.0 uses a new database format which required rewrite of much of the code for this module. If you find bugs, please open an [issue](https://github.com/RhetTbull/osxphotos/issues/). This module will read Photos databases for any supported version on any supported OS version. E.g. you can read a database created with Photos 4.0 on MacOS 10.14 on a machine running MacOS 10.12 @@ -409,11 +408,28 @@ Returns latitude and longitude as a tuple of floats (latitude, longitude). If l #### `to_json()` Returns a JSON representation of all photo info -Examples: +#### `export(self, *args, edited=False, overwrite=False, increment=True)` +Export photo from the Photos library to another destination on disk. +- First argument of *args must be valid destination path (or exception raised). +- Second argument of *args (optional): name of picture; if not provided, will use current filename +- edited: boolean; if True (default=False), will export the edited version of the photo (or raise exception if no edited version) +- overwrite: boolean; if True (default=False), will overwrite files if they alreay exist +- increment: boolean; if True (default=True), will increment file name until a non-existant name is found + +If overwrite=False and increment=False, export will fail if destination file already exists + +Returns the full path to the exported file + +**Implementation Note**: Because the usual python file copy methods don't preserve all the metadata available on MacOS, export uses /usr/bin/ditto to do the copy for export. ditto preserves most metadata such as extended attributes, permissions, ACLs, etc. + +### Examples ```python -# assumes photosdb is a PhotosDB object (see above) +import osxphotos + +photosdb = osxphotos.PhotosDB() photos=photosdb.photos() + for p in photos: print( p.uuid(), @@ -431,10 +447,42 @@ for p in photos: ) ``` +```python +""" Export all photos to ~/Desktop/export + If file has been edited, export the edited version, + otherwise, export the original version """ + +import os.path + +import osxphotos + +photosdb = osxphotos.PhotosDB() +photos = photosdb.photos() + +export_path = os.path.expanduser("~/Desktop/export") + +for p in photos: + if not p.ismissing(): + if p.hasadjustments(): + exported = p.export(export_path, edited=True) + else: + exported = p.export(export_path) + print(f"Exported {p.filename()} to {exported}") + else: + print(f"Skipping missing photo: {p.filename()}") +``` + ## History This project started as a command line utility, `photosmeta`, available at [photosmeta](https://github.com/RhetTbull/photosmeta) This module converts the photosmeta Photos library query functionality into a module. +## Contributing + +Contributing is easy! If you find bugs or want to suggest additional features/changes, please open an [issue](https://github.com/RhetTbull/osxphotos/issues/). + +I'll gladly consider pull requests for bug fixes or feature implementations. + +If you have an interesting example that shows usage of this module, submit an issue or pull request and I'll include it or link to it. ## Implementation Notes @@ -450,7 +498,7 @@ Apple does provide a framework ([PhotoKit](https://developer.apple.com/documenta - [Click](https://pypi.org/project/click/) ## Acknowledgements -This project was inspired by photo-export by Patrick Fältström see: (https://github.com/patrikhson/photo-export) Copyright (c) 2015 Patrik Fältström paf@frobbit.se +This project was originally inspired by photo-export by Patrick Fältström see: (https://github.com/patrikhson/photo-export) Copyright (c) 2015 Patrik Fältström paf@frobbit.se To interact with the Photos app, I use [py-applescript]( https://github.com/rdhyee/py-applescript) by "Raymond Yee / rdhyee". Rather than import this module, I included the entire module (which is published as public domain code) in a private module to prevent ambiguity with diff --git a/examples/export.py b/examples/export.py new file mode 100644 index 00000000..e93c2569 --- /dev/null +++ b/examples/export.py @@ -0,0 +1,22 @@ +""" Export all photos to ~/Desktop/export + If file has been edited, export the edited version, + otherwise, export the original version """ + +import os.path + +import osxphotos + +photosdb = osxphotos.PhotosDB() +photos = photosdb.photos() + +export_path = os.path.expanduser("~/Desktop/export") + +for p in photos: + if not p.ismissing(): + if p.hasadjustments(): + exported = p.export(export_path, edited=True) + else: + exported = p.export(export_path) + print(f"Exported {p.filename()} to {exported}") + else: + print(f"Skipping missing photo: {p.filename()}") diff --git a/osxphotos/__init__.py b/osxphotos/__init__.py index 73ec89c2..c0e6551c 100644 --- a/osxphotos/__init__.py +++ b/osxphotos/__init__.py @@ -2,6 +2,7 @@ import glob import json import logging import os.path +import pathlib import platform import sqlite3 import subprocess @@ -1414,7 +1415,13 @@ class PhotoInfo: return self._info["keywords"] def name(self): + """ (deprecated) name / title of picture """ + # TODO: add warning on deprecation + return self._info["name"] + + def title(self): """ name / title of picture """ + # TODO: Update documentation and tests to use title return self._info["name"] def uuid(self): @@ -1457,6 +1464,126 @@ class PhotoInfo: """ returns (latitude, longitude) as float in degrees or None """ return (self._latitude(), self._longitude()) + def export(self, *args, edited=False, overwrite=False, increment=True): + """ export photo """ + """ first argument must be valid destination path (or exception raised) """ + """ second argument (optional): name of picture; if not provided, will use current filename """ + """ if edited=True (default=False), will export the edited version of the photo (or raise exception if no edited version) """ + """ if overwrite=True (default=False), will overwrite files if they alreay exist """ + """ if increment=True (default=True), will increment file name until a non-existant name is found """ + """ if overwrite=False and increment=False, export will fail if destination file already exists """ + """ returns the full path to the exported file """ + + # TODO: find better way to do *args + # maybe dest, *filename? + + # check arguments and get destination path and filename (if provided) + dest = None # destination path + filename = None # photo filename + if not args: + # need at least one arg (destination) + raise TypeError("Must pass destination as first argument") + else: + if len(args) > 2: + raise TypeError( + "Too many positional arguments. Should be at most two: destination, filename." + ) + else: + # verify destination is a valid path + dest = args[0] + if dest is None: + raise ValueError("Destination must not be None") + elif not os.path.isdir(dest): + raise FileNotFoundError("Invalid path passed to export") + + if len(args) == 2: + # second arg is filename of picture + filename = args[1] + 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: + # verify we have a valid path_edited and use that to get filename + if not self.path_edited(): + raise FileNotFoundError( + f"edited=True but path_edited is none; hasadjustments: {self.hasadjustments()}" + ) + edited_name = Path(self.path_edited()).name + edited_suffix = Path(edited_name).suffix + filename = ( + Path(self.filename()).stem + "_edited" + edited_suffix + ) + else: + filename = self.filename() + + # get path to source file and verify it's not None and is valid file + # TODO: how to handle ismissing or not hasadjustments and edited=True cases? + if edited: + if not self.hasadjustments(): + logging.warning( + "Attempting to export edited photo but hasadjustments=False" + ) + + if self.path_edited() is not None: + src = self.path_edited() + else: + raise FileNotFoundError( + f"edited=True but path_edited is none; hasadjustments: {self.hasadjustments()}" + ) + else: + if self.ismissing(): + logging.warning( + f"Attempting to export photo with ismissing=True: path = {self.path()}" + ) + + if self.path() is None: + logging.warning( + f"Attempting to export photo but path is None: ismissing = {self.ismissing()}" + ) + raise FileNotFoundError("Cannot export photo if path is None") + else: + src = self.path() + + if not os.path.isfile(src): + raise FileNotFoundError(f"{src} does not appear to exist") + + dest = pathlib.Path(dest) + filename = pathlib.Path(filename) + dest = dest / filename + + # check to see if file exists and if so, add (1), (2), etc until we find one that works + if increment and not overwrite: + count = 1 + dest_new = dest + while dest_new.exists(): + dest_new = dest.parent / f"{dest.stem} ({count}){dest.suffix}" + count += 1 + dest = dest_new + + logging.debug( + f"exporting {src} to {dest}, overwrite={overwrite}, incremetn={increment}, dest exists: {dest.exists()}" + ) + + # if overwrite==False and #increment==False, export should fail if file exists + if dest.exists() and not overwrite and not increment: + raise FileExistsError( + f"destination exists ({dest}); overwrite={overwrite}, increment={increment}" + ) + + # if error on copy, subprocess will raise CalledProcessError + try: + subprocess.run( + ["/usr/bin/ditto", src, dest], check=True, stderr=subprocess.PIPE + ) + except subprocess.CalledProcessError as e: + logging.critical( + f"ditto returned error: {e.returncode} {e.stderr.decode(sys.getfilesystemencoding()).rstrip()}" + ) + raise e + + return str(dest) + def _longitude(self): """ Returns longitude, in degrees """ return self._info["longitude"] diff --git a/osxphotos/_version.py b/osxphotos/_version.py index e8d64aa3..867f43a1 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,4 +1,4 @@ """ version info """ -__version__ = "0.14.21" +__version__ = "0.15.0" diff --git a/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite b/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite index 1b4f53ff..6d96e0ee 100644 Binary files a/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite and b/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite differ diff --git a/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite-shm b/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite-shm index 9309cb2d..6eaaf26a 100644 Binary files a/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite-wal b/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite-wal index e7673346..d1a5720f 100644 Binary files a/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite-wal and b/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite-wal differ diff --git a/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite.lock b/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite.lock index 0b0e9c6a..a1922ade 100644 --- a/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite.lock +++ b/tests/Test-10.15.1.photoslibrary/database/Photos.sqlite.lock @@ -7,7 +7,7 @@ hostuuid 9575E48B-8D5F-5654-ABAC-4431B1167324 pid - 4178 + 423 processname photolibraryd uid diff --git a/tests/Test-10.15.1.photoslibrary/database/search/psi.sqlite b/tests/Test-10.15.1.photoslibrary/database/search/psi.sqlite index 496915f1..7ac8d691 100644 Binary files a/tests/Test-10.15.1.photoslibrary/database/search/psi.sqlite and b/tests/Test-10.15.1.photoslibrary/database/search/psi.sqlite differ diff --git a/tests/Test-10.15.1.photoslibrary/database/search/psi.sqlite-shm b/tests/Test-10.15.1.photoslibrary/database/search/psi.sqlite-shm index 1667de94..c56e485b 100644 Binary files a/tests/Test-10.15.1.photoslibrary/database/search/psi.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/database/search/psi.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.mediaanalysisd/MediaAnalysis/mediaanalysis.db b/tests/Test-10.15.1.photoslibrary/private/com.apple.mediaanalysisd/MediaAnalysis/mediaanalysis.db index ef150231..90d9b9c0 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.mediaanalysisd/MediaAnalysis/mediaanalysis.db and b/tests/Test-10.15.1.photoslibrary/private/com.apple.mediaanalysisd/MediaAnalysis/mediaanalysis.db differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.mediaanalysisd/MediaAnalysis/mediaanalysis.db-shm b/tests/Test-10.15.1.photoslibrary/private/com.apple.mediaanalysisd/MediaAnalysis/mediaanalysis.db-shm index 25b15abc..d19297a4 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.mediaanalysisd/MediaAnalysis/mediaanalysis.db-shm and b/tests/Test-10.15.1.photoslibrary/private/com.apple.mediaanalysisd/MediaAnalysis/mediaanalysis.db-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm index 287ce981..16e828ae 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-wal b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-wal index c3b3c767..911b1dcd 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-wal and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.AOI.sqlite-wal differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm index 257e6022..d9e13d96 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-wal b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-wal index b1587a75..3868a639 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-wal and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.Nature.sqlite-wal differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm index 59868099..75fb129f 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal index 24485388..46632fa6 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.POI.sqlite-wal differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm index d191840a..ef176a0e 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-wal b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-wal index 49d4afc7..5cb86691 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-wal and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSBusinessCategoryCache.ROI.sqlite-wal differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSContactCache.sqlite-shm b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSContactCache.sqlite-shm index 7fb84897..15b02dc9 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSContactCache.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSContactCache.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSContactCache.sqlite-wal b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSContactCache.sqlite-wal index 1a203754..0c48f097 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSContactCache.sqlite-wal and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSContactCache.sqlite-wal differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm index 536e96af..a3fdd593 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-wal b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-wal index ff7fbba5..01640b2b 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-wal and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSLocationCache.sqlite-wal differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-shm b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-shm index 56707c14..ffc30a49 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-wal b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-wal index 01fa9453..df9bc056 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-wal and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/CLSPublicEventCache.sqlite-wal differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm index 0504fd84..822c040d 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-shm differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-wal b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-wal index 4bf420f7..a8613bd7 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-wal and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGCurationCache.sqlite.sqlite-wal differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGSearchComputationCache.plist b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGSearchComputationCache.plist index 8bb0e611..05b10a46 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGSearchComputationCache.plist and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PGSearchComputationCache.plist differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotoAnalysisServicePreferences.plist b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotoAnalysisServicePreferences.plist index 1640781a..5bbae0b4 100644 --- a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotoAnalysisServicePreferences.plist +++ b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotoAnalysisServicePreferences.plist @@ -3,24 +3,24 @@ BackgroundHighlightCollection - 2019-12-08T05:40:32Z + 2019-12-14T18:19:30Z BackgroundHighlightEnrichment - 2019-12-08T05:40:32Z + 2019-12-14T18:19:29Z BackgroundJobAssetRevGeocode - 2019-12-08T05:40:32Z + 2019-12-14T18:19:30Z BackgroundJobSearch - 2019-12-08T05:40:32Z + 2019-12-14T18:19:30Z BackgroundPeopleSuggestion - 2019-12-08T05:40:31Z + 2019-12-14T18:19:28Z BackgroundUserBehaviorProcessor - 2019-12-08T05:40:32Z + 0000-12-30T00:00:00Z PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey - 2019-12-08T05:40:44Z + 2019-12-14T18:19:28Z PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate - 2019-12-07T19:48:13Z + 2019-12-14T18:19:28Z PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate - 2019-12-08T05:40:33Z + 2019-12-10T06:45:58Z SiriPortraitDonation - 2019-12-08T05:40:32Z + 0000-12-30T00:00:00Z diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb index 7dba2e03..15c03a40 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/PhotosGraph/photosgraph.kgdb differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist index bd9c8111..afdcca38 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/graph/changetoken.plist differ diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/PhotoAnalysisServicePreferences.plist b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/PhotoAnalysisServicePreferences.plist index 051a0d18..fb706463 100644 --- a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/PhotoAnalysisServicePreferences.plist +++ b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/PhotoAnalysisServicePreferences.plist @@ -3,8 +3,8 @@ FaceIDModelLastGenerationKey - 2019-12-08T05:40:33Z + 2019-12-10T06:45:58Z LastContactClassificationKey - 2019-12-08T05:40:34Z + 2019-12-10T06:46:00Z diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/faceWorkerState.plist b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/faceWorkerState.plist index 736db69d..c36206de 100644 --- a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/faceWorkerState.plist +++ b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/faceWorkerState.plist @@ -3,7 +3,7 @@ IncrementalPersonProcessingStage - 0 + 6 PersonBuilderLastMinimumFaceGroupSizeForCreatingMergeCandidates 15 PersonBuilderMergeCandidatesEnabled diff --git a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/vnpersonsmodel.bin b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/vnpersonsmodel.bin index 4fb1eb57..34dcccbd 100644 Binary files a/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/vnpersonsmodel.bin and b/tests/Test-10.15.1.photoslibrary/private/com.apple.photoanalysisd/caches/vision/vnpersonsmodel.bin differ diff --git a/tests/Test-10.15.1.photoslibrary/resources/journals/Album-change.plj b/tests/Test-10.15.1.photoslibrary/resources/journals/Album-change.plj deleted file mode 100644 index 6c21a031..00000000 Binary files a/tests/Test-10.15.1.photoslibrary/resources/journals/Album-change.plj and /dev/null differ diff --git a/tests/Test-10.15.1.photoslibrary/resources/journals/Album-snapshot.plj b/tests/Test-10.15.1.photoslibrary/resources/journals/Album-snapshot.plj index 1984663f..61d6f6bb 100644 Binary files a/tests/Test-10.15.1.photoslibrary/resources/journals/Album-snapshot.plj and b/tests/Test-10.15.1.photoslibrary/resources/journals/Album-snapshot.plj differ diff --git a/tests/Test-10.15.1.photoslibrary/resources/journals/Album.plist b/tests/Test-10.15.1.photoslibrary/resources/journals/Album.plist index 3ece9a2c..4c0ce869 100644 --- a/tests/Test-10.15.1.photoslibrary/resources/journals/Album.plist +++ b/tests/Test-10.15.1.photoslibrary/resources/journals/Album.plist @@ -2,6 +2,10 @@ + coalesceDate + 2019-12-08T18:06:37Z + coalescePayloadVersion + 1 currentPayloadVersion 1 snapshotDate diff --git a/tests/Test-10.15.1.photoslibrary/resources/journals/Asset-change.plj b/tests/Test-10.15.1.photoslibrary/resources/journals/Asset-change.plj index 5e43f018..1ef0291b 100644 Binary files a/tests/Test-10.15.1.photoslibrary/resources/journals/Asset-change.plj and b/tests/Test-10.15.1.photoslibrary/resources/journals/Asset-change.plj differ diff --git a/tests/Test-10.15.1.photoslibrary/resources/journals/Folder-change.plj b/tests/Test-10.15.1.photoslibrary/resources/journals/Folder-change.plj deleted file mode 100644 index 78626bfe..00000000 Binary files a/tests/Test-10.15.1.photoslibrary/resources/journals/Folder-change.plj and /dev/null differ diff --git a/tests/Test-10.15.1.photoslibrary/resources/journals/Folder-snapshot.plj b/tests/Test-10.15.1.photoslibrary/resources/journals/Folder-snapshot.plj index 9b1cb6c6..d4b69d97 100644 Binary files a/tests/Test-10.15.1.photoslibrary/resources/journals/Folder-snapshot.plj and b/tests/Test-10.15.1.photoslibrary/resources/journals/Folder-snapshot.plj differ diff --git a/tests/Test-10.15.1.photoslibrary/resources/journals/Folder.plist b/tests/Test-10.15.1.photoslibrary/resources/journals/Folder.plist index a7b3527b..4c0ce869 100644 --- a/tests/Test-10.15.1.photoslibrary/resources/journals/Folder.plist +++ b/tests/Test-10.15.1.photoslibrary/resources/journals/Folder.plist @@ -3,7 +3,7 @@ coalesceDate - 2019-10-27T15:36:05Z + 2019-12-08T18:06:37Z coalescePayloadVersion 1 currentPayloadVersion diff --git a/tests/Test-10.15.1.photoslibrary/resources/journals/HistoryToken.plist b/tests/Test-10.15.1.photoslibrary/resources/journals/HistoryToken.plist index f6000146..20f0e3b0 100644 Binary files a/tests/Test-10.15.1.photoslibrary/resources/journals/HistoryToken.plist and b/tests/Test-10.15.1.photoslibrary/resources/journals/HistoryToken.plist differ diff --git a/tests/test_catalina_10_15_1.py b/tests/test_catalina_10_15_1.py index 18ac7245..dd5ddea0 100644 --- a/tests/test_catalina_10_15_1.py +++ b/tests/test_catalina_10_15_1.py @@ -42,6 +42,21 @@ ALBUM_DICT = { "Test Album": 2, } # Note: there are 2 albums named "Test Album" for testing duplicate album names +UUID_DICT = { + "missing": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C", + "favorite": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51", + "not_favorite": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C", + "hidden": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C", + "not_hidden": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51", + "has_adjustments": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51", + "no_adjustments": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4", + "location": "DC99FBDD-7A52-4100-A5BB-344131646C30", + "no_location": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4", + "external_edit": "DC99FBDD-7A52-4100-A5BB-344131646C30", + "no_external_edit": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51", + "export": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg" +} + def test_init(): # test named argument @@ -197,7 +212,7 @@ def test_missing(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"]) + photos = photosdb.photos(uuid=[UUID_DICT["missing"]]) assert len(photos) == 1 p = photos[0] assert p.path() == None @@ -208,7 +223,7 @@ def test_favorite(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"]) + photos = photosdb.photos(uuid=[UUID_DICT["favorite"]]) assert len(photos) == 1 p = photos[0] assert p.favorite() == True @@ -218,7 +233,7 @@ def test_not_favorite(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"]) + photos = photosdb.photos(uuid=[UUID_DICT["not_favorite"]]) assert len(photos) == 1 p = photos[0] assert p.favorite() == False @@ -228,7 +243,7 @@ def test_hidden(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"]) + photos = photosdb.photos(uuid=[UUID_DICT["hidden"]]) assert len(photos) == 1 p = photos[0] assert p.hidden() == True @@ -238,7 +253,7 @@ def test_not_hidden(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"]) + photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]]) assert len(photos) == 1 p = photos[0] assert p.hidden() == False @@ -249,7 +264,7 @@ def test_location_1(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["DC99FBDD-7A52-4100-A5BB-344131646C30"]) + photos = photosdb.photos(uuid=[UUID_DICT["location"]]) assert len(photos) == 1 p = photos[0] lat, lon = p.location() @@ -262,7 +277,7 @@ def test_location_2(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"]) + photos = photosdb.photos(uuid=[UUID_DICT["no_location"]]) assert len(photos) == 1 p = photos[0] lat, lon = p.location() @@ -275,7 +290,7 @@ def test_hasadjustments1(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"]) + photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) assert len(photos) == 1 p = photos[0] assert p.hasadjustments() == True @@ -286,7 +301,7 @@ def test_hasadjustments2(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"]) + photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]]) assert len(photos) == 1 p = photos[0] assert p.hasadjustments() == False @@ -297,7 +312,7 @@ def test_external_edit1(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["DC99FBDD-7A52-4100-A5BB-344131646C30"]) + photos = photosdb.photos(uuid=[UUID_DICT["external_edit"]]) assert len(photos) == 1 p = photos[0] @@ -309,7 +324,7 @@ def test_external_edit2(): import osxphotos photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) - photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"]) + photos = photosdb.photos(uuid=[UUID_DICT["no_external_edit"]]) assert len(photos) == 1 p = photos[0] @@ -386,3 +401,338 @@ def test_get_library_path(): lib_path = photosdb.get_library_path() assert lib_path.endswith(PHOTOS_LIBRARY_PATH) + +def test_export_1(): + # test basic export + # get an unedited image and export it using default filename + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + got_dest = photos[0].export(dest) + + assert got_dest == expected_dest + assert os.path.isfile(got_dest) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_2(): + # test export with user provided filename + import os + import os.path + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + timestamp = time.time() + filename = f"osxphotos-export-2-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + got_dest = photos[0].export(dest, filename) + + assert got_dest == expected_dest + assert os.path.isfile(got_dest) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_3(): + # test file already exists and test increment=True (default) + import os + import os.path + import pathlib + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + filename2 = pathlib.Path(filename) + filename2 = f"{filename2.stem} (1){filename2.suffix}" + expected_dest = os.path.join(dest, filename) + expected_dest_2 = os.path.join(dest, filename2) + + got_dest = photos[0].export(dest) + got_dest_2 = photos[0].export(dest) + + assert got_dest_2 == expected_dest_2 + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + os.remove(got_dest_2) + + +def test_export_4(): + # test user supplied file already exists and test increment=True (default) + import os + import os.path + import pathlib + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + timestamp = time.time() + filename = f"osxphotos-export-2-test-{timestamp}.jpg" + filename2 = f"osxphotos-export-2-test-{timestamp} (1).jpg" + expected_dest = os.path.join(dest, filename) + expected_dest_2 = os.path.join(dest, filename2) + + got_dest = photos[0].export(dest, filename) + got_dest_2 = photos[0].export(dest, filename) + + assert got_dest_2 == expected_dest_2 + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + os.remove(got_dest_2) + + +def test_export_5(): + # test file already exists and test increment=True (default) + # and overwrite = True + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest) + got_dest_2 = photos[0].export(dest, overwrite=True) + + assert got_dest_2 == got_dest + assert got_dest_2 == expected_dest + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_6(): + # test user supplied file already exists and test increment=True (default) + # and overwrite = True + import os + import os.path + import pathlib + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + timestamp = time.time() + filename = f"osxphotos-export-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest, filename) + got_dest_2 = photos[0].export(dest, filename, overwrite=True) + + assert got_dest_2 == got_dest + assert got_dest_2 == expected_dest + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_7(): + # test file already exists and test increment=False (not default), overwrite=False (default) + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest) + with pytest.raises(Exception) as e: + # try to export again with increment = False + assert photos[0].export(dest, increment=False) + assert e.type == type(FileExistsError()) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_8(): + # try to export missing file + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["missing"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest) + assert e.type == type(FileNotFoundError()) + + +def test_export_9(): + # try to export edited file that's not edited + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest, edited=True) + assert e.type == type(FileNotFoundError()) + + +def test_export_10(): + # try to export edited file that's not edited and name provided + # should raise exception + import os + import os.path + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]]) + + timestamp = time.time() + filename = f"osxphotos-export-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest, filename, edited=True) + assert e.type == type(FileNotFoundError()) + + +def test_export_11(): + # export edited file with name provided + import os + import os.path + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) + + timestamp = time.time() + filename = f"osxphotos-export-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest, filename, edited=True) + assert got_dest == expected_dest + + # remove the temporary file + os.remove(got_dest) + + +def test_export_12(): + # export edited file with default name + import os + import os.path + import pathlib + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) + + edited_name = pathlib.Path(photos[0].path_edited()).name + edited_suffix = pathlib.Path(edited_name).suffix + filename = pathlib.Path(photos[0].filename()).stem + "_edited" + edited_suffix + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest, edited=True) + assert got_dest == expected_dest + + # remove the temporary file + os.remove(got_dest) + + +def test_export_13(): + # export to invalid destination + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + + # create a folder that doesn't exist + i = 0 + while os.path.isdir(dest): + dest = os.path.join(dest, str(i)) + i += 1 + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest) + assert e.type == type(FileNotFoundError()) + diff --git a/tests/test_export_catalina_10_15_1.py b/tests/test_export_catalina_10_15_1.py new file mode 100644 index 00000000..a3ce57ac --- /dev/null +++ b/tests/test_export_catalina_10_15_1.py @@ -0,0 +1,392 @@ +import pytest + +from osxphotos import _UNKNOWN_PERSON + +# TODO: put some of this code into a pre-function + +PHOTOS_DB = "./tests/Test-10.15.1.photoslibrary/database/photos.db" +PHOTOS_DB_PATH = "/Test-10.15.1.photoslibrary/database/Photos.sqlite" +PHOTOS_LIBRARY_PATH = "/Test-10.15.1.photoslibrary" + +KEYWORDS = [ + "Kids", + "wedding", + "flowers", + "England", + "London", + "London 2018", + "St. James's Park", + "UK", + "United Kingdom", +] +# Photos 5 includes blank person for detected face +PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON] +ALBUMS = [ + "Pumpkin Farm", + "Test Album", +] # Note: there are 2 albums named "Test Album" for testing duplicate album names +KEYWORDS_DICT = { + "Kids": 4, + "wedding": 2, + "flowers": 1, + "England": 1, + "London": 1, + "London 2018": 1, + "St. James's Park": 1, + "UK": 1, + "United Kingdom": 1, +} +PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1, _UNKNOWN_PERSON: 1} +ALBUM_DICT = { + "Pumpkin Farm": 3, + "Test Album": 2, +} # Note: there are 2 albums named "Test Album" for testing duplicate album names + +UUID_DICT = { + "missing": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C", + "favorite": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51", + "not_favorite": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C", + "hidden": "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C", + "not_hidden": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51", + "has_adjustments": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51", + "no_adjustments": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4", + "location": "DC99FBDD-7A52-4100-A5BB-344131646C30", + "no_location": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4", + "external_edit": "DC99FBDD-7A52-4100-A5BB-344131646C30", + "no_external_edit": "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51", + "export": "D79B8D77-BFFC-460B-9312-034F2877D35B", # "Pumkins2.jpg" +} + +def test_export_1(): + # test basic export + # get an unedited image and export it using default filename + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + got_dest = photos[0].export(dest) + + assert got_dest == expected_dest + assert os.path.isfile(got_dest) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_2(): + # test export with user provided filename + import os + import os.path + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + timestamp = time.time() + filename = f"osxphotos-export-2-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + got_dest = photos[0].export(dest, filename) + + assert got_dest == expected_dest + assert os.path.isfile(got_dest) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_3(): + # test file already exists and test increment=True (default) + import os + import os.path + import pathlib + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + filename2 = pathlib.Path(filename) + filename2 = f"{filename2.stem} (1){filename2.suffix}" + expected_dest = os.path.join(dest, filename) + expected_dest_2 = os.path.join(dest, filename2) + + got_dest = photos[0].export(dest) + got_dest_2 = photos[0].export(dest) + + assert got_dest_2 == expected_dest_2 + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + os.remove(got_dest_2) + + +def test_export_4(): + # test user supplied file already exists and test increment=True (default) + import os + import os.path + import pathlib + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + timestamp = time.time() + filename = f"osxphotos-export-2-test-{timestamp}.jpg" + filename2 = f"osxphotos-export-2-test-{timestamp} (1).jpg" + expected_dest = os.path.join(dest, filename) + expected_dest_2 = os.path.join(dest, filename2) + + got_dest = photos[0].export(dest, filename) + got_dest_2 = photos[0].export(dest, filename) + + assert got_dest_2 == expected_dest_2 + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + os.remove(got_dest_2) + + +def test_export_5(): + # test file already exists and test increment=True (default) + # and overwrite = True + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest) + got_dest_2 = photos[0].export(dest, overwrite=True) + + assert got_dest_2 == got_dest + assert got_dest_2 == expected_dest + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_6(): + # test user supplied file already exists and test increment=True (default) + # and overwrite = True + import os + import os.path + import pathlib + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + timestamp = time.time() + filename = f"osxphotos-export-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest, filename) + got_dest_2 = photos[0].export(dest, filename, overwrite=True) + + assert got_dest_2 == got_dest + assert got_dest_2 == expected_dest + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_7(): + # test file already exists and test increment=False (not default), overwrite=False (default) + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest) + with pytest.raises(Exception) as e: + # try to export again with increment = False + assert photos[0].export(dest, increment=False) + assert e.type == type(FileExistsError()) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_8(): + # try to export missing file + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["missing"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest) + assert e.type == type(FileNotFoundError()) + + +def test_export_9(): + # try to export edited file that's not edited + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest, edited=True) + assert e.type == type(FileNotFoundError()) + + +def test_export_10(): + # try to export edited file that's not edited and name provided + # should raise exception + import os + import os.path + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]]) + + timestamp = time.time() + filename = f"osxphotos-export-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest, filename, edited=True) + assert e.type == type(FileNotFoundError()) + + +def test_export_11(): + # export edited file with name provided + import os + import os.path + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) + + timestamp = time.time() + filename = f"osxphotos-export-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest, filename, edited=True) + assert got_dest == expected_dest + + # remove the temporary file + os.remove(got_dest) + + +def test_export_12(): + # export edited file with default name + import os + import os.path + import pathlib + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) + + edited_name = pathlib.Path(photos[0].path_edited()).name + edited_suffix = pathlib.Path(edited_name).suffix + filename = pathlib.Path(photos[0].filename()).stem + "_edited" + edited_suffix + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest, edited=True) + assert got_dest == expected_dest + + # remove the temporary file + os.remove(got_dest) + + +def test_export_13(): + # export to invalid destination + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + + # create a folder that doesn't exist + i = 0 + while os.path.isdir(dest): + dest = os.path.join(dest, str(i)) + i += 1 + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest) + assert e.type == type(FileNotFoundError()) diff --git a/tests/test_export_mojave_10_14_6.py b/tests/test_export_mojave_10_14_6.py new file mode 100644 index 00000000..3bfeb3ca --- /dev/null +++ b/tests/test_export_mojave_10_14_6.py @@ -0,0 +1,378 @@ +import pytest + +from osxphotos import _UNKNOWN_PERSON + +# 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" + +KEYWORDS = [ + "Kids", + "wedding", + "flowers", + "England", + "London", + "London 2018", + "St. James's Park", + "UK", + "United Kingdom", +] +PERSONS = ["Katie", "Suzy", "Maria"] +ALBUMS = ["Pumpkin Farm", "Test Album", "Test Album (1)"] +KEYWORDS_DICT = { + "Kids": 4, + "wedding": 2, + "flowers": 1, + "England": 1, + "London": 1, + "London 2018": 1, + "St. James's Park": 1, + "UK": 1, + "United Kingdom": 1, +} +PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 1} +ALBUM_DICT = {"Pumpkin Farm": 3, "Test Album": 1, "Test Album (1)": 1} + +UUID_DICT = { + "missing": "od0fmC7NQx+ayVr+%i06XA", + "has_adjustments": "6bxcNnzRQKGnK4uPrCJ9UQ", + "no_adjustments": "15uNd7%8RguTEgNPKHfTWw", + "export": "15uNd7%8RguTEgNPKHfTWw", +} + +def test_export_1(): + # test basic export + # get an unedited image and export it using default filename + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + got_dest = photos[0].export(dest) + + assert got_dest == expected_dest + assert os.path.isfile(got_dest) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_2(): + # test export with user provided filename + import os + import os.path + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + timestamp = time.time() + filename = f"osxphotos-export-2-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + got_dest = photos[0].export(dest, filename) + + assert got_dest == expected_dest + assert os.path.isfile(got_dest) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_3(): + # test file already exists and test increment=True (default) + import os + import os.path + import pathlib + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + filename2 = pathlib.Path(filename) + filename2 = f"{filename2.stem} (1){filename2.suffix}" + expected_dest = os.path.join(dest, filename) + expected_dest_2 = os.path.join(dest, filename2) + + got_dest = photos[0].export(dest) + got_dest_2 = photos[0].export(dest) + + assert got_dest_2 == expected_dest_2 + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + os.remove(got_dest_2) + + +def test_export_4(): + # test user supplied file already exists and test increment=True (default) + import os + import os.path + import pathlib + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + timestamp = time.time() + filename = f"osxphotos-export-2-test-{timestamp}.jpg" + filename2 = f"osxphotos-export-2-test-{timestamp} (1).jpg" + expected_dest = os.path.join(dest, filename) + expected_dest_2 = os.path.join(dest, filename2) + + got_dest = photos[0].export(dest, filename) + got_dest_2 = photos[0].export(dest, filename) + + assert got_dest_2 == expected_dest_2 + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + os.remove(got_dest_2) + + +def test_export_5(): + # test file already exists and test increment=True (default) + # and overwrite = True + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest) + got_dest_2 = photos[0].export(dest, overwrite=True) + + assert got_dest_2 == got_dest + assert got_dest_2 == expected_dest + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_6(): + # test user supplied file already exists and test increment=True (default) + # and overwrite = True + import os + import os.path + import pathlib + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + timestamp = time.time() + filename = f"osxphotos-export-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest, filename) + got_dest_2 = photos[0].export(dest, filename, overwrite=True) + + assert got_dest_2 == got_dest + assert got_dest_2 == expected_dest + assert os.path.isfile(got_dest_2) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_7(): + # test file already exists and test increment=False (not default), overwrite=False (default) + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest) + with pytest.raises(Exception) as e: + # try to export again with increment = False + assert photos[0].export(dest, increment=False) + assert e.type == type(FileExistsError()) + + # remove the temporary file + os.remove(got_dest) + + +def test_export_8(): + # try to export missing file + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["missing"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest) + assert e.type == type(FileNotFoundError()) + + +def test_export_9(): + # try to export edited file that's not edited + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest, edited=True) + assert e.type == type(FileNotFoundError()) + + +def test_export_10(): + # try to export edited file that's not edited and name provided + # should raise exception + import os + import os.path + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]]) + + timestamp = time.time() + filename = f"osxphotos-export-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest, filename, edited=True) + assert e.type == type(FileNotFoundError()) + + +def test_export_11(): + # export edited file with name provided + import os + import os.path + import tempfile + import time + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) + + timestamp = time.time() + filename = f"osxphotos-export-test-{timestamp}.jpg" + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest, filename, edited=True) + assert got_dest == expected_dest + + # remove the temporary file + os.remove(got_dest) + + +def test_export_12(): + # export edited file with default name + import os + import os.path + import pathlib + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]]) + + edited_name = pathlib.Path(photos[0].path_edited()).name + edited_suffix = pathlib.Path(edited_name).suffix + filename = pathlib.Path(photos[0].filename()).stem + "_edited" + edited_suffix + expected_dest = os.path.join(dest, filename) + + got_dest = photos[0].export(dest, edited=True) + assert got_dest == expected_dest + + # remove the temporary file + os.remove(got_dest) + + +def test_export_13(): + # export to invalid destination + # should raise exception + import os + import os.path + import tempfile + + import osxphotos + + dest = tempfile.gettempdir() + + # create a folder that doesn't exist + i = 0 + while os.path.isdir(dest): + dest = os.path.join(dest, str(i)) + i += 1 + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=[UUID_DICT["export"]]) + + filename = photos[0].filename() + expected_dest = os.path.join(dest, filename) + + with pytest.raises(Exception) as e: + assert photos[0].export(dest) + assert e.type == type(FileNotFoundError())