Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdc4b23f42 | ||
|
|
3cdfc8700d | ||
|
|
2f866256ad | ||
|
|
86018d5cc0 | ||
|
|
d657fc6ccd | ||
|
|
875f79b92d | ||
|
|
3a110bb6d3 | ||
|
|
e73327c164 | ||
|
|
3799594473 | ||
|
|
db430173b5 | ||
|
|
defe5cb61a | ||
|
|
f58f8dd804 | ||
|
|
2773ff7381 | ||
|
|
348ef54b30 | ||
|
|
9c18cee37e | ||
|
|
651ed50a07 | ||
|
|
248c95237c | ||
|
|
ddce731a5d |
@@ -156,6 +156,16 @@
|
||||
"bug",
|
||||
"userTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "martinhrpi",
|
||||
"name": "Martin",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/19407684?v=4",
|
||||
"profile": "https://github.com/martinhrpi",
|
||||
"contributions": [
|
||||
"research",
|
||||
"userTesting"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -4,6 +4,55 @@ 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.39.24](https://github.com/RhetTbull/osxphotos/compare/v0.39.23...v0.39.24)
|
||||
|
||||
> 19 January 2021
|
||||
|
||||
- Fixed face regions for exif orientation 6, 8 [`86018d5`](https://github.com/RhetTbull/osxphotos/commit/86018d5cc0d964760fd64047ce52f1f54fc28dc0)
|
||||
- version bump [`2f86625`](https://github.com/RhetTbull/osxphotos/commit/2f866256adfdf39244241ca6bbcc7a8d072555b9)
|
||||
|
||||
#### [v0.39.23](https://github.com/RhetTbull/osxphotos/compare/v0.39.22...v0.39.23)
|
||||
|
||||
> 18 January 2021
|
||||
|
||||
- Fixed face region orientation [`875f79b`](https://github.com/RhetTbull/osxphotos/commit/875f79b92d9510e59fe8ca0aa21a42abc7600f70)
|
||||
- Updated documentation for new face region properties [`3a110bb`](https://github.com/RhetTbull/osxphotos/commit/3a110bb6d3d23d1c9fd8612b4201144046fed567)
|
||||
|
||||
#### [v0.39.22](https://github.com/RhetTbull/osxphotos/compare/v0.39.21...v0.39.22)
|
||||
|
||||
> 18 January 2021
|
||||
|
||||
- Beta fix for Digikam reading XMP [`3799594`](https://github.com/RhetTbull/osxphotos/commit/379959447373f951ffca372598ea8f1d5834fe52)
|
||||
- Add @martinhrpi as a contributor [`db43017`](https://github.com/RhetTbull/osxphotos/commit/db430173b59732f944ca52b53c928370684580df)
|
||||
|
||||
#### [v0.39.21](https://github.com/RhetTbull/osxphotos/compare/v0.39.20...v0.39.21)
|
||||
|
||||
> 18 January 2021
|
||||
|
||||
- Added beta support for face regions in xmp [`2773ff7`](https://github.com/RhetTbull/osxphotos/commit/2773ff73815ef4667f88a45b016539e490d31769)
|
||||
- Fixed osxphotos.spec datas [`f58f8dd`](https://github.com/RhetTbull/osxphotos/commit/f58f8dd804f432d07048b98e5dcedca57fec0a5e)
|
||||
|
||||
#### [v0.39.20](https://github.com/RhetTbull/osxphotos/compare/v0.39.19...v0.39.20)
|
||||
|
||||
> 16 January 2021
|
||||
|
||||
- Added isreference property and --is-reference, #321 [`651ed50`](https://github.com/RhetTbull/osxphotos/commit/651ed50a076bd3685c7d7a568e53960363d5c30b)
|
||||
- version bump [`9c18cee`](https://github.com/RhetTbull/osxphotos/commit/9c18cee37e961d2e1059490ad1dbe4e45c501002)
|
||||
|
||||
#### [v0.39.19](https://github.com/RhetTbull/osxphotos/compare/v0.39.18...v0.39.19)
|
||||
|
||||
> 15 January 2021
|
||||
|
||||
- Added retry to use_photos_export, issue #351 [`ddce731`](https://github.com/RhetTbull/osxphotos/commit/ddce731a5d354e833d56a64d06cdbc39711f693e)
|
||||
|
||||
#### [v0.39.18](https://github.com/RhetTbull/osxphotos/compare/v0.39.17...v0.39.18)
|
||||
|
||||
> 15 January 2021
|
||||
|
||||
- Fixed XMP sidecars to conform with exiftool format, #349, #350 [`1fd0fe5`](https://github.com/RhetTbull/osxphotos/commit/1fd0fe5ea477ccea43c78086af440bd32dc702d8)
|
||||
- Added update_readme.py to auto-build README [`fd5976b`](https://github.com/RhetTbull/osxphotos/commit/fd5976b75c79a3d205db2e8132c388de95632b77)
|
||||
- Added modified.strftime template, refactored test_template.py [`088476c`](https://github.com/RhetTbull/osxphotos/commit/088476c59126c6d6fe75551ff122e81aababf818)
|
||||
|
||||
#### [v0.39.17](https://github.com/RhetTbull/osxphotos/compare/v0.39.16...v0.39.17)
|
||||
|
||||
> 12 January 2021
|
||||
|
||||
25
README.md
25
README.md
@@ -3,7 +3,7 @@
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors)
|
||||
[](#contributors)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||
@@ -269,6 +269,9 @@ Options:
|
||||
--no-comment Search for photos with no comments.
|
||||
--has-likes Search for photos that have likes.
|
||||
--no-likes Search for photos with no likes.
|
||||
--is-reference Search for photos that were imported as
|
||||
referenced files (not copied into Photos
|
||||
library).
|
||||
--missing Export only photos missing from the Photos
|
||||
library; must be used with --download-missing.
|
||||
--deleted Include photos from the 'Recently Deleted'
|
||||
@@ -1601,6 +1604,9 @@ Returns list of [LikeInfo](#likeinfo) objects for likes on shared photos or empt
|
||||
|
||||
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns empty list.
|
||||
|
||||
#### `isreference`
|
||||
Returns `True` if the original image file is a referenced file (imported without copying to the Photos library) otherwise returns `False`.
|
||||
|
||||
#### `isphoto`
|
||||
Returns True if type is photo/still image, otherwise False
|
||||
|
||||
@@ -2262,6 +2268,22 @@ UUID of the photo this face is associated with.
|
||||
#### `photo`
|
||||
[PhotoInfo](#photoinfo) object representing the photo that contains this face.
|
||||
|
||||
#### `mwg_rs_area`
|
||||
Returns named tuple with following coordinates as used in Metdata Working Group (mwg) face regions in XMP files.
|
||||
|
||||
* `x` = `stArea:x`
|
||||
* `y` = `stArea:y`
|
||||
* `h` = `stArea:h`
|
||||
* `w` = `stArea:w`
|
||||
|
||||
#### `mpri_reg_rect`
|
||||
Returnes named tuple with following coordinates as used in Microsoft Photo Region Rectangle (mpri) in XMP files.
|
||||
|
||||
* `x` = x coordinate of top left corner of rectangle
|
||||
* `y` = y coordinate of top left corner of rectangle
|
||||
* `h` = height of rectangle
|
||||
* `w` = width of rectangle
|
||||
|
||||
#### `face_rect()`
|
||||
Returns list of x, y coordinates as tuples `[(x0, y0), (x1, y1)]` representing the corners of rectangular region that contains the face. Coordinates are in same format and [reference frame](https://pillow.readthedocs.io/en/stable/handbook/concepts.html#coordinate-system) as used by [Pillow](https://pypi.org/project/Pillow/) imaging library. **Note**: face_rect() and all other properties/methods that return coordinates refer to the *current version* of the image. E.g. if the image has been edited ([`PhotoInfo.hasadjustments`](#hasadjustments)), these refer to [`PhotoInfo.path_edited`](#pathedited). If the image has no adjustments, these coordinates refer to the original photo ([`PhotoInfo.path`](#path)).
|
||||
|
||||
@@ -2589,6 +2611,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Rott-Apple"><img src="https://avatars1.githubusercontent.com/u/67875570?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Rott-Apple</b></sub></a><br /><a href="#research-Rott-Apple" title="Research">🔬</a></td>
|
||||
<td align="center"><a href="https://github.com/narensankar0529"><img src="https://avatars3.githubusercontent.com/u/74054766?v=4?s=75" width="75px;" alt=""/><br /><sub><b>narensankar0529</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anarensankar0529" title="Bug reports">🐛</a> <a href="#userTesting-narensankar0529" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/martinhrpi"><img src="https://avatars2.githubusercontent.com/u/19407684?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Martin</b></sub></a><br /><a href="#research-martinhrpi" title="Research">🔬</a> <a href="#userTesting-martinhrpi" title="User Testing">📓</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
3
cli.py
3
cli.py
@@ -3,8 +3,7 @@
|
||||
To build this into an executable:
|
||||
- install pyinstaller:
|
||||
python3 -m pip install pyinstaller
|
||||
- then use make_cli_exe.sh to run pyinstaller or execute the following command:
|
||||
pyinstaller --onefile --hidden-import="pkg_resources.py2_warn" --name osxphotos --add-data osxphotos/templates/xmp_sidecar.mako:osxphotos/templates cli.py
|
||||
- then use make_cli_exe.sh to run pyinstaller
|
||||
|
||||
Resulting executable will be in "dist/osxphotos"
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import importlib
|
||||
pathex = os.getcwd()
|
||||
|
||||
# include necessary data files
|
||||
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates')]
|
||||
datas=[('osxphotos/templates/xmp_sidecar.mako', 'osxphotos/templates'), ('osxphotos/templates/xmp_sidecar_beta.mako', 'osxphotos/templates')]
|
||||
package_imports = [['photoscript', ['photoscript.applescript']]]
|
||||
for package, files in package_imports:
|
||||
proot = os.path.dirname(importlib.import_module(package).__file__)
|
||||
|
||||
@@ -674,6 +674,11 @@ def query_options(f):
|
||||
o("--no-comment", is_flag=True, help="Search for photos with no comments."),
|
||||
o("--has-likes", is_flag=True, help="Search for photos that have likes."),
|
||||
o("--no-likes", is_flag=True, help="Search for photos with no likes."),
|
||||
o(
|
||||
"--is-reference",
|
||||
is_flag=True,
|
||||
help="Search for photos that were imported as referenced files (not copied into Photos library).",
|
||||
),
|
||||
]
|
||||
for o in options[::-1]:
|
||||
f = o(f)
|
||||
@@ -1058,6 +1063,9 @@ def cli(ctx, db, json_, debug):
|
||||
help=("Save options to file for use with --load-config. File format is TOML."),
|
||||
type=click.Path(),
|
||||
)
|
||||
@click.option(
|
||||
"--beta", is_flag=True, default=False, hidden=True, help="Enable beta options."
|
||||
)
|
||||
@DB_ARGUMENT
|
||||
@click.argument("dest", nargs=1, type=click.Path(exists=True))
|
||||
@click.pass_obj
|
||||
@@ -1166,6 +1174,8 @@ def export(
|
||||
exportdb,
|
||||
load_config,
|
||||
save_config,
|
||||
is_reference,
|
||||
beta,
|
||||
):
|
||||
"""Export photos from the Photos database.
|
||||
Export path DEST is required.
|
||||
@@ -1304,6 +1314,7 @@ def export(
|
||||
report = cfg.report
|
||||
cleanup = cfg.cleanup
|
||||
exportdb = cfg.exportdb
|
||||
beta = cfg.beta
|
||||
|
||||
# config file might have changed verbose
|
||||
VERBOSE = bool(verbose)
|
||||
@@ -1531,6 +1542,10 @@ def export(
|
||||
)
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_, exiftool=exiftool_path)
|
||||
|
||||
# enable beta features if requested
|
||||
photosdb._beta = beta
|
||||
|
||||
photos = _query(
|
||||
photosdb=photosdb,
|
||||
keyword=keyword,
|
||||
@@ -1590,6 +1605,7 @@ def export(
|
||||
no_comment=no_comment,
|
||||
has_likes=has_likes,
|
||||
no_likes=no_likes,
|
||||
is_reference=is_reference,
|
||||
)
|
||||
|
||||
if photos:
|
||||
@@ -1865,6 +1881,7 @@ def query(
|
||||
no_comment,
|
||||
has_likes,
|
||||
no_likes,
|
||||
is_reference,
|
||||
):
|
||||
"""Query the Photos database using 1 or more search options;
|
||||
if more than one option is provided, they are treated as "AND"
|
||||
@@ -1887,6 +1904,7 @@ def query(
|
||||
from_date,
|
||||
to_date,
|
||||
label,
|
||||
is_reference,
|
||||
]
|
||||
exclusive = [
|
||||
(favorite, not_favorite),
|
||||
@@ -2002,6 +2020,7 @@ def query(
|
||||
no_comment=no_comment,
|
||||
has_likes=has_likes,
|
||||
no_likes=no_likes,
|
||||
is_reference=is_reference,
|
||||
)
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
@@ -2172,6 +2191,7 @@ def _query(
|
||||
no_comment=False,
|
||||
has_likes=False,
|
||||
no_likes=False,
|
||||
is_reference=False,
|
||||
):
|
||||
"""Run a query against PhotosDB to extract the photos based on user supply criteria used by query and export commands
|
||||
|
||||
@@ -2316,9 +2336,9 @@ def _query(
|
||||
photos = [p for p in photos if not p.hidden]
|
||||
|
||||
if missing:
|
||||
photos = [p for p in photos if p.ismissing]
|
||||
photos = [p for p in photos if not p.path]
|
||||
elif not_missing:
|
||||
photos = [p for p in photos if not p.ismissing]
|
||||
photos = [p for p in photos if p.path]
|
||||
|
||||
if shared:
|
||||
photos = [p for p in photos if p.shared]
|
||||
@@ -2401,6 +2421,9 @@ def _query(
|
||||
elif no_likes:
|
||||
photos = [p for p in photos if not p.likes]
|
||||
|
||||
if is_reference:
|
||||
photos = [p for p in photos if p.isreference]
|
||||
|
||||
return photos
|
||||
|
||||
|
||||
@@ -3760,5 +3783,6 @@ SOFTWARE.
|
||||
click.echo(f"Source code available at: {OSXPHOTOS_URL}")
|
||||
click.echo(license)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli() # pylint: disable=no-value-for-parameter
|
||||
|
||||
@@ -89,6 +89,7 @@ _MOVIE_TYPE = 1
|
||||
# Name of XMP template file
|
||||
_TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "templates")
|
||||
_XMP_TEMPLATE_NAME = "xmp_sidecar.mako"
|
||||
_XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
|
||||
|
||||
# Constants used for processing folders and albums
|
||||
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.39.18"
|
||||
__version__ = "0.39.25"
|
||||
|
||||
@@ -4,6 +4,11 @@ import json
|
||||
import logging
|
||||
import math
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
MWG_RS_Area = namedtuple("MWG_RS_Area", ["x", "y", "h", "w"])
|
||||
MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
|
||||
|
||||
|
||||
class PersonInfo:
|
||||
""" Info about a person in the Photos library
|
||||
@@ -216,6 +221,62 @@ class FaceInfo:
|
||||
logging.warning(f"Could not get photo for uuid: {self.asset_uuid}")
|
||||
return self._photo
|
||||
|
||||
@property
|
||||
def mwg_rs_area(self):
|
||||
""" Get coordinates for Metadata Working Group Region Area.
|
||||
|
||||
Returns:
|
||||
MWG_RS_Area named tuple with x, y, h, w where:
|
||||
x = stArea:x
|
||||
y = stArea:y
|
||||
h = stArea:h
|
||||
w = stArea:w
|
||||
|
||||
Reference:
|
||||
https://photo.stackexchange.com/questions/106410/how-does-xmp-define-the-face-region
|
||||
"""
|
||||
x, y = self.center_x, self.center_y
|
||||
x, y = self._fix_orientation((x, y))
|
||||
|
||||
if self.photo.orientation in [5, 6, 7, 8]:
|
||||
w = self.size_pixels / self.photo.height
|
||||
h = self.size_pixels / self.photo.width
|
||||
else:
|
||||
h = self.size_pixels / self.photo.height
|
||||
w = self.size_pixels / self.photo.width
|
||||
|
||||
return MWG_RS_Area(x, y, h, w)
|
||||
|
||||
@property
|
||||
def mpri_reg_rect(self):
|
||||
""" Get coordinates for Microsoft Photo Region Rectangle.
|
||||
|
||||
Returns:
|
||||
MPRI_Reg_Rect named tuple with x, y, h, w where:
|
||||
x = x coordinate of top left corner of rectangle
|
||||
y = y coordinate of top left corner of rectangle
|
||||
h = height of rectangle
|
||||
w = width of rectangle
|
||||
|
||||
Reference:
|
||||
https://docs.microsoft.com/en-us/windows/win32/wic/-wic-people-tagging
|
||||
"""
|
||||
x, y = self.center_x, self.center_y
|
||||
x, y = self._fix_orientation((x, y))
|
||||
|
||||
if self.photo.orientation in [5, 6, 7, 8]:
|
||||
w = self.size_pixels / self.photo.width
|
||||
h = self.size_pixels / self.photo.height
|
||||
x = x - self.size_pixels / self.photo.height / 2
|
||||
y = y - self.size_pixels / self.photo.width / 2
|
||||
else:
|
||||
h = self.size_pixels / self.photo.width
|
||||
w = self.size_pixels / self.photo.height
|
||||
x = x - self.size_pixels / self.photo.width / 2
|
||||
y = y - self.size_pixels / self.photo.height / 2
|
||||
|
||||
return MPRI_Reg_Rect(x, y, h, w)
|
||||
|
||||
def face_rect(self):
|
||||
""" Get face rectangle coordinates for current version of the associated image
|
||||
If image has been edited, rectangle applies to edited version, otherwise original version
|
||||
@@ -259,6 +320,43 @@ class FaceInfo:
|
||||
_, _, yaw = self.roll_pitch_yaw()
|
||||
return yaw
|
||||
|
||||
def _fix_orientation(self, xy):
|
||||
""" Translate an (x, y) tuple based on image orientation
|
||||
|
||||
Arguments:
|
||||
xy: tuple of (x, y) coordinates for point to translate
|
||||
in format used by Photos (percent of height/width)
|
||||
|
||||
Returns:
|
||||
(x, y) tuple of translated coordinates
|
||||
"""
|
||||
# Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94
|
||||
|
||||
orientation = self.photo.orientation
|
||||
x, y = xy
|
||||
if orientation == 1:
|
||||
y = 1.0 - y
|
||||
elif orientation == 2:
|
||||
y = 1.0 - y
|
||||
x = 1.0 - x
|
||||
elif orientation == 3:
|
||||
x = 1.0 - x
|
||||
elif orientation == 4:
|
||||
pass
|
||||
elif orientation == 5:
|
||||
x, y = 1.0 - y, x
|
||||
elif orientation == 6:
|
||||
x, y = 1.0 - y, 1.0 - x
|
||||
elif orientation == 7:
|
||||
x, y = y, x
|
||||
y = 1.0 - y
|
||||
elif orientation ==8:
|
||||
x, y = y, x
|
||||
else:
|
||||
logging.warning(f"Unhandled orientation: {orientation}")
|
||||
|
||||
return (x, y)
|
||||
|
||||
def _make_point(self, xy):
|
||||
""" Translate an (x, y) tuple based on image orientation
|
||||
and convert to image coordinates
|
||||
@@ -273,22 +371,11 @@ class FaceInfo:
|
||||
# Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94
|
||||
|
||||
orientation = self.photo.orientation
|
||||
x, y = xy
|
||||
x, y = self._fix_orientation(xy)
|
||||
dx = self.photo.width
|
||||
dy = self.photo.height
|
||||
if orientation in [1, 2]:
|
||||
y = 1.0 - y
|
||||
elif orientation in [3, 4]:
|
||||
x = 1.0 - x
|
||||
elif orientation in [5, 6]:
|
||||
x, y = 1.0 - y, 1.0 - x
|
||||
if orientation in [5, 6, 7, 8]:
|
||||
dx, dy = dy, dx
|
||||
elif orientation in [7, 8]:
|
||||
x, y = y, x
|
||||
dx, dy = dy, dx
|
||||
else:
|
||||
logging.warning(f"Unhandled orientation: {orientation}")
|
||||
|
||||
return (int(x * dx), int(y * dy))
|
||||
|
||||
def _make_point_with_rotation(self, xy):
|
||||
@@ -336,6 +423,8 @@ class FaceInfo:
|
||||
"right_eye": self.right_eye,
|
||||
"size": self.size,
|
||||
"face_rect": self.face_rect(),
|
||||
"mpri_reg_rect": self.mpri_reg_rect._asdict(),
|
||||
"mwg_rs_area": self.mwg_rs_area._asdict(),
|
||||
"roll": roll,
|
||||
"pitch": pitch,
|
||||
"yaw": yaw,
|
||||
|
||||
@@ -35,10 +35,12 @@ from .._constants import (
|
||||
_TEMPLATE_DIR,
|
||||
_UNKNOWN_PERSON,
|
||||
_XMP_TEMPLATE_NAME,
|
||||
_XMP_TEMPLATE_NAME_BETA,
|
||||
SIDECAR_EXIFTOOL,
|
||||
SIDECAR_JSON,
|
||||
SIDECAR_XMP,
|
||||
)
|
||||
from .._version import __version__
|
||||
from ..datetime_utils import datetime_tz_to_utc
|
||||
from ..exiftool import ExifTool
|
||||
from ..export_db import ExportDBNoOp
|
||||
@@ -49,7 +51,10 @@ from ..photokit import (
|
||||
PhotoKitFetchFailed,
|
||||
PhotoLibrary,
|
||||
)
|
||||
from ..utils import dd_to_dms_str, findfiles, noop, get_preferred_uti_extension
|
||||
from ..utils import findfiles, get_preferred_uti_extension, noop
|
||||
|
||||
# retry if use_photos_export fails the first time (which sometimes it does)
|
||||
MAX_PHOTOSCRIPT_RETRIES = 3
|
||||
|
||||
|
||||
class ExportError(Exception):
|
||||
@@ -233,9 +238,16 @@ def _export_photo_uuid_applescript(
|
||||
exported_files = []
|
||||
filename = None
|
||||
try:
|
||||
photo = photoscript.Photo(uuid)
|
||||
filename = photo.filename
|
||||
exported_files = photo.export(tmpdir.name, original=original, timeout=timeout)
|
||||
# I've seen intermittent failures with the PhotoScript export so retry if
|
||||
# export doesn't return anything
|
||||
retries = 0
|
||||
while not exported_files and retries < MAX_PHOTOSCRIPT_RETRIES:
|
||||
photo = photoscript.Photo(uuid)
|
||||
filename = photo.filename
|
||||
exported_files = photo.export(
|
||||
tmpdir.name, original=original, timeout=timeout
|
||||
)
|
||||
retries += 1
|
||||
except Exception as e:
|
||||
raise ExportError(e)
|
||||
|
||||
@@ -268,7 +280,7 @@ def _export_photo_uuid_applescript(
|
||||
exported_paths.append(str(dest_new))
|
||||
return exported_paths
|
||||
else:
|
||||
return None
|
||||
return []
|
||||
|
||||
|
||||
# _check_export_suffix is not a class method, don't import this into PhotoInfo
|
||||
@@ -1703,11 +1715,15 @@ def _xmp_sidecar(
|
||||
use_persons_as_keywords: treat person names as keywords
|
||||
keyword_template: (list of strings); list of template strings to render as keywords
|
||||
description_template: string; optional template string that will be rendered for use as photo description
|
||||
extension: which extension to use for SidecarForExtension property
|
||||
merge_exif_keywords: boolean; if True, merged keywords found in file's exif data (requires exiftool)
|
||||
merge_exif_persons: boolean; if True, merged persons found in file's exif data (requires exiftool)
|
||||
"""
|
||||
|
||||
xmp_template = Template(filename=os.path.join(_TEMPLATE_DIR, _XMP_TEMPLATE_NAME))
|
||||
xmp_template_file = (
|
||||
_XMP_TEMPLATE_NAME if not self._db._beta else _XMP_TEMPLATE_NAME_BETA
|
||||
)
|
||||
xmp_template = Template(filename=os.path.join(_TEMPLATE_DIR, xmp_template_file))
|
||||
|
||||
if extension is None:
|
||||
extension = pathlib.Path(self.original_filename)
|
||||
@@ -1793,6 +1809,7 @@ def _xmp_sidecar(
|
||||
persons=person_list,
|
||||
subjects=subject_list,
|
||||
extension=extension,
|
||||
version=__version__,
|
||||
)
|
||||
|
||||
# remove extra lines that mako inserts from template
|
||||
@@ -1814,4 +1831,3 @@ def _write_sidecar(self, filename, sidecar_str):
|
||||
f = open(filename, "w")
|
||||
f.write(sidecar_str)
|
||||
f.close()
|
||||
|
||||
|
||||
@@ -138,6 +138,7 @@ class PhotoInfo:
|
||||
except AttributeError:
|
||||
self._path = None
|
||||
photopath = None
|
||||
# TODO: should path try to return path even if ismissing?
|
||||
if self._info["isMissing"] == 1:
|
||||
return photopath # path would be meaningless until downloaded
|
||||
|
||||
@@ -643,6 +644,11 @@ class PhotoInfo:
|
||||
else:
|
||||
return True if self._info["cloudAssetGUID"] is not None else False
|
||||
|
||||
@property
|
||||
def isreference(self):
|
||||
""" Returns True if photo is a reference (not copied to the Photos library), otherwise False """
|
||||
return self._info["isreference"]
|
||||
|
||||
@property
|
||||
def burst(self):
|
||||
""" Returns True if photo is part of a Burst photo set, otherwise False """
|
||||
@@ -1033,6 +1039,7 @@ class PhotoInfo:
|
||||
"path_live_photo": self.path_live_photo,
|
||||
"iscloudasset": self.iscloudasset,
|
||||
"incloud": self.incloud,
|
||||
"isreference": self.isreference,
|
||||
"date_modified": self.date_modified,
|
||||
"portrait": self.portrait,
|
||||
"screenshot": self.screenshot,
|
||||
|
||||
@@ -99,6 +99,9 @@ class PhotosDB:
|
||||
raise TypeError("verbose must be callable")
|
||||
self._verbose = verbose
|
||||
|
||||
# enable beta features
|
||||
self._beta = False
|
||||
|
||||
self._exiftool_path = exiftool
|
||||
|
||||
# create a temporary directory
|
||||
@@ -889,7 +892,8 @@ class PhotosDB:
|
||||
RKMaster.fileSize,
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary
|
||||
RKVersion.showInLibrary,
|
||||
RKMaster.fileIsReference
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
@@ -919,8 +923,9 @@ class PhotosDB:
|
||||
RKMaster.originalFileSize,
|
||||
RKVersion.subType,
|
||||
RKVersion.inTrashDate,
|
||||
RKVersion.showInLibrary
|
||||
FROM RKVersion, RKMaster
|
||||
RKVersion.showInLibrary,
|
||||
RKMaster.fileIsReference
|
||||
FROM RKVersion, RKMaster
|
||||
WHERE RKVersion.masterUuid = RKMaster.uuid"""
|
||||
)
|
||||
|
||||
@@ -968,6 +973,7 @@ class PhotosDB:
|
||||
# 40 RKVersion.subType
|
||||
# 41 RKVersion.inTrashDate
|
||||
# 42 RKVersion.showInLibrary -- is item visible in library (e.g. non-selected burst images are not visible)
|
||||
# 43 RKMaster.fileIsReference -- file is reference (imported without copying to Photos library)
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -1164,6 +1170,10 @@ class PhotosDB:
|
||||
self._dbphotos[uuid]["visibility_state"] = row[42]
|
||||
self._dbphotos[uuid]["visible"] = row[42] == 1
|
||||
|
||||
# file is reference (not copied into Photos library)
|
||||
self._dbphotos[uuid]["isreference"] = row[43] == 1
|
||||
self._dbphotos[uuid]["saved_asset_type"] = None # Photos 5+
|
||||
|
||||
# import session not yet handled for Photos 4
|
||||
self._dbphotos[uuid]["import_session"] = None
|
||||
self._dbphotos[uuid]["import_uuid"] = None
|
||||
@@ -1859,7 +1869,8 @@ class PhotosDB:
|
||||
{depth_state},
|
||||
{asset_table}.ZADJUSTMENTTIMESTAMP,
|
||||
{asset_table}.ZVISIBILITYSTATE,
|
||||
{asset_table}.ZTRASHEDDATE
|
||||
{asset_table}.ZTRASHEDDATE,
|
||||
{asset_table}.ZSAVEDASSETTYPE
|
||||
FROM {asset_table}
|
||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
||||
ORDER BY {asset_table}.ZUUID """
|
||||
@@ -1906,6 +1917,7 @@ class PhotosDB:
|
||||
# 37 ZGENERICASSET.ZADJUSTMENTTIMESTAMP -- when was photo edited?
|
||||
# 38 ZGENERICASSET.ZVISIBILITYSTATE -- 0 if visible, 2 if not (e.g. a burst image)
|
||||
# 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash
|
||||
# 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported
|
||||
|
||||
for row in c:
|
||||
uuid = row[0]
|
||||
@@ -2085,6 +2097,14 @@ class PhotosDB:
|
||||
info["visibility_state"] = row[38]
|
||||
info["visible"] = row[38] == 0
|
||||
|
||||
# ZSAVEDASSETTYPE Values:
|
||||
# 3: imported by copying to Photos library
|
||||
# 4: shared iCloud photo
|
||||
# 6: imported by iCloud (e.g. from iPhone)
|
||||
# 10: referenced file (not copied to Photos library)
|
||||
info["saved_asset_type"] = row[40]
|
||||
info["isreference"] = row[40] == 10
|
||||
|
||||
# initialize import session info which will be filled in later
|
||||
# not every photo has an import session so initialize all records now
|
||||
info["import_session"] = None
|
||||
|
||||
210
osxphotos/templates/xmp_sidecar_beta.mako
Normal file
210
osxphotos/templates/xmp_sidecar_beta.mako
Normal file
@@ -0,0 +1,210 @@
|
||||
<%def name="photoshop_sidecar_for_extension(extension)">
|
||||
% if extension is None:
|
||||
<photoshop:SidecarForExtension></photoshop:SidecarForExtension>
|
||||
% else:
|
||||
<photoshop:SidecarForExtension>${extension}</photoshop:SidecarForExtension>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_description(desc)">
|
||||
% if desc is None:
|
||||
<dc:description>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'/>
|
||||
</rdf:Alt>
|
||||
</dc:description>
|
||||
% else:
|
||||
<dc:description>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'>${desc | x}</rdf:li>
|
||||
</rdf:Alt>
|
||||
</dc:description>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_title(title)">
|
||||
% if title is None:
|
||||
<dc:title>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'/>
|
||||
</rdf:Alt>
|
||||
</dc:title>
|
||||
% else:
|
||||
<dc:title>
|
||||
<rdf:Alt>
|
||||
<rdf:li xml:lang='x-default'>${title | x}</rdf:li>
|
||||
</rdf:Alt>
|
||||
</dc:title>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_subject(subject)">
|
||||
% if subject:
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
% for subj in subject:
|
||||
<rdf:li>${subj | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dc_datecreated(date)">
|
||||
% if date is not None:
|
||||
<photoshop:DateCreated>${date.isoformat()}</photoshop:DateCreated>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="iptc_personinimage(persons)">
|
||||
% if persons:
|
||||
<Iptc4xmpExt:PersonInImage>
|
||||
<rdf:Bag>
|
||||
% for person in persons:
|
||||
<rdf:li>${person | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</Iptc4xmpExt:PersonInImage>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="dk_tagslist(keywords)">
|
||||
% if keywords:
|
||||
<digiKam:TagsList>
|
||||
<rdf:Seq>
|
||||
% for keyword in keywords:
|
||||
<rdf:li>${keyword | x}</rdf:li>
|
||||
% endfor
|
||||
</rdf:Seq>
|
||||
</digiKam:TagsList>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="adobe_createdate(date)">
|
||||
% if date is not None:
|
||||
<xmp:CreateDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:CreateDate>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="adobe_modifydate(date)">
|
||||
% if date is not None:
|
||||
<xmp:ModifyDate>${date.strftime("%Y-%m-%dT%H:%M:%S")}</xmp:ModifyDate>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="gps_info(latitude, longitude)">
|
||||
% if latitude is not None and longitude is not None:
|
||||
<exif:GPSLongitude>${int(abs(longitude))},${(abs(longitude) % 1) * 60}${"E" if longitude >= 0 else "W"}</exif:GPSLongitude>
|
||||
<exif:GPSLatitude>${int(abs(latitude))},${(abs(latitude) % 1) * 60}${"N" if latitude >= 0 else "S"}</exif:GPSLatitude>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="orientation(orientation)">
|
||||
% if orientation is not None:
|
||||
<tiff:Orientation>${orientation}</tiff:Orientation>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="mwg_face_regions(photo)">
|
||||
% if photo.face_info:
|
||||
<mwg-rs:Regions rdf:parseType="Resource">
|
||||
<mwg-rs:AppliedToDimensions rdf:parseType="Resource">
|
||||
<stDim:unit>pixel</stDim:unit>
|
||||
</mwg-rs:AppliedToDimensions>
|
||||
<mwg-rs:RegionList>
|
||||
<rdf:Bag>
|
||||
% for face in photo.face_info:
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<mwg-rs:Area rdf:parseType="Resource">
|
||||
<stArea:h>${'{0:.6f}'.format(face.mwg_rs_area.h)}</stArea:h>
|
||||
<stArea:w>${'{0:.6f}'.format(face.mwg_rs_area.w)}</stArea:w>
|
||||
<stArea:x>${'{0:.6f}'.format(face.mwg_rs_area.x)}</stArea:x>
|
||||
<stArea:y>${'{0:.6f}'.format(face.mwg_rs_area.y)}</stArea:y>
|
||||
<stArea:unit>normalized</stArea:unit>
|
||||
</mwg-rs:Area>
|
||||
<mwg-rs:Name>${face.name}</mwg-rs:Name>
|
||||
<mwg-rs:Rotation>${face.roll}</mwg-rs:Rotation>
|
||||
<mwg-rs:Type>Face</mwg-rs:Type>
|
||||
</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</mwg-rs:RegionList>
|
||||
</mwg-rs:Regions>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="mpri_face_regions(photo)">
|
||||
% if photo.face_info:
|
||||
<MP:RegionInfo rdf:parseType="Resource">
|
||||
<MPRI:Regions>
|
||||
<rdf:Bag>
|
||||
% for face in photo.face_info:
|
||||
<rdf:li rdf:parseType="Resource">
|
||||
<MPReg:PersonDisplayName>${face.name}</MPReg:PersonDisplayName>
|
||||
<MPReg:Rectangle>${'{0:.6f}'.format(face.mpri_reg_rect.x)}, ${'{0:.6f}'.format(face.mpri_reg_rect.y)}, ${'{0:.6f}'.format(face.mpri_reg_rect.h)}, ${'{0:.6f}'.format(face.mpri_reg_rect.w)}</MPReg:Rectangle>
|
||||
</rdf:li>
|
||||
% endfor
|
||||
</rdf:Bag>
|
||||
</MPRI:Regions>
|
||||
</MP:RegionInfo>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
|
||||
<?xpacket begin="${"\uFEFF"}" id="W5M0MpCehiHzreSzNTczkc9d"?>
|
||||
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="osxphotos ${version}">
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/">
|
||||
${photoshop_sidecar_for_extension(extension)}
|
||||
${dc_description(description)}
|
||||
${dc_title(photo.title)}
|
||||
${dc_subject(subjects)}
|
||||
${dc_datecreated(photo.date)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:Iptc4xmpExt='http://iptc.org/std/Iptc4xmpExt/2008-02-29/'>
|
||||
${iptc_personinimage(persons)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:digiKam='http://www.digikam.org/ns/1.0/'>
|
||||
${dk_tagslist(keywords)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
|
||||
${adobe_createdate(photo.date)}
|
||||
${adobe_modifydate(photo.date)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
|
||||
${gps_info(*photo.location)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=''
|
||||
xmlns:tiff='http://ns.adobe.com/tiff/1.0/'>
|
||||
${orientation(photo.orientation)}
|
||||
</rdf:Description>
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:mwg-rs="http://www.metadataworkinggroup.com/schemas/regions/"
|
||||
xmlns:stArea="http://ns.adobe.com/xmp/sType/Area#"
|
||||
xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#">
|
||||
${mwg_face_regions(photo)}
|
||||
</rdf:Description>
|
||||
|
||||
|
||||
<rdf:Description rdf:about=""
|
||||
xmlns:MP="http://ns.microsoft.com/photo/1.2/"
|
||||
xmlns:MPRI="http://ns.microsoft.com/photo/1.2/t/RegionInfo#"
|
||||
xmlns:MPReg="http://ns.microsoft.com/photo/1.2/t/Region#">
|
||||
${mpri_face_regions(photo)}
|
||||
</rdf:Description>
|
||||
|
||||
</rdf:RDF>
|
||||
</x:xmpmeta>
|
||||
<?xpacket end="w"?>
|
||||
@@ -6,7 +6,8 @@ import sys
|
||||
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
db = sys.argv[1]
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||
|
||||
face_photos = [p for p in photosdb.photos() if p.face_info]
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
import pytest
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
|
||||
PHOTOS_DB = "tests/Test-10.16.0.1.photoslibrary/database/photos.db"
|
||||
PHOTOS_DB_PATH = "/Test-10.16.0.1.photoslibrary/database/photos.db"
|
||||
@@ -169,6 +169,15 @@ PATH_HEIC_EDITED = (
|
||||
"resources/renders/7/7783E8E6-9CAC-40F3-BE22-81FB7051C266_1_201_a.heic"
|
||||
)
|
||||
|
||||
# file is reference (not copied to library)
|
||||
UUID_IS_REFERENCE = "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"
|
||||
UUID_NOT_REFERENCE = "E9BC5C36-7CD1-40A1-A72B-8B8FAC227D51"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def photosdb():
|
||||
return osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
|
||||
def test_init1():
|
||||
# test named argument
|
||||
@@ -230,80 +239,56 @@ def test_init5(mocker):
|
||||
assert osxphotos.PhotosDB()
|
||||
|
||||
|
||||
def test_db_len():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_db_len(photosdb):
|
||||
# assert photosdb.db_version in osxphotos._TESTED_DB_VERSIONS
|
||||
assert len(photosdb) == PHOTOS_DB_LEN
|
||||
|
||||
|
||||
def test_db_version():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_db_version(photosdb):
|
||||
# assert photosdb.db_version in osxphotos._TESTED_DB_VERSIONS
|
||||
assert photosdb.db_version == "6000"
|
||||
|
||||
|
||||
def test_persons():
|
||||
import osxphotos
|
||||
def test_persons(photosdb):
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert "Katie" in photosdb.persons
|
||||
assert collections.Counter(PERSONS) == collections.Counter(photosdb.persons)
|
||||
|
||||
|
||||
def test_keywords():
|
||||
import osxphotos
|
||||
def test_keywords(photosdb):
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert "wedding" in photosdb.keywords
|
||||
assert collections.Counter(KEYWORDS) == collections.Counter(photosdb.keywords)
|
||||
|
||||
|
||||
def test_album_names():
|
||||
import osxphotos
|
||||
def test_album_names(photosdb):
|
||||
import collections
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
assert "Pumpkin Farm" in photosdb.albums
|
||||
assert collections.Counter(ALBUMS) == collections.Counter(photosdb.albums)
|
||||
|
||||
|
||||
def test_keywords_dict():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_keywords_dict(photosdb):
|
||||
keywords = photosdb.keywords_as_dict
|
||||
assert keywords["wedding"] == 3
|
||||
assert keywords == KEYWORDS_DICT
|
||||
|
||||
|
||||
def test_persons_as_dict():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_persons_as_dict(photosdb):
|
||||
persons = photosdb.persons_as_dict
|
||||
assert persons["Maria"] == 2
|
||||
assert persons == PERSONS_DICT
|
||||
|
||||
|
||||
def test_albums_as_dict():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_albums_as_dict(photosdb):
|
||||
albums = photosdb.albums_as_dict
|
||||
assert albums["Pumpkin Farm"] == 3
|
||||
assert albums == ALBUM_DICT
|
||||
|
||||
|
||||
def test_album_sort_order():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_album_sort_order(photosdb):
|
||||
album = [a for a in photosdb.album_info if a.title == "Pumpkin Farm"][0]
|
||||
photos = album.photos
|
||||
|
||||
@@ -311,20 +296,15 @@ def test_album_sort_order():
|
||||
assert uuids == ALBUM_SORT_ORDER
|
||||
|
||||
|
||||
def test_album_empty_album():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_album_empty_album(photosdb):
|
||||
album = [a for a in photosdb.album_info if a.title == "EmptyAlbum"][0]
|
||||
photos = album.photos
|
||||
assert photos == []
|
||||
|
||||
|
||||
def test_attributes():
|
||||
def test_attributes(photosdb):
|
||||
import datetime
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=["D79B8D77-BFFC-460B-9312-034F2877D35B"])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
@@ -344,12 +324,10 @@ def test_attributes():
|
||||
assert p.ismissing == False
|
||||
|
||||
|
||||
def test_attributes_2():
|
||||
def test_attributes_2(photosdb):
|
||||
""" Test attributes including height, width, etc """
|
||||
import datetime
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
@@ -384,10 +362,7 @@ def test_attributes_2():
|
||||
assert p.original_filesize == 460483
|
||||
|
||||
|
||||
def test_missing():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_missing(photosdb):
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
@@ -395,51 +370,36 @@ def test_missing():
|
||||
assert p.ismissing == True
|
||||
|
||||
|
||||
def test_favorite():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_favorite(photosdb):
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["favorite"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.favorite == True
|
||||
|
||||
|
||||
def test_not_favorite():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_not_favorite(photosdb):
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_favorite"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.favorite == False
|
||||
|
||||
|
||||
def test_hidden():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_hidden(photosdb):
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.hidden == True
|
||||
|
||||
|
||||
def test_not_hidden():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_not_hidden(photosdb):
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["not_hidden"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.hidden == False
|
||||
|
||||
|
||||
def test_location_1():
|
||||
def test_location_1(photosdb):
|
||||
# test photo with lat/lon info
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["location"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
@@ -448,11 +408,8 @@ def test_location_1():
|
||||
assert lon == pytest.approx(-0.1318055)
|
||||
|
||||
|
||||
def test_location_2():
|
||||
def test_location_2(photosdb):
|
||||
# test photo with no location info
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["no_location"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
@@ -461,33 +418,24 @@ def test_location_2():
|
||||
assert lon is None
|
||||
|
||||
|
||||
def test_hasadjustments1():
|
||||
def test_hasadjustments1(photosdb):
|
||||
# test hasadjustments == True
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.hasadjustments == True
|
||||
|
||||
|
||||
def test_hasadjustments2():
|
||||
def test_hasadjustments2(photosdb):
|
||||
# test hasadjustments == False
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
assert p.hasadjustments == False
|
||||
|
||||
|
||||
def test_external_edit1():
|
||||
def test_external_edit1(photosdb):
|
||||
# test image has been edited in external editor
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["external_edit"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
@@ -495,11 +443,8 @@ def test_external_edit1():
|
||||
assert p.external_edit == True
|
||||
|
||||
|
||||
def test_external_edit2():
|
||||
def test_external_edit2(photosdb):
|
||||
# test image has not been edited in external editor
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["no_external_edit"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
@@ -507,12 +452,10 @@ def test_external_edit2():
|
||||
assert p.external_edit == False
|
||||
|
||||
|
||||
def test_path_edited_jpeg():
|
||||
def test_path_edited_jpeg(photosdb):
|
||||
# test a valid edited path
|
||||
import os.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]
|
||||
@@ -523,22 +466,17 @@ def test_path_edited_jpeg():
|
||||
assert os.path.exists(path)
|
||||
|
||||
|
||||
def test_path_edited_heic():
|
||||
def test_path_edited_heic(photosdb):
|
||||
# test a valid edited path for .heic image
|
||||
import pathlib
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photo = photosdb.get_photo(UUID_HEIC_EDITED)
|
||||
assert photo.path_edited.endswith(PATH_HEIC_EDITED)
|
||||
assert pathlib.Path(photo.path_edited).is_file()
|
||||
|
||||
|
||||
def test_path_edited2():
|
||||
def test_path_edited2(photosdb):
|
||||
# test an invalid edited path
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
|
||||
assert len(photos) == 1
|
||||
p = photos[0]
|
||||
@@ -546,115 +484,79 @@ def test_path_edited2():
|
||||
assert path is None
|
||||
|
||||
|
||||
def test_count():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_count(photosdb):
|
||||
photos = photosdb.photos()
|
||||
assert len(photos) == PHOTOS_NOT_IN_TRASH_LEN
|
||||
|
||||
|
||||
def test_photos_intrash_1():
|
||||
def test_photos_intrash_1(photosdb):
|
||||
""" test PhotosDB.photos(intrash=True) """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(intrash=True)
|
||||
assert len(photos) == PHOTOS_IN_TRASH_LEN
|
||||
|
||||
|
||||
def test_photos_intrash_2():
|
||||
def test_photos_intrash_2(photosdb):
|
||||
""" test PhotosDB.photos(intrash=True) """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(intrash=True)
|
||||
for p in photos:
|
||||
assert p.intrash
|
||||
|
||||
|
||||
def test_photos_intrash_3():
|
||||
def test_photos_intrash_3(photosdb):
|
||||
""" test PhotosDB.photos(intrash=False) """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(intrash=False)
|
||||
for p in photos:
|
||||
assert not p.intrash
|
||||
|
||||
|
||||
def test_photoinfo_intrash_1():
|
||||
def test_photoinfo_intrash_1(photosdb):
|
||||
""" Test PhotoInfo.intrash """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash"]], intrash=True)[0]
|
||||
assert p.intrash
|
||||
|
||||
|
||||
def test_photoinfo_intrash_2():
|
||||
def test_photoinfo_intrash_2(photosdb):
|
||||
""" Test PhotoInfo.intrash and intrash=default"""
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash"]])
|
||||
assert not p
|
||||
|
||||
|
||||
def test_photoinfo_intrash_3():
|
||||
def test_photoinfo_intrash_3(photosdb):
|
||||
""" Test PhotoInfo.intrash and photo has keyword and person """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["intrash_person_keywords"]], intrash=True)[0]
|
||||
assert p.intrash
|
||||
assert "Maria" in p.persons
|
||||
assert "wedding" in p.keywords
|
||||
|
||||
|
||||
def test_photoinfo_intrash_4():
|
||||
def test_photoinfo_intrash_4(photosdb):
|
||||
""" Test PhotoInfo.intrash and photo has keyword and person """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(persons=["Maria"], intrash=True)[0]
|
||||
assert p.intrash
|
||||
assert "Maria" in p.persons
|
||||
assert "wedding" in p.keywords
|
||||
|
||||
|
||||
def test_photoinfo_intrash_5():
|
||||
def test_photoinfo_intrash_5(photosdb):
|
||||
""" Test PhotoInfo.intrash and photo has keyword and person """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(keywords=["wedding"], intrash=True)[0]
|
||||
assert p.intrash
|
||||
assert "Maria" in p.persons
|
||||
assert "wedding" in p.keywords
|
||||
|
||||
|
||||
def test_photoinfo_not_intrash():
|
||||
def test_photoinfo_not_intrash(photosdb):
|
||||
""" Test PhotoInfo.intrash """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
p = photosdb.photos(uuid=[UUID_DICT["not_intrash"]])[0]
|
||||
assert not p.intrash
|
||||
|
||||
|
||||
def test_keyword_2():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_keyword_2(photosdb):
|
||||
photos = photosdb.photos(keywords=["wedding"])
|
||||
assert len(photos) == 2 # won't show the one in the trash
|
||||
|
||||
|
||||
def test_keyword_not_in_album():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_keyword_not_in_album(photosdb):
|
||||
|
||||
# find all photos with keyword "Kids" not in the album "Pumpkin Farm"
|
||||
photos1 = photosdb.photos(albums=["Pumpkin Farm"])
|
||||
@@ -664,47 +566,33 @@ def test_keyword_not_in_album():
|
||||
assert photos3[0].uuid == "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"
|
||||
|
||||
|
||||
def test_album_folder_name():
|
||||
def test_album_folder_name(photosdb):
|
||||
"""Test query with album name same as a folder name """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
photos = photosdb.photos(albums=["Pumpkin Farm"])
|
||||
assert sorted(p.uuid for p in photos) == sorted(UUID_PUMPKIN_FARM)
|
||||
|
||||
|
||||
def test_multi_person():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
def test_multi_person(photosdb):
|
||||
photos = photosdb.photos(persons=["Katie", "Suzy"])
|
||||
|
||||
assert len(photos) == 3
|
||||
|
||||
|
||||
def test_get_db_path():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_get_db_path(photosdb):
|
||||
db_path = photosdb.db_path
|
||||
assert db_path.endswith(PHOTOS_DB_PATH)
|
||||
|
||||
|
||||
def test_get_library_path():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_get_library_path(photosdb):
|
||||
lib_path = photosdb.library_path
|
||||
assert lib_path.endswith(PHOTOS_LIBRARY_PATH)
|
||||
|
||||
|
||||
def test_get_db_connection():
|
||||
def test_get_db_connection(photosdb):
|
||||
""" Test PhotosDB.get_db_connection """
|
||||
import osxphotos
|
||||
import sqlite3
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
conn, cursor = photosdb.get_db_connection()
|
||||
|
||||
assert isinstance(conn, sqlite3.Connection)
|
||||
@@ -717,18 +605,15 @@ def test_get_db_connection():
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_export_1():
|
||||
def test_export_1(photosdb):
|
||||
# test basic export
|
||||
# get an unedited image and export it using default filename
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
@@ -739,18 +624,15 @@ def test_export_1():
|
||||
assert os.path.isfile(got_dest)
|
||||
|
||||
|
||||
def test_export_2():
|
||||
def test_export_2(photosdb):
|
||||
# test export with user provided filename
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
timestamp = time.time()
|
||||
@@ -762,18 +644,15 @@ def test_export_2():
|
||||
assert os.path.isfile(got_dest)
|
||||
|
||||
|
||||
def test_export_3():
|
||||
def test_export_3(photosdb):
|
||||
# test file already exists and test increment=True (default)
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
@@ -788,7 +667,7 @@ def test_export_3():
|
||||
assert os.path.isfile(got_dest_2)
|
||||
|
||||
|
||||
def test_export_4():
|
||||
def test_export_4(photosdb):
|
||||
# test user supplied file already exists and test increment=True (default)
|
||||
import os
|
||||
import os.path
|
||||
@@ -796,11 +675,8 @@ def test_export_4():
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
timestamp = time.time()
|
||||
@@ -815,18 +691,15 @@ def test_export_4():
|
||||
assert os.path.isfile(got_dest_2)
|
||||
|
||||
|
||||
def test_export_5():
|
||||
def test_export_5(photosdb):
|
||||
# test file already exists and test increment=True (default)
|
||||
# and overwrite = True
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
@@ -840,7 +713,7 @@ def test_export_5():
|
||||
assert os.path.isfile(got_dest_2)
|
||||
|
||||
|
||||
def test_export_6():
|
||||
def test_export_6(photosdb):
|
||||
# test user supplied file already exists and test increment=True (default)
|
||||
# and overwrite = True
|
||||
import os
|
||||
@@ -849,11 +722,8 @@ def test_export_6():
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
timestamp = time.time()
|
||||
@@ -868,18 +738,15 @@ def test_export_6():
|
||||
assert os.path.isfile(got_dest_2)
|
||||
|
||||
|
||||
def test_export_7():
|
||||
def test_export_7(photosdb):
|
||||
# test file already exists and test increment=False (not default), overwrite=False (default)
|
||||
# should raise exception
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
@@ -891,18 +758,15 @@ def test_export_7():
|
||||
assert e.type == type(FileExistsError())
|
||||
|
||||
|
||||
def test_export_8():
|
||||
def test_export_8(photosdb):
|
||||
# try to export missing file
|
||||
# should raise exception
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["missing"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
@@ -912,16 +776,13 @@ def test_export_8():
|
||||
assert e.type == type(FileNotFoundError())
|
||||
|
||||
|
||||
def test_export_9():
|
||||
def test_export_9(photosdb):
|
||||
# try to export edited file that's not edited
|
||||
# should raise exception
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
|
||||
@@ -933,7 +794,7 @@ def test_export_9():
|
||||
assert e.type == ValueError
|
||||
|
||||
|
||||
def test_export_10():
|
||||
def test_export_10(photosdb):
|
||||
# try to export edited file that's not edited and name provided
|
||||
# should raise exception
|
||||
import os
|
||||
@@ -941,9 +802,6 @@ def test_export_10():
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["no_adjustments"]])
|
||||
@@ -956,18 +814,15 @@ def test_export_10():
|
||||
assert e.type == ValueError
|
||||
|
||||
|
||||
def test_export_11():
|
||||
def test_export_11(photosdb):
|
||||
# export edited file with name provided
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
|
||||
timestamp = time.time()
|
||||
@@ -978,18 +833,15 @@ def test_export_11():
|
||||
assert got_dest == expected_dest
|
||||
|
||||
|
||||
def test_export_12():
|
||||
def test_export_12(photosdb):
|
||||
# export edited file with default name
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
import tempfile
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
|
||||
|
||||
edited_name = pathlib.Path(photos[0].path_edited).name
|
||||
@@ -1001,15 +853,13 @@ def test_export_12():
|
||||
assert got_dest == expected_dest
|
||||
|
||||
|
||||
def test_export_13():
|
||||
def test_export_13(photosdb):
|
||||
# export to invalid destination
|
||||
# should raise exception
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
|
||||
@@ -1019,7 +869,6 @@ def test_export_13():
|
||||
dest = os.path.join(dest, str(i))
|
||||
i += 1
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
|
||||
filename = photos[0].filename
|
||||
@@ -1029,18 +878,15 @@ def test_export_13():
|
||||
assert e.type == type(FileNotFoundError())
|
||||
|
||||
|
||||
def test_export_14(caplog):
|
||||
def test_export_14(caplog, photosdb):
|
||||
# test export with user provided filename with different (but valid) extension than source
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import osxphotos
|
||||
|
||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos_")
|
||||
dest = tempdir.name
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["export_tif"]])
|
||||
|
||||
timestamp = time.time()
|
||||
@@ -1054,24 +900,22 @@ def test_export_14(caplog):
|
||||
assert "Invalid destination suffix" not in caplog.text
|
||||
|
||||
|
||||
def test_eq():
|
||||
def test_eq(photosdb):
|
||||
""" Test equality of two PhotoInfo objects """
|
||||
import osxphotos
|
||||
|
||||
photosdb1 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photosdb2 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos1 = photosdb1.photos(uuid=[UUID_DICT["export"]])
|
||||
photos1 = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
photos2 = photosdb2.photos(uuid=[UUID_DICT["export"]])
|
||||
assert photos1[0] == photos2[0]
|
||||
|
||||
|
||||
def test_eq_2():
|
||||
def test_eq_2(photosdb):
|
||||
""" Test equality of two PhotoInfo objects when one has memoized property """
|
||||
import osxphotos
|
||||
|
||||
photosdb1 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photosdb2 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos1 = photosdb1.photos(uuid=[UUID_DICT["in_album"]])
|
||||
photos1 = photosdb.photos(uuid=[UUID_DICT["in_album"]])
|
||||
photos2 = photosdb2.photos(uuid=[UUID_DICT["in_album"]])
|
||||
|
||||
# memoize a value
|
||||
@@ -1081,18 +925,13 @@ def test_eq_2():
|
||||
assert photos1[0] == photos2[0]
|
||||
|
||||
|
||||
def test_not_eq():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
def test_not_eq(photosdb):
|
||||
photos1 = photosdb.photos(uuid=[UUID_DICT["export"]])
|
||||
photos2 = photosdb.photos(uuid=[UUID_DICT["missing"]])
|
||||
assert photos1[0] != photos2[0]
|
||||
|
||||
|
||||
def test_photosdb_repr():
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photosdb2 = eval(repr(photosdb))
|
||||
|
||||
@@ -1102,11 +941,9 @@ def test_photosdb_repr():
|
||||
}
|
||||
|
||||
|
||||
def test_photosinfo_repr():
|
||||
import osxphotos
|
||||
def test_photosinfo_repr(photosdb):
|
||||
import datetime
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["favorite"]])
|
||||
photo = photos[0]
|
||||
photo2 = eval(repr(photo))
|
||||
@@ -1116,8 +953,7 @@ def test_photosinfo_repr():
|
||||
}
|
||||
|
||||
|
||||
def test_from_to_date():
|
||||
import osxphotos
|
||||
def test_from_to_date(photosdb):
|
||||
import datetime as dt
|
||||
import os
|
||||
import time
|
||||
@@ -1125,8 +961,6 @@ def test_from_to_date():
|
||||
os.environ["TZ"] = "US/Pacific"
|
||||
time.tzset()
|
||||
|
||||
photosdb = osxphotos.PhotosDB(PHOTOS_DB)
|
||||
|
||||
photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28))
|
||||
assert len(photos) == 7
|
||||
|
||||
@@ -1139,12 +973,10 @@ def test_from_to_date():
|
||||
assert len(photos) == 4
|
||||
|
||||
|
||||
def test_date_invalid():
|
||||
def test_date_invalid(photosdb):
|
||||
""" Test date is invalid """
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
# UUID_DICT["date_invalid"] has an invalid date that's
|
||||
# been manually adjusted in the database
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
|
||||
@@ -1155,12 +987,10 @@ def test_date_invalid():
|
||||
assert p.date == datetime(1970, 1, 1).astimezone(tz=tz)
|
||||
|
||||
|
||||
def test_date_modified_invalid():
|
||||
def test_date_modified_invalid(photosdb):
|
||||
""" Test date modified is invalid """
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
# UUID_DICT["date_invalid"] has an invalid modified date that's
|
||||
# been manually adjusted in the database
|
||||
photos = photosdb.photos(uuid=[UUID_DICT["date_invalid"]])
|
||||
@@ -1169,11 +999,8 @@ def test_date_modified_invalid():
|
||||
assert p.date_modified is None
|
||||
|
||||
|
||||
def test_uti():
|
||||
def test_uti(photosdb):
|
||||
""" test uti """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
for uuid, uti in UTI_DICT.items():
|
||||
photo = photosdb.get_photo(uuid)
|
||||
@@ -1181,11 +1008,8 @@ def test_uti():
|
||||
assert photo.uti_original == UTI_ORIGINAL_DICT[uuid]
|
||||
|
||||
|
||||
def test_raw():
|
||||
def test_raw(photosdb):
|
||||
""" Test various raw properties """
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||
|
||||
for uuid, rawinfo in RAW_DICT.items():
|
||||
photo = photosdb.get_photo(uuid)
|
||||
@@ -1195,3 +1019,12 @@ def test_raw():
|
||||
assert photo.uti == rawinfo.uti
|
||||
assert photo.uti_original == rawinfo.uti_original
|
||||
assert photo.uti_raw == rawinfo.uti_raw
|
||||
|
||||
|
||||
def test_is_reference(photosdb):
|
||||
""" test isreference """
|
||||
|
||||
photo = photosdb.get_photo(UUID_IS_REFERENCE)
|
||||
assert photo.isreference
|
||||
photo = photosdb.get_photo(UUID_NOT_REFERENCE)
|
||||
assert not photo.isreference
|
||||
|
||||
@@ -198,6 +198,9 @@ ORIGINAL_FILENAME_DICT = {
|
||||
"original_filename": "Pumkins2.jpg",
|
||||
}
|
||||
|
||||
UUID_IS_REFERENCE = "A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C"
|
||||
UUID_NOT_REFERENCE = "F12384F6-CD17-4151-ACBA-AE0E3688539E"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def photosdb():
|
||||
@@ -1185,3 +1188,11 @@ def test_visible_burst(photosdb_local):
|
||||
assert photo.burst
|
||||
assert len(photo.burst_photos) == 4
|
||||
|
||||
|
||||
def test_is_reference(photosdb):
|
||||
""" test isreference """
|
||||
|
||||
photo = photosdb.get_photo(UUID_IS_REFERENCE)
|
||||
assert photo.isreference
|
||||
photo = photosdb.get_photo(UUID_NOT_REFERENCE)
|
||||
assert not photo.isreference
|
||||
|
||||
@@ -573,6 +573,11 @@ UUID_JPEGS_DICT = {
|
||||
|
||||
UUID_HEIC = {"7783E8E6-9CAC-40F3-BE22-81FB7051C266": "IMG_3092"}
|
||||
|
||||
UUID_IS_REFERENCE = [
|
||||
"8E1D7BC9-9321-44F9-8CFB-4083F6B9232A",
|
||||
"A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C",
|
||||
]
|
||||
|
||||
|
||||
def modify_file(filename):
|
||||
""" appends data to a file to modify it """
|
||||
@@ -857,6 +862,26 @@ def test_query_no_likes():
|
||||
assert uuid not in UUID_HAS_LIKES
|
||||
|
||||
|
||||
def test_query_is_reference():
|
||||
""" Test query with --is-reference """
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
from osxphotos.__main__ import query
|
||||
|
||||
runner = CliRunner()
|
||||
cwd = os.getcwd()
|
||||
result = runner.invoke(
|
||||
query, ["--json", "--db", os.path.join(cwd, PHOTOS_DB_15_7), "--is-reference"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# build list of uuids we got from the output JSON
|
||||
json_got = json.loads(result.output)
|
||||
uuid_got = [photo["uuid"] for photo in json_got]
|
||||
assert sorted(uuid_got) == sorted(UUID_IS_REFERENCE)
|
||||
|
||||
|
||||
def test_export():
|
||||
import glob
|
||||
import os
|
||||
@@ -1587,7 +1612,7 @@ def test_export_convert_to_jpeg():
|
||||
files = glob.glob("*")
|
||||
assert sorted(files) == sorted(CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG)
|
||||
large_file = pathlib.Path(CLI_EXPORT_CONVERT_TO_JPEG_LARGE_FILE)
|
||||
assert large_file.stat().st_size > 7000000
|
||||
assert large_file.stat().st_size > 7000000
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
|
||||
@@ -623,3 +623,4 @@ def test_xmp_sidecar_keyword_template(photosdb):
|
||||
keyword_template=["{created.year}", "{folder_album}"], extension="jpg"
|
||||
)
|
||||
assert xmp_got == xmp_expected
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -100,6 +100,8 @@ PHOTOS_DB_LEN = 13
|
||||
PHOTOS_NOT_IN_TRASH_LEN = 12
|
||||
PHOTOS_IN_TRASH_LEN = 1
|
||||
|
||||
UUID_NOT_REFERENCE = "6bxcNnzRQKGnK4uPrCJ9UQ"
|
||||
UUID_IS_REFERENCE = "od0fmC7NQx+ayVr+%i06XA"
|
||||
|
||||
RawInfo = namedtuple(
|
||||
"RawInfo",
|
||||
@@ -595,7 +597,7 @@ def test_raw(photosdb):
|
||||
photo = photosdb.get_photo(UUID_DICT["raw"])
|
||||
# assert photo.israw
|
||||
assert not photo.has_raw
|
||||
assert photo.uti_raw == None
|
||||
assert photo.uti_raw is None
|
||||
assert photo.uti == "com.adobe.raw-image"
|
||||
assert photo.path_raw is None
|
||||
|
||||
@@ -626,3 +628,12 @@ def test_raw():
|
||||
assert photo.uti == rawinfo.uti
|
||||
assert photo.uti_original == rawinfo.uti_original
|
||||
assert photo.uti_raw == rawinfo.uti_raw
|
||||
|
||||
|
||||
def test_is_reference(photosdb):
|
||||
""" test isreference """
|
||||
|
||||
photo = photosdb.get_photo(UUID_IS_REFERENCE)
|
||||
assert photo.isreference
|
||||
photo = photosdb.get_photo(UUID_NOT_REFERENCE)
|
||||
assert not photo.isreference
|
||||
|
||||
Reference in New Issue
Block a user