Compare commits

...

12 Commits

Author SHA1 Message Date
Rhet Turnbull
15d7ad538d Added media type specials, closes #60 2020-03-08 12:52:44 -07:00
Rhet Turnbull
1f8fd6e929 Updated README.md 2020-03-07 14:56:46 -08:00
Rhet Turnbull
08a9793651 Updated CHANGELOG.md 2020-03-07 14:53:24 -08:00
Rhet Turnbull
2c8fc9789f Added check for exiftool in path 2020-03-07 14:50:16 -08:00
Rhet Turnbull
dbededcd0e Test database update 2020-03-07 14:37:29 -08:00
Rhet Turnbull
ef799610ae Added --exiftool to CLI export 2020-03-07 14:37:11 -08:00
Rhet Turnbull
8dea41961b Added exiftool 2020-03-07 09:50:30 -08:00
Rhet Turnbull
5799afbdc1 Updated TODO 2020-03-07 09:13:48 -08:00
Rhet Turnbull
9a0fc0db3e Updated test library 2020-03-07 09:13:09 -08:00
Rhet Turnbull
549170fa36 test library updates 2020-02-09 09:30:15 -08:00
Rhet Turnbull
dede640ef3 test library updates 2020-02-08 07:37:31 -08:00
Rhet Turnbull
2b3491bdc4 Updated CHANGELOG.md 2020-02-08 07:34:51 -08:00
37 changed files with 973 additions and 93 deletions

View File

@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.22.12](https://github.com/RhetTbull/osxphotos/compare/0.22.10...v0.22.12)
> 7 March 2020
- Added exiftool [`8dea419`](https://github.com/RhetTbull/osxphotos/commit/8dea41961bad285be7058a68e5f7199e5cfb740e)
- Added --exiftool to CLI export [`ef79961`](https://github.com/RhetTbull/osxphotos/commit/ef799610aea67b703a7d056b7eee227534ba78a5)
- Updated test library [`9a0fc0d`](https://github.com/RhetTbull/osxphotos/commit/9a0fc0db3e79359610fd0f124a97b03fcf97d8a7)
#### [0.22.10](https://github.com/RhetTbull/osxphotos/compare/v0.22.9...0.22.10)
> 8 February 2020
- Fixed bug in --download-missing to fix issue #64 [`c654e3d`](https://github.com/RhetTbull/osxphotos/commit/c654e3dc61283382b37b6892dab1516ec517143a)
- removed commented out code [`69addc3`](https://github.com/RhetTbull/osxphotos/commit/69addc34649f992c6a4a0e0e334754a72530f0ba)
- Updated CHANGELOG.md [`1e013b6`](https://github.com/RhetTbull/osxphotos/commit/1e013b6802e49e26ec5a94eb702e841b2eb68395)
#### [v0.22.9](https://github.com/RhetTbull/osxphotos/compare/v0.22.7...v0.22.9)
> 1 February 2020

View File

@@ -175,6 +175,11 @@ Options:
require internet connection. This obviously
only works if the Photos library is synched
to iCloud.
--exiftool Use exiftool to write metadata directly to
exported photos. To use this option,
exiftool must be installed and in the path.
exiftool may be installed from
https://exiftool.org/
-h, --help Show this message and exit.
```
@@ -652,10 +657,29 @@ Returns the path to the live video component of a [live photo](#live_photo). If
**Note**: will also return None if the live video component is missing on disk. It's possible that the original photo may be on disk ([ismissing](#ismissing)==False) but the video component is missing, likely because it has not been downloaded from iCloud.
#### `portrait`
Returns True if photo was taken in iPhone portrait mode, otherwise False.
#### `hdr`
Returns True if photo was taken in High Dynamic Range (HDR) mode, otherwise False.
#### `selfie`
Returns True if photo is a selfie (taken with front-facing camera), otherwise False.
**Note**: Only implemented for Photos version 3.0+. On Photos version < 3.0, returns None.
#### `time_lapse`
Returns True if photo is a time lapse video, otherwise False.
#### `panorama`
Returns True if photo is a panorama, otherwise False.
**Note**: The result of `PhotoInfo.panorama` will differ from the "Panoramas" Media Types smart album in that it will also identify panorama photos from older phones that Photos does not recognize as panoramas.
#### `json()`
Returns a JSON representation of all photo info
#### `export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120,)`
#### `export(dest, *filename, edited=False, live_photo=False, overwrite=False, increment=True, sidecar_json=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False)`
Export photo from the Photos library to another destination on disk.
- dest: must be valid destination path as str (or exception raised).
@@ -664,10 +688,11 @@ Export photo from the Photos library to another destination on disk.
- overwrite: boolean; if True (default=False), will overwrite files if they alreay exist
- live_photo: boolean; if True (default=False), will also export the associted .mov for live photos; exported live photo will be named filename.mov
- increment: boolean; if True (default=True), will increment file name until a non-existent name is found
- sidecar_json: (boolean, default = False); if True will also write a json sidecar with IPTC data in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name
- sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with IPTC data; sidecar filename will be dest/filename.xmp where filename is the stem of the photo name
- sidecar_json: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name
- sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with metadata; sidecar filename will be dest/filename.xmp where filename is the stem of the photo name
- use_photos_export: boolean; (default=False), if True will attempt to export photo via applescript interaction with Photos; useful for forcing download of missing photos. This only works if the Photos library being used is the default library (last opened by Photos) as applescript will directly interact with whichever library Photos is currently using.
- timeout: (int, default=120) timeout in seconds used with use_photos_export
- exiftool: (boolean, default = False) if True, will use [exiftool](https://exiftool.org/) to write metadata directly to the exported photo; exiftool must be installed and in the system path
The json sidecar file can be used by exiftool to apply the metadata from the json file to the image. For example:

View File

@@ -14,6 +14,7 @@ import osxphotos
from ._constants import _EXIF_TOOL_URL, _PHOTOS_5_VERSION
from ._version import __version__
from .utils import create_path_by_date, _copy_file
from .exiftool import get_exiftool_path
def get_photos_db(*db_options):
@@ -662,6 +663,13 @@ def query(
"the photo does not exist on disk. This will be slow and will require internet connection. "
"This obviously only works if the Photos library is synched to iCloud.",
)
@click.option(
"--exiftool",
is_flag=True,
help="Use exiftool to write metadata directly to exported photos. "
"To use this option, exiftool must be installed and in the path. "
"exiftool may be installed from https://exiftool.org/",
)
@DB_ARGUMENT
@click.argument("dest", nargs=1, type=click.Path(exists=True))
@click.pass_obj
@@ -707,6 +715,7 @@ def export(
not_live,
download_missing,
dest,
exiftool,
):
""" Export photos from the Photos database.
Export path DEST is required.
@@ -733,6 +742,18 @@ def export(
click.echo(cli.commands["export"].get_help(ctx), err=True)
return
# verify exiftool installed an in path
if exiftool:
try:
_ = get_exiftool_path()
except FileNotFoundError:
click.echo(
"Could not find exiftool. Please download and install"
" from https://exiftool.org/",
err=True,
)
ctx.exit(2)
isphoto = ismovie = True # default searches for everything
if only_movies:
isphoto = False
@@ -810,6 +831,7 @@ def export(
original_name,
export_live,
download_missing,
exiftool,
)
else:
for p in photos:
@@ -824,6 +846,7 @@ def export(
original_name,
export_live,
download_missing,
exiftool,
)
if export_path:
click.echo(f"Exported {p.filename} to {export_path}")
@@ -1078,6 +1101,7 @@ def export_photo(
original_name,
export_live,
download_missing,
exiftool,
):
""" Helper function for export that does the actual export
photo: PhotoInfo object
@@ -1090,6 +1114,7 @@ def export_photo(
export_live: boolean; also export live video component if photo is a live photo
live video will have same name as photo but with .mov extension
download_missing: attempt download of missing iCloud photos
exiftool: use exiftool to write EXIF metadata directly to exported photo
returns destination path of exported photo or None if photo was missing
"""
@@ -1139,6 +1164,7 @@ def export_photo(
live_photo=export_live,
overwrite=overwrite,
use_photos_export=download_missing,
exiftool=exiftool,
)
# if export-edited, also export the edited version
@@ -1157,6 +1183,7 @@ def export_photo(
overwrite=overwrite,
edited=True,
use_photos_export=download_missing,
exiftool=exiftool,
)
else:
click.echo(f"Skipping missing edited photo for {filename}")

View File

@@ -13,6 +13,9 @@ import os.path
# TODO: Should this also use compatibleBackToVersion from LiGlobals?
_TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
# only version 3 - 4 have RKVersion.selfPortrait
_PHOTOS_3_VERSION = "3301"
# versions later than this have a different database structure
_PHOTOS_5_VERSION = "6000"

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.22.10"
__version__ = "0.22.13"

251
osxphotos/exiftool.py Normal file
View File

@@ -0,0 +1,251 @@
""" Yet another simple exiftool wrapper
I rolled my own for following reasons:
1. I wanted something under MIT license (best alternative was licensed under GPL/BSD)
2. I wanted singleton behavior so only a single exiftool process was ever running
If these aren't important to you, I highly recommend you use Sven Marnach's excellent
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
import json
import logging
import os
import subprocess
import sys
from functools import lru_cache
from .utils import _debug
# exiftool -stay_open commands outputs this EOF marker after command is run
EXIFTOOL_STAYOPEN_EOF = "{ready}"
EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
@lru_cache(maxsize=1)
def get_exiftool_path():
""" return path of exiftool, cache result """
result = subprocess.run(["which", "exiftool"], stdout=subprocess.PIPE)
exiftool_path = result.stdout.decode("utf-8")
if _debug():
logging.debug("exiftool path = %s" % (exiftool_path))
if exiftool_path:
return exiftool_path.rstrip()
else:
raise FileNotFoundError(
"Could not find exiftool. Please download and install from "
"https://exiftool.org/"
)
class _ExifToolProc:
""" Runs exiftool in a subprocess via Popen
Creates a singleton object """
def __new__(cls, *args, **kwargs):
""" create new object or return instance of already created singleton """
if not hasattr(cls, "instance") or not cls.instance:
cls.instance = super().__new__(cls)
return cls.instance
def __init__(self, exiftool=None):
""" construct _ExifToolProc singleton object or return instance of already created object
exiftool: optional path to exiftool binary (if not provided, will search path to find it) """
if hasattr(self, "_process_running") and self._process_running:
# already running
if exiftool is not None:
logging.warning(
f"exiftool subprocess already running, "
f"ignoring exiftool={exiftool}"
)
return
if exiftool:
self._exiftool = exiftool
else:
self._exiftool = get_exiftool_path()
self._process_running = False
self._start_proc()
@property
def process(self):
""" return the exiftool subprocess """
if self._process_running:
return self._process
else:
raise ValueError("exiftool process is not running")
@property
def pid(self):
""" return process id (PID) of the exiftool process """
return self._process.pid
@property
def exiftool(self):
""" return path to exiftool process """
return self._exiftool
def _start_proc(self):
""" start exiftool in batch mode """
if self._process_running:
logging.warning("exiftool already running: {self._process}")
return
# open exiftool process
self._process = subprocess.Popen(
[
self._exiftool,
"-stay_open", # keep process open in batch mode
"True", # -stay_open=True, keep process open in batch mode
"-@", # read command-line arguments from file
"-", # read from stdin
"-common_args", # specifies args common to all commands subsequently run
"-n", # no print conversion (e.g. print tag values in machine readable format)
"-G", # print group name for each tag
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
self._process_running = True
def _stop_proc(self):
""" stop the exiftool process if it's running, otherwise, do nothing """
if not self._process_running:
logging.warning("exiftool process is not running")
return
self._process.stdin.write(b"-stay_open\n")
self._process.stdin.write(b"False\n")
self._process.stdin.flush()
try:
self._process.communicate(timeout=5)
except subprocess.TimeoutExpired:
logging.warning(
f"exiftool pid {self._process.pid} did not exit, killing it"
)
self._process.kill()
self._process.communicate()
del self._process
self._process_running = False
def __del__(self):
self._stop_proc()
class ExifTool:
""" Basic exiftool interface for reading and writing EXIF tags """
def __init__(self, filepath, exiftool=None, overwrite=True):
""" Return ExifTool object
file: path to image file
exiftool: path to exiftool, if not specified will look in path
overwrite: if True, will overwrite image file without creating backup, default=False """
self.file = filepath
self.overwrite = overwrite
self.data = {}
self._exiftoolproc = _ExifToolProc(exiftool=exiftool)
self._process = self._exiftoolproc.process
self._read_exif()
def setvalue(self, tag, value):
""" Set tag to value(s)
if value is None, will delete tag """
if value is None:
value = ""
command = []
command.append(f"-{tag}={value}")
if self.overwrite:
command.append("-overwrite_original")
self.run_commands(*command)
def addvalues(self, tag, *values):
""" Add one or more value(s) to tag
If more than one value is passed, each value will be added to the tag
Notes: exiftool may add duplicate values for some tags so the caller must ensure
the values being added are not already in the EXIF data
For some tags, such as IPTC:Keywords, this will add a new value to the list of keywords,
but for others, such as EXIF:ISO, this will literally add a value to the existing value.
It's up to the caller to know what exiftool will do for each tag
If setvalue called before addvalues, exiftool does not appear to add duplicates,
but if addvalues called without first calling setvalue, exiftool will add duplicate values
"""
if not values:
raise ValueError("Must pass at least one value")
command = []
for value in values:
if value is None:
raise ValueError("Can't add None value to tag")
command.append(f"-{tag}+={value}")
if self.overwrite:
command.append("-overwrite_original")
if command:
self.run_commands(*command)
def run_commands(self, *commands, no_file=False):
""" run commands in the exiftool process and return result
no_file: (bool) do not pass the filename to exiftool (default=False)
by default, all commands will be run against self.file
use no_file=True to run a command without passing the filename """
if not hasattr(self, "_process") or not self._process:
raise ValueError("exiftool process is not running")
if not commands:
raise TypeError("must provide one or more command to run")
filename = os.fsencode(self.file) if not no_file else b""
command_str = (
b"\n".join([c.encode("utf-8") for c in commands])
+ b"\n"
+ filename
+ b"\n"
+ b"-execute\n"
)
if _debug():
logging.debug(command_str)
# send the command
self._process.stdin.write(command_str)
self._process.stdin.flush()
# read the output
output = b""
while EXIFTOOL_STAYOPEN_EOF not in str(output):
output += self._process.stdout.readline().strip()
return output[:-EXIFTOOL_STAYOPEN_EOF_LEN]
@property
def pid(self):
""" return process id (PID) of the exiftool process """
return self._process.pid
@property
def version(self):
""" returns exiftool version """
ver = self.run_commands("-ver", no_file=True)
return ver.decode("utf-8")
def json(self):
""" return JSON dictionary from exiftool as dict """
json_str = self.run_commands("-json")
if json_str:
return json.loads(json_str)
else:
return None
def _read_exif(self):
""" read exif data from file """
json = self.json()
self.data = {k: v for k, v in json[0].items()}
def __str__(self):
str_ = f"file: {self.file}\nexiftool: {self._exiftoolproc._exiftool}"
return str_

View File

@@ -32,6 +32,7 @@ from .utils import (
_get_resource_loc,
dd_to_dms_str,
)
from .exiftool import ExifTool
class PhotoInfo:
@@ -453,6 +454,41 @@ class PhotoInfo:
return photopath
@property
def panorama(self):
""" Returns True if photo is a panorama, otherwise False """
return self._info["panorama"]
@property
def slow_mo(self):
""" Returns True if photo is a slow motion video, otherwise False """
return self._info["slow_mo"]
@property
def time_lapse(self):
""" Returns True if photo is a time lapse video, otherwise False """
return self._info["time_lapse"]
@property
def hdr(self):
""" Returns True if photo is an HDR photo, otherwise False """
return self._info["hdr"]
@property
def screenshot(self):
""" Returns True if photo is an HDR photo, otherwise False """
return self._info["screenshot"]
@property
def portrait(self):
""" Returns True if photo is a portrait, otherwise False """
return self._info["portrait"]
@property
def selfie(self):
""" Returns True if photo is a selfie (front facing camera), otherwise False """
return self._info["selfie"]
def export(
self,
dest,
@@ -465,6 +501,7 @@ class PhotoInfo:
sidecar_xmp=False,
use_photos_export=False,
timeout=120,
exiftool=False,
):
""" export photo
dest: must be valid destination path (or exception raised)
@@ -481,11 +518,15 @@ class PhotoInfo:
sidecar filename will be dest/filename.xmp
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
timeout: (int, default=120) timeout in seconds used with use_photos_export
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
returns the full path to the exported file """
# TODO: add this docs:
# ( for jpeg in *.jpeg; do exiftool -v -json=$jpeg.json $jpeg; done )
# list of all files exported during this call to export
exported_files = []
# check arguments and get destination path and filename (if provided)
if filename and len(filename) > 2:
raise TypeError(
@@ -586,6 +627,7 @@ class PhotoInfo:
# copy the file, _copy_file uses ditto to preserve Mac extended attributes
_copy_file(src, dest)
exported_files.append(str(dest))
# copy live photo associated .mov if requested
if live_photo and self.live_photo:
@@ -597,6 +639,7 @@ class PhotoInfo:
f"Exporting live photo video of {filename} as {live_name.name}"
)
_copy_file(src_live, str(live_name))
exported_files.append(str(live_name))
else:
logging.warning(f"Skipping missing live movie for {filename}")
else:
@@ -634,7 +677,9 @@ class PhotoInfo:
timeout=timeout,
)
if exported is None:
if exported is not None:
exported_files.extend(exported)
else:
logging.warning(f"Error exporting photo {self.uuid} to {dest}")
if sidecar_json:
@@ -657,8 +702,32 @@ class PhotoInfo:
logging.warning(f"Error writing xmp sidecar to {sidecar_filename}")
raise e
logging.debug(f"export exported_files: {exported_files}")
# if exiftool, write the metadata
if exiftool and exported_files:
for exported_file in exported_files:
self._write_exif_data(exported_file)
return str(dest)
def _write_exif_data(self, filepath):
""" write exif data to image file at filepath
filepath: full path to the image file """
if not os.path.exists(filepath):
raise FileNotFoundError(f"Could not find file {filepath}")
exiftool = ExifTool(filepath)
exif_info = json.loads(self._exiftool_json_sidecar())[0]
for exiftag, val in exif_info.items():
if type(val) == list:
# more than one, set first value the add additional values
exiftool.setvalue(exiftag, val.pop(0))
if val:
# add any remaining items
exiftool.addvalues(exiftag, *val)
else:
exiftool.setvalue(exiftag, val)
def _exiftool_json_sidecar(self):
""" return json string of EXIF details in exiftool sidecar format
Does not include all the EXIF fields as those are likely already in the image
@@ -680,27 +749,27 @@ class PhotoInfo:
exif = {}
exif["_CreatedBy"] = "osxphotos, https://github.com/RhetTbull/osxphotos"
exif["FileName"] = self.filename
exif["File:FileName"] = self.filename
if self.description:
exif["ImageDescription"] = self.description
exif["Description"] = self.description
exif["EXIF:ImageDescription"] = self.description
exif["XMP:Description"] = self.description
if self.title:
exif["Title"] = self.title
exif["XMP:Title"] = self.title
if self.keywords:
exif["TagsList"] = exif["Keywords"] = list(self.keywords)
exif["XMP:TagsList"] = exif["IPTC:Keywords"] = list(self.keywords)
# Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
exif["Subject"] = list(self.keywords)
exif["XMP:Subject"] = list(self.keywords)
if self.persons:
exif["PersonInImage"] = self.persons
exif["XMP:PersonInImage"] = self.persons
# Photos puts both keywords and persons in Subject when using "Export IPTC as XMP"
if "Subject" in exif:
exif["Subject"].extend(self.persons)
if "XMP:Subject" in exif:
exif["XMP:Subject"].extend(self.persons)
else:
exif["Subject"] = self.persons
exif["XMP:Subject"] = self.persons
# if self.favorite():
# exif["Rating"] = 5
@@ -708,13 +777,13 @@ class PhotoInfo:
(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
exif["GPSLongitude"] = lon_str
exif["GPSPosition"] = f"{lat_str}, {lon_str}"
exif["EXIF:GPSLatitude"] = lat_str
exif["EXIF:GPSLongitude"] = lon_str
exif["Composite:GPSPosition"] = f"{lat_str}, {lon_str}"
lat_ref = "North" if lat >= 0 else "South"
lon_ref = "East" if lon >= 0 else "West"
exif["GPSLatitudeRef"] = lat_ref
exif["GPSLongitudeRef"] = lon_ref
exif["EXIF:GPSLatitudeRef"] = lat_ref
exif["EXIF:GPSLongitudeRef"] = lon_ref
# process date/time and timezone offset
date = self.date
@@ -725,11 +794,11 @@ class PhotoInfo:
offset = re.findall(r"([+-]?)([\d]{2})([\d]{2})", offsettime)
offset = offset[0] # findall returns list of tuples
offsettime = f"{offset[0]}{offset[1]}:{offset[2]}"
exif["DateTimeOriginal"] = datetimeoriginal
exif["OffsetTimeOriginal"] = offsettime
exif["EXIF:DateTimeOriginal"] = datetimeoriginal
exif["EXIF:OffsetTimeOriginal"] = offsettime
if self.date_modified is not None:
exif["ModifyDate"] = self.date_modified.strftime("%Y:%m:%d %H:%M:%S")
exif["EXIF:ModifyDate"] = self.date_modified.strftime("%Y:%m:%d %H:%M:%S")
json_str = json.dumps([exif])
return json_str

View File

@@ -18,6 +18,7 @@ from shutil import copyfile
from ._constants import (
_MOVIE_TYPE,
_PHOTO_TYPE,
_PHOTOS_3_VERSION,
_PHOTOS_5_VERSION,
_TESTED_DB_VERSIONS,
_TESTED_OS_VERSIONS,
@@ -39,6 +40,7 @@ from .utils import (
# Or fix the help text to match behavior
# TODO: Add test for __str__
# TODO: Add special albums and magic albums
# TODO: fix "if X not in y" dictionary checks to use try/except EAFP style
class PhotosDB:
@@ -521,21 +523,36 @@ class PhotosDB:
self._dbvolumes[vol[0]] = vol[1]
# Get photo details
c.execute(
""" SELECT RKVersion.uuid, RKVersion.modelId, RKVersion.masterUuid, RKVersion.filename,
RKVersion.lastmodifieddate, RKVersion.imageDate, RKVersion.mainRating,
RKVersion.hasAdjustments, RKVersion.hasKeywords, RKVersion.imageTimeZoneOffsetSeconds,
RKMaster.volumeId, RKMaster.imagePath, RKVersion.extendedDescription, RKVersion.name,
RKMaster.isMissing, RKMaster.originalFileName, RKVersion.isFavorite, RKVersion.isHidden,
RKVersion.latitude, RKVersion.longitude,
RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI,
RKVersion.burstUuid, RKVersion.burstPickType,
RKVersion.specialType, RKMaster.modelID
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
)
# TODO: RKVersion.selfPortrait -- only in Photos 3 and up
if self._db_version < _PHOTOS_3_VERSION:
# Photos < 3.0 doesn't have RKVersion.selfPortrait (selfie)
c.execute(
""" SELECT RKVersion.uuid, RKVersion.modelId, RKVersion.masterUuid, RKVersion.filename,
RKVersion.lastmodifieddate, RKVersion.imageDate, RKVersion.mainRating,
RKVersion.hasAdjustments, RKVersion.hasKeywords, RKVersion.imageTimeZoneOffsetSeconds,
RKMaster.volumeId, RKMaster.imagePath, RKVersion.extendedDescription, RKVersion.name,
RKMaster.isMissing, RKMaster.originalFileName, RKVersion.isFavorite, RKVersion.isHidden,
RKVersion.latitude, RKVersion.longitude,
RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI,
RKVersion.burstUuid, RKVersion.burstPickType,
RKVersion.specialType, RKMaster.modelID
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
)
else:
c.execute(
""" SELECT RKVersion.uuid, RKVersion.modelId, RKVersion.masterUuid, RKVersion.filename,
RKVersion.lastmodifieddate, RKVersion.imageDate, RKVersion.mainRating,
RKVersion.hasAdjustments, RKVersion.hasKeywords, RKVersion.imageTimeZoneOffsetSeconds,
RKMaster.volumeId, RKMaster.imagePath, RKVersion.extendedDescription, RKVersion.name,
RKMaster.isMissing, RKMaster.originalFileName, RKVersion.isFavorite, RKVersion.isHidden,
RKVersion.latitude, RKVersion.longitude,
RKVersion.adjustmentUuid, RKVersion.type, RKMaster.UTI,
RKVersion.burstUuid, RKVersion.burstPickType,
RKVersion.specialType, RKMaster.modelID,
RKVersion.selfPortrait
FROM RKVersion, RKMaster WHERE RKVersion.isInTrash = 0 AND
RKVersion.masterUuid = RKMaster.uuid AND RKVersion.filename NOT LIKE '%.pdf' """
)
# order of results
# 0 RKVersion.uuid
@@ -565,8 +582,7 @@ class PhotosDB:
# 24 RKVersion.burstPickType
# 25 RKVersion.specialType
# 26 RKMaster.modelID
# 27 RKVersion.selfPortrait -- 1 if selfie (not yet implemented)
# 27 RKVersion.selfPortrait -- 1 if selfie, Photos >= 3, not present for Photos < 3
for row in c:
uuid = row[0]
@@ -670,9 +686,11 @@ class PhotosDB:
self._dbphotos[uuid]["screenshot"] = True if row[25] == 6 else False
self._dbphotos[uuid]["portrait"] = True if row[25] == 9 else False
# TODO: Handle selfies (front facing camera, RKVersion.selfPortrait == 1)
# self._dbphotos[uuid]["selfie"] = True if row[27] == 1 else False
self._dbphotos[uuid]["selfie"] = None
# selfies (front facing camera, RKVersion.selfPortrait == 1)
if self._db_version >= _PHOTOS_3_VERSION:
self._dbphotos[uuid]["selfie"] = True if row[27] == 1 else False
else:
self._dbphotos[uuid]["selfie"] = None
# Init cloud details that will be filled in later if cloud asset
self._dbphotos[uuid]["cloudAssetGUID"] = None # Photos 5

View File

@@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>9575E48B-8D5F-5654-ABAC-4431B1167324</string>
<key>pid</key>
<integer>1309</integer>
<integer>1134</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2020-01-30T02:33:23Z</date>
<date>2020-03-07T16:11:53Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2020-01-30T02:33:23Z</date>
<date>2020-03-07T16:11:53Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2020-01-30T04:13:27Z</date>
<date>2020-03-07T18:15:22Z</date>
<key>BackgroundJobSearch</key>
<date>2020-01-30T02:33:24Z</date>
<date>2020-03-07T16:11:53Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2020-01-30T02:33:23Z</date>
<date>2020-03-07T16:11:52Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2020-01-30T02:33:24Z</date>
<date>2020-03-07T08:11:00Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-01-30T04:13:27Z</date>
<date>2020-03-07T18:15:23Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-01-30T02:33:23Z</date>
<date>2020-03-07T08:10:59Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2020-01-30T02:33:24Z</date>
<date>2020-03-07T18:15:22Z</date>
<key>SiriPortraitDonation</key>
<date>2020-01-30T02:33:24Z</date>
<date>2020-03-07T08:11:00Z</date>
</dict>
</plist>

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>FaceIDModelLastGenerationKey</key>
<date>2020-01-30T02:33:24Z</date>
<date>2020-03-07T08:11:01Z</date>
<key>LastContactClassificationKey</key>
<date>2020-01-30T02:33:26Z</date>
<date>2020-03-07T08:11:02Z</date>
</dict>
</plist>

178
tests/test_exiftool.py Normal file
View File

@@ -0,0 +1,178 @@
import pytest
from osxphotos.exiftool import get_exiftool_path
TEST_FILE_ONE_KEYWORD = "tests/test-images/wedding.jpg"
TEST_FILE_MULTI_KEYWORD = "tests/test-images/Tulips.jpg"
TEST_MULTI_KEYWORDS = [
"Top Shot",
"flowers",
"flower",
"design",
"Stock Photography",
"vibrant",
"plastic",
"Digital Nomad",
"close up",
"stock photo",
"outdoor",
"wedding",
"Reiseblogger",
"fake",
"colorful",
"Indoor",
"display",
"photography",
]
try:
exiftool = get_exiftool_path()
except:
exiftool = None
if exiftool is None:
pytest.skip("could not find exiftool in path", allow_module_level=True)
def test_get_exiftool_path():
import osxphotos.exiftool
exiftool = osxphotos.exiftool.get_exiftool_path()
assert exiftool is not None
def test_version():
import osxphotos.exiftool
exif = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
assert exif.version is not None
assert isinstance(exif.version, str)
def test_read():
import osxphotos.exiftool
exif = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
assert exif.data["File:MIMEType"] == "image/jpeg"
assert exif.data["EXIF:ISO"] == 160
assert exif.data["IPTC:Keywords"] == "wedding"
def test_setvalue_1():
# test setting a tag value
import os.path
import tempfile
from osxphotos.utils import _copy_file
import osxphotos.exiftool
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD))
_copy_file(TEST_FILE_ONE_KEYWORD, tempfile)
exif = osxphotos.exiftool.ExifTool(tempfile)
exif.setvalue("IPTC:Keywords", "test")
exif._read_exif()
assert exif.data["IPTC:Keywords"] == "test"
def test_clear_value():
# test clearing a tag value
import os.path
import tempfile
from osxphotos.utils import _copy_file
import osxphotos.exiftool
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD))
_copy_file(TEST_FILE_ONE_KEYWORD, tempfile)
exif = osxphotos.exiftool.ExifTool(tempfile)
assert "IPTC:Keywords" in exif.data
exif.setvalue("IPTC:Keywords", None)
exif._read_exif()
assert "IPTC:Keywords" not in exif.data
def test_addvalues_1():
# test setting a tag value
import os.path
import tempfile
from osxphotos.utils import _copy_file
import osxphotos.exiftool
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_ONE_KEYWORD))
_copy_file(TEST_FILE_ONE_KEYWORD, tempfile)
exif = osxphotos.exiftool.ExifTool(tempfile)
exif.addvalues("IPTC:Keywords", "test")
exif._read_exif()
assert sorted(exif.data["IPTC:Keywords"]) == sorted(["wedding", "test"])
def test_addvalues_2():
# test setting a tag value where multiple values already exist
import os.path
import tempfile
from osxphotos.utils import _copy_file
import osxphotos.exiftool
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
tempfile = os.path.join(tempdir.name, os.path.basename(TEST_FILE_MULTI_KEYWORD))
_copy_file(TEST_FILE_MULTI_KEYWORD, tempfile)
exif = osxphotos.exiftool.ExifTool(tempfile)
assert sorted(exif.data["IPTC:Keywords"]) == sorted(TEST_MULTI_KEYWORDS)
exif.addvalues("IPTC:Keywords", "test")
exif._read_exif()
assert "IPTC:Keywords" in exif.data
test_multi = TEST_MULTI_KEYWORDS.copy()
test_multi.append("test")
assert sorted(exif.data["IPTC:Keywords"]) == sorted(test_multi)
def test_singleton():
import osxphotos.exiftool
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
exif2 = osxphotos.exiftool.ExifTool(TEST_FILE_MULTI_KEYWORD)
assert exif1._process.pid == exif2._process.pid
def test_pid():
import osxphotos.exiftool
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
assert exif1.pid == exif1._process.pid
def test_exiftoolproc_process():
import osxphotos.exiftool
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
assert exif1._exiftoolproc.process is not None
def test_exiftoolproc_exiftool():
import osxphotos.exiftool
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
assert exif1._exiftoolproc.exiftool == osxphotos.exiftool.get_exiftool_path()
def test_json():
import osxphotos.exiftool
import json
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
json1 = exif1.json()
assert json1[0]["XMP:TagsList"] == "wedding"
def test_str():
import osxphotos.exiftool
exif1 = osxphotos.exiftool.ExifTool(TEST_FILE_ONE_KEYWORD)
assert "file: " in str(exif1)
assert "exiftool: " in str(exif1)

View File

@@ -446,29 +446,38 @@ def test_exiftool_json_sidecar():
json_expected = json.loads(
"""
[{"FileName": "DC99FBDD-7A52-4100-A5BB-344131646C30.jpeg",
"Title": "St. James\'s Park",
"TagsList": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"Keywords": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"GPSLatitude": "51 deg 30\' 12.86\\" N",
"GPSLongitude": "0 deg 7\' 54.50\\" W",
"GPSPosition": "51 deg 30\' 12.86\\" N, 0 deg 7\' 54.50\\" W",
"GPSLatitudeRef": "North", "GPSLongitudeRef": "West",
"DateTimeOriginal": "2018:10:13 09:18:12",
"OffsetTimeOriginal": "-04:00",
"ModifyDate": "2019:12:08 14:06:44"}] """
)
[{"File:FileName": "DC99FBDD-7A52-4100-A5BB-344131646C30.jpeg",
"XMP:Title": "St. James\'s Park",
"XMP:TagsList": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"IPTC:Keywords": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"XMP:Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"EXIF:GPSLatitude": "51 deg 30\' 12.86\\" N",
"EXIF:GPSLongitude": "0 deg 7\' 54.50\\" W",
"Composite:GPSPosition": "51 deg 30\' 12.86\\" N, 0 deg 7\' 54.50\\" W",
"EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West",
"EXIF:DateTimeOriginal": "2018:10:13 09:18:12",
"EXIF:OffsetTimeOriginal": "-04:00",
"EXIF:ModifyDate": "2019:12:08 14:06:44",
"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos"
}] """
)[0]
json_got = photos[0]._exiftool_json_sidecar()
json_got = json.loads(json_got)
json_got = json.loads(json_got)[0]
# some gymnastics to account for different sort order in different pythons
for item in zip(sorted(json_got[0].items()), sorted(json_expected[0].items())):
if type(item[0][1]) in (list, tuple):
assert sorted(item[0][1]) == sorted(item[1][1])
# some gymnastics to account for different sort order in different pythons
for k, v in json_got.items():
if type(v) in (list, tuple):
assert sorted(json_expected[k]) == sorted(v)
else:
assert item[0][1] == item[1][1]
assert json_expected[k] == v
for k, v in json_expected.items():
if type(v) in (list, tuple):
assert sorted(json_got[k]) == sorted(v)
else:
assert json_got[k] == v
def test_xmp_sidecar():

View File

@@ -390,30 +390,37 @@ def test_exiftool_json_sidecar():
json_expected = json.loads(
"""
[{"FileName": "St James Park.jpg",
"Title": "St. James\'s Park",
"TagsList": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"Keywords": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"GPSLatitude": "51 deg 30\' 12.86\\" N",
"GPSLongitude": "0 deg 7\' 54.50\\" W",
"GPSPosition": "51 deg 30\' 12.86\\" N, 0 deg 7\' 54.50\\" W",
"GPSLatitudeRef": "North", "GPSLongitudeRef": "West",
"DateTimeOriginal": "2018:10:13 09:18:12",
"OffsetTimeOriginal": "-04:00",
"ModifyDate": "2019:12:01 11:43:45"}]
"""
)
[{"File:FileName": "St James Park.jpg",
"XMP:Title": "St. James\'s Park",
"XMP:TagsList": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"IPTC:Keywords": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"XMP:Subject": ["London 2018", "St. James\'s Park", "England", "United Kingdom", "UK", "London"],
"EXIF:GPSLatitude": "51 deg 30\' 12.86\\" N",
"EXIF:GPSLongitude": "0 deg 7\' 54.50\\" W",
"Composite:GPSPosition": "51 deg 30\' 12.86\\" N, 0 deg 7\' 54.50\\" W",
"EXIF:GPSLatitudeRef": "North", "EXIF:GPSLongitudeRef": "West",
"EXIF:DateTimeOriginal": "2018:10:13 09:18:12",
"EXIF:OffsetTimeOriginal": "-04:00",
"EXIF:ModifyDate": "2019:12:01 11:43:45",
"_CreatedBy": "osxphotos, https://github.com/RhetTbull/osxphotos"
}] """
)[0]
json_got = photos[0]._exiftool_json_sidecar()
json_got = json.loads(json_got)
json_got = json.loads(json_got)[0]
# some gymnastics to account for different sort order in different pythons
for item in zip(sorted(json_got[0].items()), sorted(json_expected[0].items())):
if type(item[0][1]) in (list, tuple):
assert sorted(item[0][1]) == sorted(item[1][1])
for k, v in json_got.items():
if type(v) in (list, tuple):
assert sorted(json_expected[k]) == sorted(v)
else:
assert item[0][1] == item[1][1]
assert json_expected[k] == v
for k, v in json_expected.items():
if type(v) in (list, tuple):
assert sorted(json_got[k]) == sorted(v)
else:
assert json_got[k] == v
def test_xmp_sidecar():

View File

@@ -0,0 +1,94 @@
# Test cloud photos
import pytest
PHOTOS_DB_CLOUD = "./tests/Test-Cloud-10.15.1.photoslibrary/database/photos.db"
UUID_DICT = {
"portrait": "7CDA5F84-AA16-4D28-9AA6-A49E1DF8A332",
"hdr": "D11D25FF-5F31-47D2-ABA9-58418878DC15",
"selfie": "080525C4-1F05-48E5-A3F4-0C53127BB39C",
"time_lapse": "4614086E-C797-4876-B3B9-3057E8D757C9",
"panorama": "1C1C8F1F-826B-4A24-B1CB-56628946A834",
"no_specials": "C2BBC7A4-5333-46EE-BAF0-093E72111B39",
}
def test_portrait():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["portrait"]])
assert photos[0].portrait
assert not photos[0].hdr
assert not photos[0].selfie
assert not photos[0].time_lapse
assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].portrait
def test_hdr():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["hdr"]])
assert photos[0].hdr
assert not photos[0].portrait
assert not photos[0].selfie
assert not photos[0].time_lapse
assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].hdr
def test_selfie():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["selfie"]])
assert photos[0].selfie
assert not photos[0].portrait
assert not photos[0].hdr
assert not photos[0].time_lapse
assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].selfie
def test_time_lapse():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["time_lapse"]], movies=True)
assert photos[0].time_lapse
assert not photos[0].portrait
assert not photos[0].hdr
assert not photos[0].selfie
assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].time_lapse
def test_panorama():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["panorama"]])
assert photos[0].panorama
assert not photos[0].portrait
assert not photos[0].selfie
assert not photos[0].time_lapse
assert not photos[0].hdr
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].panorama

View File

@@ -0,0 +1,96 @@
# Test cloud photos
import pytest
PHOTOS_DB_CLOUD = "./tests/Test-Cloud-10.14.6.photoslibrary/database/photos.db"
UUID_DICT = {
# "portrait": "7CDA5F84-AA16-4D28-9AA6-A49E1DF8A332",
"hdr": "UIgouj2cQqyKJnB2bCHrSg",
"selfie": "NsO5Yg8qSPGBGiVxsCd5Kw",
"time_lapse": "pKAWFwtlQYuR962KEaonPA",
# "panorama": "1C1C8F1F-826B-4A24-B1CB-56628946A834",
"no_specials": "%PgMNP%xRTWTJF+oOyZbXQ",
}
@pytest.mark.skip(reason="don't have portrait photo in the 10.14.6yy database")
def test_portrait():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["portrait"]])
assert photos[0].portrait
assert not photos[0].hdr
assert not photos[0].selfie
assert not photos[0].time_lapse
assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].portrait
def test_hdr():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["hdr"]])
assert photos[0].hdr
assert not photos[0].portrait
assert not photos[0].selfie
assert not photos[0].time_lapse
assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].hdr
def test_selfie():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["selfie"]])
assert photos[0].selfie
assert not photos[0].portrait
assert not photos[0].hdr
assert not photos[0].time_lapse
assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].selfie
def test_time_lapse():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["time_lapse"]], movies=True)
assert photos[0].time_lapse
assert not photos[0].portrait
assert not photos[0].hdr
assert not photos[0].selfie
assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].time_lapse
@pytest.mark.skip(reason="no panorama in 10.14.6 database")
def test_panorama():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB_CLOUD)
photos = photosdb.photos(uuid=[UUID_DICT["panorama"]])
assert photos[0].panorama
assert not photos[0].portrait
assert not photos[0].selfie
assert not photos[0].time_lapse
assert not photos[0].hdr
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].panorama

View File

@@ -0,0 +1,87 @@
# Test cloud photos
import pytest
PHOTOS_DB = "./tests/Test-10.12.6.photoslibrary/database/photos.db"
UUID_DICT = {"no_specials": "Pj99JmYjQkeezdY2OFuSaw"}
def test_portrait():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
# photos = photosdb.photos(uuid=[UUID_DICT["portrait"]])
# assert photos[0].portrait
# assert not photos[0].hdr
# assert not photos[0].selfie
# assert not photos[0].time_lapse
# assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].portrait
def test_hdr():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
# photos = photosdb.photos(uuid=[UUID_DICT["hdr"]])
# assert photos[0].hdr
# assert not photos[0].portrait
# assert not photos[0].selfie
# assert not photos[0].time_lapse
# assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].hdr
def test_selfie():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
# photos = photosdb.photos(uuid=[UUID_DICT["selfie"]])
# assert photos[0].selfie
# assert not photos[0].portrait
# assert not photos[0].hdr
# assert not photos[0].time_lapse
# assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert photos[0].selfie is None
def test_time_lapse():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
# photos = photosdb.photos(uuid=[UUID_DICT["time_lapse"]], movies=True)
# assert photos[0].time_lapse
# assert not photos[0].portrait
# assert not photos[0].hdr
# assert not photos[0].selfie
# assert not photos[0].panorama
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].time_lapse
def test_panorama():
import osxphotos
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
# photos = photosdb.photos(uuid=[UUID_DICT["panorama"]])
# assert photos[0].panorama
# assert not photos[0].portrait
# assert not photos[0].selfie
# assert not photos[0].time_lapse
# assert not photos[0].hdr
photos = photosdb.photos(uuid=[UUID_DICT["no_specials"]])
assert not photos[0].panorama