Added path_edited() for Photos 5, still needs to be added for Photos <= 4.0
This commit is contained in:
@@ -330,6 +330,9 @@ Returns a list of the names of the persons in the photo
|
|||||||
#### `path()`
|
#### `path()`
|
||||||
Returns the absolute path to the photo on disk as a string. Note: this returns the path to the *original* unedited file (see `hasadjustments()`). If the file is missing on disk, path=`None` (see `ismissing()`)
|
Returns the absolute path to the photo on disk as a string. Note: this returns the path to the *original* unedited file (see `hasadjustments()`). If the file is missing on disk, path=`None` (see `ismissing()`)
|
||||||
|
|
||||||
|
#### `path_edited()`
|
||||||
|
Returns the absolute path to the edited photo on disk as a string. If the photo has not beed edited, returns `None`. See also `path()` and `hasadjustments()`. Note: Currently only implemented for Photos 5.0+ (MacOS 10.15); returns `None` on previous versions.
|
||||||
|
|
||||||
#### `ismissing()`
|
#### `ismissing()`
|
||||||
Returns `True` if the original image file is missing on disk, otherwise `False`. This can occur if the file has been uploaded to iCloud but not yet downloaded to the local library or if the file was deleted or imported from a disk that has been unmounted. Note: this status is set by Photos and osxphotos does not verify that the file path returned by `path()` actually exists. It merely reports what Photos has stored in the library database.
|
Returns `True` if the original image file is missing on disk, otherwise `False`. This can occur if the file has been uploaded to iCloud but not yet downloaded to the local library or if the file was deleted or imported from a disk that has been unmounted. Note: this status is set by Photos and osxphotos does not verify that the file path returned by `path()` actually exists. It merely reports what Photos has stored in the library database.
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from Foundation import *
|
|||||||
|
|
||||||
from . import _applescript
|
from . import _applescript
|
||||||
|
|
||||||
# TODO: add hasAdjustments to process_database5 (see ZGENERICASSET.ZHASADJUSTMENTS = 1 )
|
|
||||||
# TODO: find edited photos: see https://github.com/orangeturtle739/photos-export/blob/master/extract_photos.py
|
# TODO: find edited photos: see https://github.com/orangeturtle739/photos-export/blob/master/extract_photos.py
|
||||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||||
# TODO: Fix command line so multiple --keyword, etc. are AND (instead of OR as they are in .photos())
|
# TODO: Fix command line so multiple --keyword, etc. are AND (instead of OR as they are in .photos())
|
||||||
@@ -149,12 +148,11 @@ class PhotosDB:
|
|||||||
self._dbfile = dbfile
|
self._dbfile = dbfile
|
||||||
self._tmp_db = self._copy_db_file(self._dbfile)
|
self._tmp_db = self._copy_db_file(self._dbfile)
|
||||||
|
|
||||||
# zzz
|
# TODO: replace os.path with pathlib?
|
||||||
|
# TODO: clean this up -- library path computed twice
|
||||||
# TODO: replace os.path with pathlib
|
|
||||||
# TODO: clean this up -- we'll already know library_path
|
|
||||||
library_path = os.path.dirname(os.path.abspath(dbfile))
|
library_path = os.path.dirname(os.path.abspath(dbfile))
|
||||||
(library_path, _) = os.path.split(library_path)
|
(library_path, _) = os.path.split(library_path)
|
||||||
|
self._library_path = library_path
|
||||||
if int(self._db_version) < int(_PHOTOS_5_VERSION):
|
if int(self._db_version) < int(_PHOTOS_5_VERSION):
|
||||||
masters_path = os.path.join(library_path, "Masters")
|
masters_path = os.path.join(library_path, "Masters")
|
||||||
self._masters_path = masters_path
|
self._masters_path = masters_path
|
||||||
@@ -273,8 +271,10 @@ class PhotosDB:
|
|||||||
return os.path.abspath(self._dbfile)
|
return os.path.abspath(self._dbfile)
|
||||||
|
|
||||||
def get_photos_library_path(self):
|
def get_photos_library_path(self):
|
||||||
""" return the path to the Photos library """
|
""" return the path to the last opened Photos library """
|
||||||
# TODO: move this to a module-level function
|
# 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(
|
plist_file = Path(
|
||||||
str(Path.home())
|
str(Path.home())
|
||||||
+ "/Library/Containers/com.apple.Photos/Data/Library/Preferences/com.apple.Photos.plist"
|
+ "/Library/Containers/com.apple.Photos/Data/Library/Preferences/com.apple.Photos.plist"
|
||||||
@@ -776,9 +776,7 @@ class PhotosDB:
|
|||||||
row[5] + td
|
row[5] + td
|
||||||
)
|
)
|
||||||
|
|
||||||
self._dbphotos[uuid]["imageDate"] = datetime.fromtimestamp(
|
self._dbphotos[uuid]["imageDate"] = datetime.fromtimestamp(row[5] + td)
|
||||||
row[5] + td
|
|
||||||
)
|
|
||||||
self._dbphotos[uuid]["imageTimeZoneOffsetSeconds"] = row[6]
|
self._dbphotos[uuid]["imageTimeZoneOffsetSeconds"] = row[6]
|
||||||
self._dbphotos[uuid]["hidden"] = row[9]
|
self._dbphotos[uuid]["hidden"] = row[9]
|
||||||
self._dbphotos[uuid]["favorite"] = row[10]
|
self._dbphotos[uuid]["favorite"] = row[10]
|
||||||
@@ -824,6 +822,23 @@ class PhotosDB:
|
|||||||
f"WARNING: found description {row[1]} but no photo for {uuid}"
|
f"WARNING: found description {row[1]} but no photo for {uuid}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# get information about adjusted/edited photos
|
||||||
|
c.execute(
|
||||||
|
"SELECT ZGENERICASSET.ZUUID, ZGENERICASSET.ZHASADJUSTMENTS, ZUNMANAGEDADJUSTMENT.ZADJUSTMENTFORMATIDENTIFIER "
|
||||||
|
"FROM ZGENERICASSET, ZUNMANAGEDADJUSTMENT "
|
||||||
|
"JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = ZGENERICASSET.Z_PK "
|
||||||
|
"WHERE ZADDITIONALASSETATTRIBUTES.ZUNMANAGEDADJUSTMENT = ZUNMANAGEDADJUSTMENT.Z_PK "
|
||||||
|
"AND ZGENERICASSET.ZTRASHEDSTATE = 0 AND ZGENERICASSET.ZKIND = 0 "
|
||||||
|
)
|
||||||
|
for row in c:
|
||||||
|
uuid = row[0]
|
||||||
|
if uuid in self._dbphotos:
|
||||||
|
self._dbphotos[uuid]["adjustmentID"] = row[2]
|
||||||
|
else:
|
||||||
|
logging.debug(
|
||||||
|
f"WARNING: found adjustmentformatidentifier {row[2]} but no photo for uuid {row[0]}"
|
||||||
|
)
|
||||||
|
|
||||||
# get information on local/remote availability
|
# get information on local/remote availability
|
||||||
c.execute(
|
c.execute(
|
||||||
"SELECT ZGENERICASSET.ZUUID, "
|
"SELECT ZGENERICASSET.ZUUID, "
|
||||||
@@ -986,7 +1001,7 @@ class PhotoInfo:
|
|||||||
return self.__info["imageTimeZoneOffsetSeconds"]
|
return self.__info["imageTimeZoneOffsetSeconds"]
|
||||||
|
|
||||||
def path(self):
|
def path(self):
|
||||||
""" absolute path on disk of the picture """
|
""" absolute path on disk of the original picture """
|
||||||
photopath = ""
|
photopath = ""
|
||||||
|
|
||||||
if self.__db._db_version < _PHOTOS_5_VERSION:
|
if self.__db._db_version < _PHOTOS_5_VERSION:
|
||||||
@@ -1026,6 +1041,56 @@ class PhotoInfo:
|
|||||||
|
|
||||||
return photopath
|
return photopath
|
||||||
|
|
||||||
|
def path_edited(self):
|
||||||
|
""" absolute path on disk of the edited picture """
|
||||||
|
""" None if photo has not been edited """
|
||||||
|
photopath = ""
|
||||||
|
|
||||||
|
if self.__db._db_version < _PHOTOS_5_VERSION:
|
||||||
|
# TODO: implement this
|
||||||
|
photopath = None
|
||||||
|
logging.debug(
|
||||||
|
"WARNING: path_edited not implemented yet for this database version"
|
||||||
|
)
|
||||||
|
# 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
|
||||||
|
|
||||||
|
if self.__info["hasAdjustments"]:
|
||||||
|
library = self.__db._library_path
|
||||||
|
directory = self.__uuid[0] # first char of uuid
|
||||||
|
photopath = os.path.join(
|
||||||
|
library,
|
||||||
|
"resources",
|
||||||
|
"renders",
|
||||||
|
directory,
|
||||||
|
f"{self.__uuid}_1_201_a.jpeg",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.isfile(photopath):
|
||||||
|
logging.warning(
|
||||||
|
f"WARNING: edited file should be at {photopath} but does not appear to exist"
|
||||||
|
)
|
||||||
|
photopath = None
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
|
||||||
|
return photopath
|
||||||
|
|
||||||
def description(self):
|
def description(self):
|
||||||
""" long / extended description of picture """
|
""" long / extended description of picture """
|
||||||
return self.__info["extendedDescription"]
|
return self.__info["extendedDescription"]
|
||||||
@@ -1064,7 +1129,6 @@ class PhotoInfo:
|
|||||||
|
|
||||||
def hasadjustments(self):
|
def hasadjustments(self):
|
||||||
""" True if picture has adjustments """
|
""" True if picture has adjustments """
|
||||||
""" TODO: not accurate for Photos version >= 5 """
|
|
||||||
return True if self.__info["hasAdjustments"] == 1 else False
|
return True if self.__info["hasAdjustments"] == 1 else False
|
||||||
|
|
||||||
def favorite(self):
|
def favorite(self):
|
||||||
@@ -1108,6 +1172,7 @@ class PhotoInfo:
|
|||||||
"hidden": self.hidden(),
|
"hidden": self.hidden(),
|
||||||
"latitude": self._latitude(),
|
"latitude": self._latitude(),
|
||||||
"longitude": self._longitude(),
|
"longitude": self._longitude(),
|
||||||
|
"path_edited": self.path_edited(),
|
||||||
}
|
}
|
||||||
return yaml.dump(info, sort_keys=False)
|
return yaml.dump(info, sort_keys=False)
|
||||||
|
|
||||||
@@ -1131,6 +1196,7 @@ class PhotoInfo:
|
|||||||
"hidden": self.hidden(),
|
"hidden": self.hidden(),
|
||||||
"latitude": self._latitude(),
|
"latitude": self._latitude(),
|
||||||
"longitude": self._longitude(),
|
"longitude": self._longitude(),
|
||||||
|
"path_edited": self.path_edited(),
|
||||||
}
|
}
|
||||||
return json.dumps(pic)
|
return json.dumps(pic)
|
||||||
|
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ def print_photo_info(photos, json=False):
|
|||||||
"hidden",
|
"hidden",
|
||||||
"latitude",
|
"latitude",
|
||||||
"longitude",
|
"longitude",
|
||||||
|
"path_edited",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
for p in photos:
|
for p in photos:
|
||||||
@@ -348,6 +349,7 @@ def print_photo_info(photos, json=False):
|
|||||||
p.hidden(),
|
p.hidden(),
|
||||||
p._latitude(),
|
p._latitude(),
|
||||||
p._longitude(),
|
p._longitude(),
|
||||||
|
p.path_edited(),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
for row in dump:
|
for row in dump:
|
||||||
|
|||||||
8
setup.py
8
setup.py
@@ -38,7 +38,7 @@ with open(path.join(this_directory, "README.md"), encoding="utf-8") as f:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="osxphotos",
|
name="osxphotos",
|
||||||
version="0.14.6",
|
version="0.14.7",
|
||||||
description="Manipulate (read-only) Apple's Photos app library on Mac OS X",
|
description="Manipulate (read-only) Apple's Photos app library on Mac OS X",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
@@ -58,8 +58,6 @@ setup(
|
|||||||
"Programming Language :: Python :: 3.6",
|
"Programming Language :: Python :: 3.6",
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
],
|
],
|
||||||
install_requires=["pyobjc","Click","pyyaml",],
|
install_requires=["pyobjc", "Click", "pyyaml"],
|
||||||
entry_points = {
|
entry_points={"console_scripts": ["osxphotos=osxphotos.cmd_line:cli"]},
|
||||||
'console_scripts' : ['osxphotos=osxphotos.cmd_line:cli'],
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -230,6 +230,32 @@ def test_hasadjustments2():
|
|||||||
assert p.hasadjustments() == False
|
assert p.hasadjustments() == False
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_edited1():
|
||||||
|
# test a valid edited path
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
path = p.path_edited()
|
||||||
|
assert path.endswith(
|
||||||
|
"resources/renders/E/E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51_1_201_a.jpeg"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_edited2():
|
||||||
|
# test an invalid edited path
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=["6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
path = p.path_edited()
|
||||||
|
assert path is None
|
||||||
|
|
||||||
|
|
||||||
def test_count():
|
def test_count():
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ def test_location_1():
|
|||||||
assert lat == pytest.approx(51.50357167)
|
assert lat == pytest.approx(51.50357167)
|
||||||
assert lon == pytest.approx(-0.1318055)
|
assert lon == pytest.approx(-0.1318055)
|
||||||
|
|
||||||
|
|
||||||
def test_location_2():
|
def test_location_2():
|
||||||
# test photo with no location info
|
# test photo with no location info
|
||||||
import osxphotos
|
import osxphotos
|
||||||
@@ -205,6 +206,57 @@ def test_location_2():
|
|||||||
assert lat is None
|
assert lat is None
|
||||||
assert lon is None
|
assert lon is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_hasadjustments1():
|
||||||
|
# test hasadjustments == True
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=["6bxcNnzRQKGnK4uPrCJ9UQ"])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
assert p.hasadjustments() == True
|
||||||
|
|
||||||
|
|
||||||
|
def test_hasadjustments2():
|
||||||
|
# test hasadjustments == False
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=["15uNd7%8RguTEgNPKHfTWw"])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
assert p.hasadjustments() == False
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_edited1():
|
||||||
|
# test a valid edited path
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=["6bxcNnzRQKGnK4uPrCJ9UQ"])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
path = p.path_edited()
|
||||||
|
assert path is None
|
||||||
|
# TODO: update when implemented
|
||||||
|
# assert path.endswith(
|
||||||
|
# "resources/renders/E/E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51_1_201_a.jpeg"
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_edited2():
|
||||||
|
# test an invalid edited path
|
||||||
|
import osxphotos
|
||||||
|
|
||||||
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
|
photos = photosdb.photos(uuid=["15uNd7%8RguTEgNPKHfTWw"])
|
||||||
|
assert len(photos) == 1
|
||||||
|
p = photos[0]
|
||||||
|
path = p.path_edited()
|
||||||
|
assert path is None
|
||||||
|
|
||||||
|
|
||||||
def test_count():
|
def test_count():
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user