From 1d006a4b50ed58b01c6116734bef5f740655a063 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sat, 7 Dec 2019 23:54:55 -0800 Subject: [PATCH] Added get_db_path and get_library_path to PhotosDB --- README.md | 8 ++- osxphotos/__init__.py | 118 +++++++++++++++++---------------- tests/test_catalina_10_15_1.py | 30 ++++++++- tests/test_mojave_10_14_6.py | 44 +++++------- 4 files changed, 110 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 59d24696..30646d9e 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,11 @@ if __name__ == "__main__": #### ```get_system_library_path()``` -**MacOS 10.15 Only** Return path to System Photo Library as string. On MacOS version < 10.15, raises Exception. +**MacOS 10.15 Only** Returns path to System Photo Library as string. On MacOS version < 10.15, raises Exception. + +#### ```get_last_library_path()``` + +Returns path to last opened Photo Library as string. ### PhotosDB @@ -216,7 +220,7 @@ Returns a dictionary of albums found in the Photos library where key is the albu **Note**: In Photos 5.0 (MacOS 10.15/Catalina), It is possible to have more than one album with the same name in Photos. Albums with duplicate names are treated as a single album and the photos in each are combined. For example, if you have two albums named "Wedding" and each has 2 photos, osxphotos will treat this as a single album named "Wedding" with 4 photos in it. -#### ```get_photos_library_path``` +#### ```get_library_path``` ```python # assumes photosdb is a PhotosDB object (see above) photosdb.get_photos_library_path() diff --git a/osxphotos/__init__.py b/osxphotos/__init__.py index 094fe729..61749b71 100644 --- a/osxphotos/__init__.py +++ b/osxphotos/__init__.py @@ -109,13 +109,16 @@ def _get_resource_loc(model_id): return folder_id, file_id + def get_system_library_path(): """ return the path to the system Photos library as string """ """ only works on MacOS 10.15+ """ """ on earlier versions, will raise exception """ - ver, major, minor = _get_os_version() + _, major, _ = _get_os_version() if int(major) < 15: - raise Exception("get_system_library_path not implemented for MacOS < 10.15",major) + raise Exception( + "get_system_library_path not implemented for MacOS < 10.15", major + ) plist_file = Path( str(Path.home()) @@ -135,7 +138,57 @@ def get_system_library_path(): else: logging.warning("Could not get path to Photos database") return None - + + +def get_last_library_path(): + """ return the path to the last opened Photos library """ + # TODO: Need a module level method for this and another PhotosDB method to get current library path + plist_file = Path( + str(Path.home()) + + "/Library/Containers/com.apple.Photos/Data/Library/Preferences/com.apple.Photos.plist" + ) + if plist_file.is_file(): + with open(plist_file, "rb") as fp: + pl = plistload(fp) + else: + logging.warning(f"could not find plist file: {str(plist_file)}") + return None + + # get the IPXDefaultLibraryURLBookmark from com.apple.Photos.plist + # this is a serialized CFData object + photosurlref = pl["IPXDefaultLibraryURLBookmark"] + + if photosurlref is not None: + # use CFURLCreateByResolvingBookmarkData to de-serialize bookmark data into a CFURLRef + photosurl = CoreFoundation.CFURLCreateByResolvingBookmarkData( + kCFAllocatorDefault, photosurlref, 0, None, None, None, None + ) + + # the CFURLRef we got is a sruct that python treats as an array + # I'd like to pass this to CFURLGetFileSystemRepresentation to get the path but + # CFURLGetFileSystemRepresentation barfs when it gets an array from python instead of expected struct + # first element is the path string in form: + # file:///Users/username/Pictures/Photos%20Library.photoslibrary/ + photosurlstr = photosurl[0].absoluteString() if photosurl[0] else None + + # now coerce the file URI back into an OS path + # surely there must be a better way + if photosurlstr is not None: + photospath = os.path.normpath( + urllib.parse.unquote(urllib.parse.urlparse(photosurlstr).path) + ) + else: + logging.warning( + "Could not extract photos URL String from IPXDefaultLibraryURLBookmark" + ) + return None + + return photospath + else: + logging.warning("Could not get path to Photos database") + return None + + class PhotosDB: def __init__(self, dbfile=None): """ create a new PhotosDB object """ @@ -181,7 +234,7 @@ class PhotosDB: logging.debug(f"dbfile = {dbfile}") if dbfile is None: - library_path = self.get_photos_library_path() + library_path = get_last_library_path() # TODO: verify library path not None dbfile = os.path.join(library_path, "database/photos.db") @@ -283,7 +336,7 @@ class PhotosDB: """ return list of albums found in photos database """ # Could be more than one album with same name # Right now, they are treated as same album and photos are combined from albums with same name - albums = set() + albums = set() for album in self._dbalbums_album.keys(): albums.add(self._dbalbum_details[album]["title"]) return list(albums) @@ -333,59 +386,12 @@ class PhotosDB: return self._db_version def get_db_path(self): - """ return path to the Photos library database PhotosDB was initialized with """ + """ returns path to the Photos library database PhotosDB was initialized with """ return os.path.abspath(self._dbfile) - def get_photos_library_path(self): - """ return the path to the last opened Photos library """ - # TODO: this is only for last opened library - # TODO: Need a module level method for this and another PhotosDB method to get current library path - # TODO: Also need a way to get path of system library - plist_file = Path( - str(Path.home()) - + "/Library/Containers/com.apple.Photos/Data/Library/Preferences/com.apple.Photos.plist" - ) - if plist_file.is_file(): - with open(plist_file, "rb") as fp: - pl = plistload(fp) - else: - print("could not find plist file: " + str(plist_file), file=sys.stderr) - return None - - # get the IPXDefaultLibraryURLBookmark from com.apple.Photos.plist - # this is a serialized CFData object - photosurlref = pl["IPXDefaultLibraryURLBookmark"] - - if photosurlref is not None: - # use CFURLCreateByResolvingBookmarkData to de-serialize bookmark data into a CFURLRef - photosurl = CoreFoundation.CFURLCreateByResolvingBookmarkData( - kCFAllocatorDefault, photosurlref, 0, None, None, None, None - ) - - # the CFURLRef we got is a sruct that python treats as an array - # I'd like to pass this to CFURLGetFileSystemRepresentation to get the path but - # CFURLGetFileSystemRepresentation barfs when it gets an array from python instead of expected struct - # first element is the path string in form: - # file:///Users/username/Pictures/Photos%20Library.photoslibrary/ - photosurlstr = photosurl[0].absoluteString() if photosurl[0] else None - - # now coerce the file URI back into an OS path - # surely there must be a better way - if photosurlstr is not None: - photospath = os.path.normpath( - urllib.parse.unquote(urllib.parse.urlparse(photosurlstr).path) - ) - else: - print( - "Could not extract photos URL String from IPXDefaultLibraryURLBookmark", - file=sys.stderr, - ) - return None - - return photospath - else: - print("Could not get path to Photos database", file=sys.stderr) - return None + def get_library_path(self): + """ returns path to the Photos library PhotosDB was initialized with """ + return self._library_path def _copy_db_file(self, fname): """ copies the sqlite database file to a temp file """ diff --git a/tests/test_catalina_10_15_1.py b/tests/test_catalina_10_15_1.py index 5a002c4e..7a99b51d 100644 --- a/tests/test_catalina_10_15_1.py +++ b/tests/test_catalina_10_15_1.py @@ -5,6 +5,9 @@ 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", @@ -18,7 +21,10 @@ KEYWORDS = [ ] # 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 +ALBUMS = [ + "Pumpkin Farm", + "Test Album", +] # Note: there are 2 albums named "Test Album" for testing duplicate album names KEYWORDS_DICT = { "Kids": 4, "wedding": 2, @@ -31,8 +37,10 @@ KEYWORDS_DICT = { "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 - +ALBUM_DICT = { + "Pumpkin Farm": 3, + "Test Album": 2, +} # Note: there are 2 albums named "Test Album" for testing duplicate album names def test_init(): @@ -312,3 +320,19 @@ def test_keyword_not_in_album(): assert len(photos3) == 1 assert photos3[0].uuid() == "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C" + +def test_get_db_path(): + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + db_path = photosdb.get_db_path() + assert db_path.endswith(PHOTOS_DB_PATH) + + +def test_get_library_path(): + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + lib_path = photosdb.get_library_path() + assert lib_path.endswith(PHOTOS_LIBRARY_PATH) + diff --git a/tests/test_mojave_10_14_6.py b/tests/test_mojave_10_14_6.py index d0e433fa..d0a11a07 100644 --- a/tests/test_mojave_10_14_6.py +++ b/tests/test_mojave_10_14_6.py @@ -3,6 +3,9 @@ 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" + KEYWORDS = [ "Kids", "wedding", @@ -307,35 +310,18 @@ def test_keyword_not_in_album(): assert photos3[0].uuid() == "od0fmC7NQx+ayVr+%i06XA" -# def main(): -# photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) -# print(photosdb.keywords()) -# print(photosdb.persons()) -# print(photosdb.albums()) +def test_get_db_path(): + import osxphotos -# print(photosdb.keywords_as_dict()) -# print(photosdb.persons_as_dict()) -# print(photosdb.albums_as_dict()) + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + db_path = photosdb.get_db_path() + assert db_path.endswith(PHOTOS_DB_PATH) -# # # find all photos with Keyword = Foo and containing John Smith -# # photos = photosdb.photos(keywords=["Foo"],persons=["John Smith"]) -# # -# # # find all photos that include Alice Smith but do not contain the keyword Bar -# # photos = [p for p in photosdb.photos(persons=["Alice Smith"]) -# # if p not in photosdb.photos(keywords=["Bar"]) ] -# photos = photosdb.photos() -# for p in photos: -# print( -# p.uuid(), -# p.filename(), -# p.date(), -# p.description(), -# p.name(), -# p.keywords(), -# p.albums(), -# p.persons(), -# p.path(), -# ) -# if __name__ == "__main__": -# main() +def test_get_library_path(): + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + lib_path = photosdb.get_library_path() + assert lib_path.endswith(PHOTOS_LIBRARY_PATH) +