Refactored PhotoInfo to use properties instead of methods--major update
This commit is contained in:
@@ -9,7 +9,7 @@ import osxphotos
|
||||
|
||||
from ._version import __version__
|
||||
|
||||
# TODO: add "--any" to search any field (e.g. keyword, description, name contains "wedding") (add case insensitive option)
|
||||
# TODO: add "--any" to search any field (e.g. keyword, description, title contains "wedding") (add case insensitive option)
|
||||
|
||||
|
||||
class CLI_Obj:
|
||||
@@ -169,9 +169,9 @@ def list_libraries(cli_obj):
|
||||
@click.option("--album", default=None, multiple=True, help="Search for album(s).")
|
||||
@click.option("--uuid", default=None, multiple=True, help="Search for UUID(s).")
|
||||
@click.option(
|
||||
"--name", default=None, multiple=True, help="Search for TEXT in name of photo."
|
||||
"--title", default=None, multiple=True, help="Search for TEXT in title of photo."
|
||||
)
|
||||
@click.option("--no-name", is_flag=True, help="Search for photos with no name.")
|
||||
@click.option("--no-title", is_flag=True, help="Search for photos with no title.")
|
||||
@click.option(
|
||||
"--description",
|
||||
default=None,
|
||||
@@ -185,7 +185,7 @@ def list_libraries(cli_obj):
|
||||
"-i",
|
||||
"--ignore-case",
|
||||
is_flag=True,
|
||||
help="Case insensitive search for name or description. Does not apply to keyword, person, or album.",
|
||||
help="Case insensitive search for title or description. Does not apply to keyword, person, or album.",
|
||||
)
|
||||
@click.option("--edited", is_flag=True, help="Search for photos that have been edited.")
|
||||
@click.option(
|
||||
@@ -219,8 +219,8 @@ def query(
|
||||
person,
|
||||
album,
|
||||
uuid,
|
||||
name,
|
||||
no_name,
|
||||
title,
|
||||
no_title,
|
||||
description,
|
||||
no_description,
|
||||
ignore_case,
|
||||
@@ -246,8 +246,8 @@ def query(
|
||||
person,
|
||||
album,
|
||||
uuid,
|
||||
name,
|
||||
no_name,
|
||||
title,
|
||||
no_title,
|
||||
description,
|
||||
no_description,
|
||||
edited,
|
||||
@@ -274,8 +274,8 @@ def query(
|
||||
# can't search for both missing and notmissing
|
||||
click.echo(cli.commands["query"].get_help(ctx))
|
||||
return
|
||||
elif name and no_name:
|
||||
# can't search for both name and no_name
|
||||
elif title and no_title:
|
||||
# can't search for both title and no_title
|
||||
click.echo(cli.commands["query"].get_help(ctx))
|
||||
return
|
||||
elif description and no_description:
|
||||
@@ -288,19 +288,19 @@ def query(
|
||||
keywords=keyword, persons=person, albums=album, uuid=uuid
|
||||
)
|
||||
|
||||
if name:
|
||||
# search name field for text
|
||||
# if more than one, find photos with all name values in in name
|
||||
if title:
|
||||
# search title field for text
|
||||
# if more than one, find photos with all title values in title
|
||||
if ignore_case:
|
||||
# case-insensitive
|
||||
for n in name:
|
||||
n = n.lower()
|
||||
photos = [p for p in photos if p.name() and n in p.name().lower()]
|
||||
for t in title:
|
||||
t = t.lower()
|
||||
photos = [p for p in photos if p.title and t in p.title.lower()]
|
||||
else:
|
||||
for n in name:
|
||||
photos = [p for p in photos if p.name() and n in p.name()]
|
||||
elif no_name:
|
||||
photos = [p for p in photos if not p.name()]
|
||||
for t in title:
|
||||
photos = [p for p in photos if p.title and t in p.title]
|
||||
elif no_title:
|
||||
photos = [p for p in photos if not p.title]
|
||||
|
||||
if description:
|
||||
# search description field for text
|
||||
@@ -361,7 +361,7 @@ def print_photo_info(photos, json=False):
|
||||
if json:
|
||||
dump = []
|
||||
for p in photos:
|
||||
dump.append(p.to_json())
|
||||
dump.append(p.json())
|
||||
click.echo(f"[{', '.join(dump)}]")
|
||||
else:
|
||||
# dump as CSV
|
||||
@@ -377,7 +377,7 @@ def print_photo_info(photos, json=False):
|
||||
"original_filename",
|
||||
"date",
|
||||
"description",
|
||||
"name",
|
||||
"title",
|
||||
"keywords",
|
||||
"albums",
|
||||
"persons",
|
||||
@@ -395,24 +395,24 @@ def print_photo_info(photos, json=False):
|
||||
for p in photos:
|
||||
dump.append(
|
||||
[
|
||||
p.uuid(),
|
||||
p.filename(),
|
||||
p.original_filename(),
|
||||
str(p.date()),
|
||||
p.description(),
|
||||
p.name(),
|
||||
", ".join(p.keywords()),
|
||||
", ".join(p.albums()),
|
||||
", ".join(p.persons()),
|
||||
p.path(),
|
||||
p.ismissing(),
|
||||
p.hasadjustments(),
|
||||
p.external_edit(),
|
||||
p.favorite(),
|
||||
p.hidden(),
|
||||
p._latitude(),
|
||||
p._longitude(),
|
||||
p.path_edited(),
|
||||
p.uuid,
|
||||
p.filename,
|
||||
p.original_filename,
|
||||
str(p.date),
|
||||
p.description,
|
||||
p.title,
|
||||
", ".join(p.keywords),
|
||||
", ".join(p.albums),
|
||||
", ".join(p.persons),
|
||||
p.path,
|
||||
p.ismissing,
|
||||
p.hasadjustments,
|
||||
p.external_edit,
|
||||
p.favorite,
|
||||
p.hidden,
|
||||
p._latitude,
|
||||
p._longitude,
|
||||
p.path_edited,
|
||||
]
|
||||
)
|
||||
for row in dump:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.16.02"
|
||||
__version__ = "0.17.00"
|
||||
|
||||
@@ -30,15 +30,18 @@ class PhotoInfo:
|
||||
self._info = info
|
||||
self._db = db
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
""" filename of the picture """
|
||||
return self._info["filename"]
|
||||
|
||||
@property
|
||||
def original_filename(self):
|
||||
""" original filename of the picture """
|
||||
""" Photos 5 mangles filenames upon import """
|
||||
return self._info["originalFilename"]
|
||||
|
||||
@property
|
||||
def date(self):
|
||||
""" image creation date as timezone aware datetime object """
|
||||
imagedate = self._info["imageDate"]
|
||||
@@ -48,10 +51,12 @@ class PhotoInfo:
|
||||
imagedate_utc = imagedate.astimezone(tz=tz)
|
||||
return imagedate_utc
|
||||
|
||||
@property
|
||||
def tzoffset(self):
|
||||
""" timezone offset from UTC in seconds """
|
||||
return self._info["imageTimeZoneOffsetSeconds"]
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
""" absolute path on disk of the original picture """
|
||||
photopath = ""
|
||||
@@ -93,6 +98,7 @@ class PhotoInfo:
|
||||
|
||||
return photopath
|
||||
|
||||
@property
|
||||
def path_edited(self):
|
||||
""" absolute path on disk of the edited picture """
|
||||
""" None if photo has not been edited """
|
||||
@@ -165,14 +171,17 @@ class PhotoInfo:
|
||||
|
||||
return photopath
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
""" long / extended description of picture """
|
||||
return self._info["extendedDescription"]
|
||||
|
||||
@property
|
||||
def persons(self):
|
||||
""" list of persons in picture """
|
||||
return self._info["persons"]
|
||||
|
||||
@property
|
||||
def albums(self):
|
||||
""" list of albums picture is contained in """
|
||||
albums = []
|
||||
@@ -180,24 +189,23 @@ class PhotoInfo:
|
||||
albums.append(self._db._dbalbum_details[album]["title"])
|
||||
return albums
|
||||
|
||||
@property
|
||||
def keywords(self):
|
||||
""" list of keywords for picture """
|
||||
return self._info["keywords"]
|
||||
|
||||
def name(self):
|
||||
""" (deprecated) name / title of picture """
|
||||
# TODO: add warning on deprecation
|
||||
return self._info["name"]
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
""" name / title of picture """
|
||||
# TODO: Update documentation and tests to use title
|
||||
return self._info["name"]
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
""" UUID of picture """
|
||||
return self._uuid
|
||||
|
||||
@property
|
||||
def ismissing(self):
|
||||
""" returns true if photo is missing from disk (which means it's not been downloaded from iCloud)
|
||||
NOTE: the photos.db database uses an asynchrounous write-ahead log so changes in Photos
|
||||
@@ -210,10 +218,12 @@ class PhotoInfo:
|
||||
"""
|
||||
return True if self._info["isMissing"] == 1 else False
|
||||
|
||||
@property
|
||||
def hasadjustments(self):
|
||||
""" True if picture has adjustments / edits """
|
||||
return True if self._info["hasAdjustments"] == 1 else False
|
||||
|
||||
@property
|
||||
def external_edit(self):
|
||||
""" Returns True if picture was edited outside of Photos using external editor """
|
||||
return (
|
||||
@@ -222,17 +232,20 @@ class PhotoInfo:
|
||||
else False
|
||||
)
|
||||
|
||||
@property
|
||||
def favorite(self):
|
||||
""" True if picture is marked as favorite """
|
||||
return True if self._info["favorite"] == 1 else False
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
""" True if picture is hidden """
|
||||
return True if self._info["hidden"] == 1 else False
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
""" returns (latitude, longitude) as float in degrees or None """
|
||||
return (self._latitude(), self._longitude())
|
||||
return (self._latitude, self._longitude)
|
||||
|
||||
def export(
|
||||
self,
|
||||
@@ -278,43 +291,43 @@ class PhotoInfo:
|
||||
# need to use file extension from edited file as Photos saves a jpeg once edited
|
||||
if edited:
|
||||
# verify we have a valid path_edited and use that to get filename
|
||||
if not self.path_edited():
|
||||
if not self.path_edited:
|
||||
raise FileNotFoundError(
|
||||
f"edited=True but path_edited is none; hasadjustments: {self.hasadjustments()}"
|
||||
f"edited=True but path_edited is none; hasadjustments: {self.hasadjustments}"
|
||||
)
|
||||
edited_name = Path(self.path_edited()).name
|
||||
edited_name = Path(self.path_edited).name
|
||||
edited_suffix = Path(edited_name).suffix
|
||||
filename = Path(self.filename()).stem + "_edited" + edited_suffix
|
||||
filename = Path(self.filename).stem + "_edited" + edited_suffix
|
||||
else:
|
||||
filename = self.filename()
|
||||
filename = self.filename
|
||||
|
||||
# get path to source file and verify it's not None and is valid file
|
||||
# TODO: how to handle ismissing or not hasadjustments and edited=True cases?
|
||||
if edited:
|
||||
if not self.hasadjustments():
|
||||
if not self.hasadjustments:
|
||||
logging.warning(
|
||||
"Attempting to export edited photo but hasadjustments=False"
|
||||
)
|
||||
|
||||
if self.path_edited() is not None:
|
||||
src = self.path_edited()
|
||||
if self.path_edited is not None:
|
||||
src = self.path_edited
|
||||
else:
|
||||
raise FileNotFoundError(
|
||||
f"edited=True but path_edited is none; hasadjustments: {self.hasadjustments()}"
|
||||
f"edited=True but path_edited is none; hasadjustments: {self.hasadjustments}"
|
||||
)
|
||||
else:
|
||||
if self.ismissing():
|
||||
if self.ismissing:
|
||||
logging.warning(
|
||||
f"Attempting to export photo with ismissing=True: path = {self.path()}"
|
||||
f"Attempting to export photo with ismissing=True: path = {self.path}"
|
||||
)
|
||||
|
||||
if self.path() is None:
|
||||
if self.path is None:
|
||||
logging.warning(
|
||||
f"Attempting to export photo but path is None: ismissing = {self.ismissing()}"
|
||||
f"Attempting to export photo but path is None: ismissing = {self.ismissing}"
|
||||
)
|
||||
raise FileNotFoundError("Cannot export photo if path is None")
|
||||
else:
|
||||
src = self.path()
|
||||
src = self.path
|
||||
|
||||
if not os.path.isfile(src):
|
||||
raise FileNotFoundError(f"{src} does not appear to exist")
|
||||
@@ -368,25 +381,25 @@ class PhotoInfo:
|
||||
def _exiftool_json_sidecar(self):
|
||||
""" return json string of EXIF details in exiftool sidecar format """
|
||||
exif = {}
|
||||
exif["FileName"] = self.filename()
|
||||
exif["FileName"] = self.filename
|
||||
|
||||
if self.description():
|
||||
exif["ImageDescription"] = self.description()
|
||||
exif["Description"] = self.description()
|
||||
if self.description:
|
||||
exif["ImageDescription"] = self.description
|
||||
exif["Description"] = self.description
|
||||
|
||||
if self.title():
|
||||
exif["Title"] = self.title()
|
||||
if self.title:
|
||||
exif["Title"] = self.title
|
||||
|
||||
if self.keywords():
|
||||
exif["TagsList"] = exif["Keywords"] = self.keywords()
|
||||
if self.keywords:
|
||||
exif["TagsList"] = exif["Keywords"] = self.keywords
|
||||
|
||||
if self.persons():
|
||||
exif["PersonInImage"] = self.persons()
|
||||
if self.persons:
|
||||
exif["PersonInImage"] = self.persons
|
||||
|
||||
# if self.favorite():
|
||||
# exif["Rating"] = 5
|
||||
|
||||
(lat, lon) = self.location()
|
||||
(lat, lon) = self.location
|
||||
if lat is not None and lon is not None:
|
||||
lat_str, lon_str = dd_to_dms_str(lat, lon)
|
||||
exif["GPSLatitude"] = lat_str
|
||||
@@ -398,7 +411,7 @@ class PhotoInfo:
|
||||
exif["GPSLongitudeRef"] = lon_ref
|
||||
|
||||
# process date/time and timezone offset
|
||||
date = self.date()
|
||||
date = self.date
|
||||
# exiftool expects format to "2015:01:18 12:00:00"
|
||||
datetimeoriginal = date.strftime("%Y:%m:%d %H:%M:%S")
|
||||
offsettime = date.strftime("%z")
|
||||
@@ -425,10 +438,12 @@ class PhotoInfo:
|
||||
f.write(json_str)
|
||||
f.close()
|
||||
|
||||
@property
|
||||
def _longitude(self):
|
||||
""" Returns longitude, in degrees """
|
||||
return self._info["longitude"]
|
||||
|
||||
@property
|
||||
def _latitude(self):
|
||||
""" Returns latitude, in degrees """
|
||||
return self._info["latitude"]
|
||||
@@ -439,49 +454,49 @@ class PhotoInfo:
|
||||
|
||||
def __str__(self):
|
||||
info = {
|
||||
"uuid": self.uuid(),
|
||||
"filename": self.filename(),
|
||||
"original_filename": self.original_filename(),
|
||||
"date": str(self.date()),
|
||||
"description": self.description(),
|
||||
"name": self.name(),
|
||||
"keywords": self.keywords(),
|
||||
"albums": self.albums(),
|
||||
"persons": self.persons(),
|
||||
"path": self.path(),
|
||||
"ismissing": self.ismissing(),
|
||||
"hasadjustments": self.hasadjustments(),
|
||||
"external_edit": self.external_edit(),
|
||||
"favorite": self.favorite(),
|
||||
"hidden": self.hidden(),
|
||||
"latitude": self._latitude(),
|
||||
"longitude": self._longitude(),
|
||||
"path_edited": self.path_edited(),
|
||||
"uuid": self.uuid,
|
||||
"filename": self.filename,
|
||||
"original_filename": self.original_filename,
|
||||
"date": str(self.date),
|
||||
"description": self.description,
|
||||
"name": self.name,
|
||||
"keywords": self.keywords,
|
||||
"albums": self.albums,
|
||||
"persons": self.persons,
|
||||
"path": self.path,
|
||||
"ismissing": self.ismissing,
|
||||
"hasadjustments": self.hasadjustments,
|
||||
"external_edit": self.external_edit,
|
||||
"favorite": self.favorite,
|
||||
"hidden": self.hidden,
|
||||
"latitude": self._latitude,
|
||||
"longitude": self._longitude,
|
||||
"path_edited": self.path_edited,
|
||||
}
|
||||
return yaml.dump(info, sort_keys=False)
|
||||
|
||||
def to_json(self):
|
||||
def json(self):
|
||||
""" return JSON representation """
|
||||
# TODO: Add additional details here
|
||||
pic = {
|
||||
"uuid": self.uuid(),
|
||||
"filename": self.filename(),
|
||||
"original_filename": self.original_filename(),
|
||||
"date": str(self.date()),
|
||||
"description": self.description(),
|
||||
"name": self.name(),
|
||||
"keywords": self.keywords(),
|
||||
"albums": self.albums(),
|
||||
"persons": self.persons(),
|
||||
"path": self.path(),
|
||||
"ismissing": self.ismissing(),
|
||||
"hasadjustments": self.hasadjustments(),
|
||||
"external_edit": self.external_edit(),
|
||||
"favorite": self.favorite(),
|
||||
"hidden": self.hidden(),
|
||||
"latitude": self._latitude(),
|
||||
"longitude": self._longitude(),
|
||||
"path_edited": self.path_edited(),
|
||||
"uuid": self.uuid,
|
||||
"filename": self.filename,
|
||||
"original_filename": self.original_filename,
|
||||
"date": str(self.date),
|
||||
"description": self.description,
|
||||
"title": self.title,
|
||||
"keywords": self.keywords,
|
||||
"albums": self.albums,
|
||||
"persons": self.persons,
|
||||
"path": self.path,
|
||||
"ismissing": self.ismissing,
|
||||
"hasadjustments": self.hasadjustments,
|
||||
"external_edit": self.external_edit,
|
||||
"favorite": self.favorite,
|
||||
"hidden": self.hidden,
|
||||
"latitude": self._latitude,
|
||||
"longitude": self._longitude,
|
||||
"path_edited": self.path_edited,
|
||||
}
|
||||
return json.dumps(pic)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user