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()`
|
||||
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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
12
setup.py
12
setup.py
@@ -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"]},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user