Added path_edited() for Photos 5, still needs to be added for Photos <= 4.0

This commit is contained in:
Rhet Turnbull
2019-11-29 08:47:11 -08:00
parent 3dc0943453
commit 68eef42599
6 changed files with 169 additions and 22 deletions

View File

@@ -330,6 +330,9 @@ Returns a list of the names of the persons in the photo
#### `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()`)
#### `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()`
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.

View File

@@ -20,7 +20,6 @@ from Foundation import *
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: Add test for imageTimeZoneOffsetSeconds = None
# 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._tmp_db = self._copy_db_file(self._dbfile)
# zzz
# TODO: replace os.path with pathlib
# TODO: clean this up -- we'll already know library_path
# TODO: replace os.path with pathlib?
# TODO: clean this up -- library path computed twice
library_path = os.path.dirname(os.path.abspath(dbfile))
(library_path, _) = os.path.split(library_path)
self._library_path = library_path
if int(self._db_version) < int(_PHOTOS_5_VERSION):
masters_path = os.path.join(library_path, "Masters")
self._masters_path = masters_path
@@ -273,8 +271,10 @@ class PhotosDB:
return os.path.abspath(self._dbfile)
def get_photos_library_path(self):
""" return the path to the Photos library """
# TODO: move this to a module-level function
""" 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"
@@ -776,9 +776,7 @@ class PhotosDB:
row[5] + td
)
self._dbphotos[uuid]["imageDate"] = datetime.fromtimestamp(
row[5] + td
)
self._dbphotos[uuid]["imageDate"] = datetime.fromtimestamp(row[5] + td)
self._dbphotos[uuid]["imageTimeZoneOffsetSeconds"] = row[6]
self._dbphotos[uuid]["hidden"] = row[9]
self._dbphotos[uuid]["favorite"] = row[10]
@@ -824,6 +822,23 @@ class PhotosDB:
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
c.execute(
"SELECT ZGENERICASSET.ZUUID, "
@@ -986,7 +1001,7 @@ class PhotoInfo:
return self.__info["imageTimeZoneOffsetSeconds"]
def path(self):
""" absolute path on disk of the picture """
""" absolute path on disk of the original picture """
photopath = ""
if self.__db._db_version < _PHOTOS_5_VERSION:
@@ -1026,6 +1041,56 @@ class PhotoInfo:
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):
""" long / extended description of picture """
return self.__info["extendedDescription"]
@@ -1064,7 +1129,6 @@ class PhotoInfo:
def hasadjustments(self):
""" True if picture has adjustments """
""" TODO: not accurate for Photos version >= 5 """
return True if self.__info["hasAdjustments"] == 1 else False
def favorite(self):
@@ -1108,6 +1172,7 @@ class PhotoInfo:
"hidden": self.hidden(),
"latitude": self._latitude(),
"longitude": self._longitude(),
"path_edited": self.path_edited(),
}
return yaml.dump(info, sort_keys=False)
@@ -1131,6 +1196,7 @@ class PhotoInfo:
"hidden": self.hidden(),
"latitude": self._latitude(),
"longitude": self._longitude(),
"path_edited": self.path_edited(),
}
return json.dumps(pic)

View File

@@ -327,6 +327,7 @@ def print_photo_info(photos, json=False):
"hidden",
"latitude",
"longitude",
"path_edited",
]
)
for p in photos:
@@ -348,6 +349,7 @@ def print_photo_info(photos, json=False):
p.hidden(),
p._latitude(),
p._longitude(),
p.path_edited(),
]
)
for row in dump:

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# setup.py script for osxphotos
# setup.py script for osxphotos
#
# Copyright (c) 2019 Rhet Turnbull, rturnbull+git@gmail.com
# All rights reserved.
@@ -38,7 +38,7 @@ with open(path.join(this_directory, "README.md"), encoding="utf-8") as f:
setup(
name="osxphotos",
version="0.14.6",
version="0.14.7",
description="Manipulate (read-only) Apple's Photos app library on Mac OS X",
long_description=long_description,
long_description_content_type="text/markdown",
@@ -47,7 +47,7 @@ setup(
url="https://github.com/RhetTbull/",
project_urls={"GitHub": "https://github.com/RhetTbull/osxphotos"},
download_url="https://github.com/RhetTbull/osxphotos",
packages=find_packages(exclude=["tests","examples"]),
packages=find_packages(exclude=["tests", "examples"]),
license="License :: OSI Approved :: MIT License",
classifiers=[
"Development Status :: 4 - Beta",
@@ -58,8 +58,6 @@ setup(
"Programming Language :: Python :: 3.6",
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=["pyobjc","Click","pyyaml",],
entry_points = {
'console_scripts' : ['osxphotos=osxphotos.cmd_line:cli'],
}
install_requires=["pyobjc", "Click", "pyyaml"],
entry_points={"console_scripts": ["osxphotos=osxphotos.cmd_line:cli"]},
)

View File

@@ -216,7 +216,7 @@ def test_hasadjustments1():
photos = photosdb.photos(uuid=["E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"])
assert len(photos) == 1
p = photos[0]
assert p.hasadjustments() == True
assert p.hasadjustments() == True
def test_hasadjustments2():
@@ -230,6 +230,32 @@ def test_hasadjustments2():
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():
import osxphotos

View File

@@ -182,7 +182,7 @@ def test_not_hidden():
def test_location_1():
#test photo with lat/lon info
# test photo with lat/lon info
import osxphotos
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
@@ -193,6 +193,7 @@ def test_location_1():
assert lat == pytest.approx(51.50357167)
assert lon == pytest.approx(-0.1318055)
def test_location_2():
# test photo with no location info
import osxphotos
@@ -202,9 +203,60 @@ def test_location_2():
assert len(photos) == 1
p = photos[0]
lat, lon = p.location()
assert lat is None
assert lat 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():
import osxphotos