From 44321da243e374c5239e9bcd28c3515e32e1076a Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Wed, 27 Nov 2019 07:27:49 -0800 Subject: [PATCH] Added location (latitude/longitude), closes issue #2 --- README.md | 5 ++++- osxphotos/__init__.py | 36 ++++++++++++++++++++++++---------- setup.py | 2 +- tests/test_catalina_10_15_1.py | 24 +++++++++++++++++++++++ tests/test_mojave_10_14_6.py | 24 +++++++++++++++++++++++ 5 files changed, 79 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6459bd01..9a40f775 100644 --- a/README.md +++ b/README.md @@ -342,6 +342,9 @@ Returns `True` if the picture has been marked as a favorite, otherwise `False` #### `hidden()` Returns `True` if the picture has been marked as hidden, otherwise `False` +#### `location()` +Returns latitude and longitude as a tuple of floats (latitude, longitude). If location is not set, latitude and longitude are returned as `None` + #### `to_json()` Returns a JSON representation of all photo info @@ -386,7 +389,7 @@ The sqlite3 database used by Photos uses write ahead logging that is updated asy - [Click](https://pypi.org/project/click/) ## Acknowledgements -This code 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 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/osxphotos/__init__.py b/osxphotos/__init__.py index a428cfe4..fe55cd51 100644 --- a/osxphotos/__init__.py +++ b/osxphotos/__init__.py @@ -378,7 +378,7 @@ class PhotosDB: """ process the Photos database to extract info """ """ works on Photos version <= 4.0 """ - #TODO: Update strings to remove + (not needed) + # TODO: Update strings to remove + (not needed) # Epoch is Jan 1, 2001 td = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds() @@ -491,7 +491,7 @@ class PhotosDB: + "RKVersion.hasAdjustments, RKVersion.hasKeywords, RKVersion.imageTimeZoneOffsetSeconds, " + "RKMaster.volumeId, RKMaster.imagePath, RKVersion.extendedDescription, RKVersion.name, " + "RKMaster.isMissing, RKMaster.originalFileName, RKVersion.isFavorite, RKVersion.isHidden, " - + "RKVersion.longitude, RKVersion.latitude " + + "RKVersion.latitude, RKVersion.longitude " + "from RKVersion, RKMaster where RKVersion.isInTrash = 0 and RKVersion.type = 2 and " + "RKVersion.masterUuid = RKMaster.uuid and RKVersion.filename not like '%.pdf'" ) @@ -529,9 +529,8 @@ class PhotosDB: self._dbphotos[uuid]["originalFilename"] = row[15] self._dbphotos[uuid]["favorite"] = row[16] self._dbphotos[uuid]["hidden"] = row[17] - self._dbphotos[uuid]["longitude"] = row[18] - self._dbphotos[uuid]["latitude"] = row[19] - + self._dbphotos[uuid]["latitude"] = row[18] + self._dbphotos[uuid]["longitude"] = row[19] conn.close() @@ -735,7 +734,9 @@ class PhotosDB: "ZGENERICASSET.ZHIDDEN, " "ZGENERICASSET.ZFAVORITE, " "ZGENERICASSET.ZDIRECTORY, " - "ZGENERICASSET.ZFILENAME " + "ZGENERICASSET.ZFILENAME, " + "ZGENERICASSET.ZLATITUDE, " + "ZGENERICASSET.ZLONGITUDE " "FROM ZGENERICASSET " "JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK " "WHERE ZGENERICASSET.ZTRASHEDSTATE = 0 AND ZGENERICASSET.ZKIND = 0 " @@ -761,7 +762,9 @@ class PhotosDB: # 9 "ZGENERICASSET.ZHIDDEN, " # 10 "ZGENERICASSET.ZFAVORITE, " # 11 "ZGENERICASSET.ZDIRECTORY, " - # 12 "ZGENERICASSET.ZFILENAME " + # 12 "ZGENERICASSET.ZFILENAME, " + # 13 "ZGENERICASSET.ZLATITUDE, " + # 14 "ZGENERICASSET.ZLONGITUDE " i = 0 for row in c: @@ -799,6 +802,15 @@ class PhotosDB: self._dbphotos[uuid]["filename"] = row[12] self._dbphotos[uuid]["directory"] = row[11] + # set latitude and longitude + # if both latitude and longitude = -180.0, then they are NULL + if row[13] == -180.0 and row[14] == -180.0: + self._dbphotos[uuid]["latitude"] = None + self._dbphotos[uuid]["longitude"] = None + else: + self._dbphotos[uuid]["latitude"] = row[13] + self._dbphotos[uuid]["longitude"] = row[14] + # these will get filled in later # init to avoid key errors self._dbphotos[uuid]["extendedDescription"] = None # fill this in later @@ -1087,14 +1099,18 @@ class PhotoInfo: """ True if picture is hidden """ return True if self.__info["hidden"] == 1 else False - def longitude(self): + def location(self): + """ returns (latitude, longitude) as float in degrees or None """ + return (self._latitude(), self._longitude()) + + def _longitude(self): """ Returns longitude, in degrees """ return self.__info["longitude"] - def latitude(self): + def _latitude(self): """ Returns latitude, in degrees """ return self.__info["latitude"] - + def __repr__(self): return f"osxphotos.PhotoInfo(db={self.__db}, uuid='{self.__uuid}', info={self.__info})" diff --git a/setup.py b/setup.py index 52807bf3..d77f2a7b 100755 --- a/setup.py +++ b/setup.py @@ -38,7 +38,7 @@ with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: setup( name="osxphotos", - version="0.14.4", + version="0.14.5", description="Manipulate (read-only) Apple's Photos app library on Mac OS X", long_description=long_description, long_description_content_type="text/markdown", diff --git a/tests/test_catalina_10_15_1.py b/tests/test_catalina_10_15_1.py index ca808f0d..10fa4060 100644 --- a/tests/test_catalina_10_15_1.py +++ b/tests/test_catalina_10_15_1.py @@ -181,6 +181,30 @@ def test_not_hidden(): p = photos[0] assert p.hidden() == False +def test_location_1(): + # test photo with lat/lon info + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=["DC99FBDD-7A52-4100-A5BB-344131646C30"]) + assert len(photos) == 1 + p = photos[0] + lat, lon = p.location() + assert lat == pytest.approx(51.50357167) + assert lon == pytest.approx(-0.1318055) + + +def test_location_2(): + # test photo with no location info + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=["6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"]) + assert len(photos) == 1 + p = photos[0] + lat, lon = p.location() + assert lat is None + assert lon is None def test_count(): import osxphotos diff --git a/tests/test_mojave_10_14_6.py b/tests/test_mojave_10_14_6.py index dd3ddf81..42bf81d5 100644 --- a/tests/test_mojave_10_14_6.py +++ b/tests/test_mojave_10_14_6.py @@ -181,6 +181,30 @@ def test_not_hidden(): assert p.hidden() == False +def test_location_1(): + #test photo with lat/lon info + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=["3Jn73XpSQQCluzRBMWRsMA"]) + assert len(photos) == 1 + p = photos[0] + lat, lon = p.location() + assert lat == pytest.approx(51.50357167) + assert lon == pytest.approx(-0.1318055) + +def test_location_2(): + # test photo with no location info + import osxphotos + + photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB) + photos = photosdb.photos(uuid=["YZFCPY24TUySvpu7owiqxA"]) + assert len(photos) == 1 + p = photos[0] + lat, lon = p.location() + assert lat is None + assert lon is None + def test_count(): import osxphotos