Compare commits

..

64 Commits

Author SHA1 Message Date
Rhet Turnbull
bfbc156821 Updated docs [skip ci] 2021-09-26 15:55:03 -07:00
Rhet Turnbull
bfd6274602 Fixed AlbumInfo.owner, #239 2021-09-26 15:52:38 -07:00
Rhet Turnbull
3abaa5ae84 Updated docs [skip ci] 2021-09-26 15:43:22 -07:00
Rhet Turnbull
65115a50a9 Updated CHANGELOG.md [skip ci] 2021-09-26 15:00:34 -07:00
Rhet Turnbull
06138e15d0 version bump 2021-09-26 14:56:10 -07:00
Rhet Turnbull
14710e3178 Performance fix for #239, owner 2021-09-26 14:55:43 -07:00
Rhet Turnbull
f705f09749 Updated CHANGELOG.md [skip ci] 2021-09-26 14:54:59 -07:00
Rhet Turnbull
82c445f41e Updated CHANGELOG.md [skip ci] 2021-09-26 14:29:36 -07:00
Rhet Turnbull
1b40e9d65f Fixed tests for comment fix 2021-09-26 14:07:35 -07:00
Rhet Turnbull
725f7c8735 Updated docs [skip ci] 2021-09-26 14:00:46 -07:00
Rhet Turnbull
7cc8578148 Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-09-26 13:53:32 -07:00
Rhet Turnbull
6adafb8ce7 Fixed formatting 2021-09-26 13:53:21 -07:00
Rhet Turnbull
ac47df8475 Fix for #517, #239 2021-09-26 13:51:47 -07:00
Rhet Turnbull
f680cf78ab Removed macOS-11, need to fix detected_text test 2021-09-26 09:06:17 -07:00
Rhet Turnbull
c86e84c534 Removed python 3.10-dev, not available in GH 2021-09-26 08:06:32 -07:00
Rhet Turnbull
3fb611825c Added python 3.10, macOS 11 2021-09-26 07:53:32 -07:00
Rhet Turnbull
1cfdad0176 Updated CHANGELOG.md [skip ci] 2021-09-25 22:51:30 -07:00
Rhet Turnbull
59ba325273 Updated docs [skip ci] 2021-09-25 22:38:46 -07:00
Rhet Turnbull
c4b7c2623f Implemented PhotoInfo.owner, AlbumInfo.owner, #216, #239 2021-09-25 22:33:37 -07:00
Rhet Turnbull
e5b2d2ee45 Updated CHANGELOG.md [skip ci] 2021-09-25 09:51:50 -07:00
Rhet Turnbull
64c226b855 Updated docs [skip ci] 2021-09-25 08:54:16 -07:00
Rhet Turnbull
e3e1da2fd8 Fix for #516 2021-09-25 08:47:09 -07:00
Rhet Turnbull
57b2f8a413 Fixed README 2021-09-21 20:14:04 -07:00
Rhet Turnbull
5a76a511db Test pre-commit hook 2021-09-21 20:13:20 -07:00
Rhet Turnbull
283f049780 Test pre-commit hook 2021-09-21 20:12:42 -07:00
Rhet Turnbull
c4743cc867 Test pre-commit hook 2021-09-21 20:11:02 -07:00
Rhet Turnbull
c429a860b1 Update docs 2021-09-17 06:24:39 -07:00
Rhet Turnbull
1f748c829b Updated CHANGELOG.md [skip ci] 2021-09-15 20:35:18 -07:00
Rhet Turnbull
dd08c7f701 Fixed detected_text to use image orientation if available 2021-09-15 20:18:18 -07:00
Rhet Turnbull
77103193c0 Updated CHANGELOG.md [skip ci] 2021-09-14 17:38:22 -07:00
Rhet Turnbull
16335a6bd6 Added twine 2021-09-14 17:36:57 -07:00
Rhet Turnbull
e0f6d8ecf2 Added wheel 2021-09-14 17:36:20 -07:00
Rhet Turnbull
59c31ff88d Fix for #515, updated tests 2021-09-14 17:24:02 -07:00
Rhet Turnbull
93bf0c210c Fix for #515 2021-09-13 22:14:48 -07:00
Rhet Turnbull
4f7642b1d2 Updated tested versions 2021-09-12 17:34:16 -07:00
Rhet Turnbull
773dca8494 Updated docs 2021-09-12 17:31:15 -07:00
Rhet Turnbull
3cd26e2e38 Updated .gitignore 2021-09-12 16:35:48 -07:00
Rhet Turnbull
271761cf04 Updated CHANGELOG.md [skip ci] 2021-08-29 19:06:43 -07:00
Rhet Turnbull
6eea552fb9 Updated README [skip ci] 2021-08-29 18:34:35 -07:00
Rhet Turnbull
81dd1a7530 Updated README [skip ci] 2021-08-29 18:31:50 -07:00
Rhet Turnbull
2eb6e70e57 Updated dependencies 2021-08-29 18:30:23 -07:00
Rhet Turnbull
6bcc67634c Bug fix for null title, #512 2021-08-29 13:06:09 -07:00
Rhet Turnbull
062d8eb206 Updated CHANGELOG.md [skip ci] 2021-08-29 12:21:59 -07:00
Rhet Turnbull
f0d7496bc6 Fix for newlines in exif tags, #513 2021-08-29 12:18:20 -07:00
allcontributors[bot]
8e2b768236 docs: add dssinger as a contributor for bug (#514)
* docs: update README.md [skip ci]

* docs: update .all-contributorsrc [skip ci]

Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com>
2021-08-29 07:07:51 -07:00
Rhet Turnbull
48bf326994 Updated CHANGELOG.md [skip ci] 2021-08-28 09:21:07 -07:00
Rhet Turnbull
159d1102aa Added {strip} template 2021-08-28 08:14:26 -07:00
Rhet Turnbull
dbb4dbc0a7 Fixed --strip behavior, #511 2021-08-28 08:01:08 -07:00
Rhet Turnbull
777e768243 Added selected and quit to repl 2021-08-28 07:23:17 -07:00
Rhet Turnbull
70999a70b8 Updated tutorial template 2021-08-27 23:52:14 -07:00
Rhet Turnbull
3a6b2c2c35 Update test_cli.py 2021-08-23 18:36:35 -07:00
Rhet Turnbull
dfb80ba8d6 Update test_cli.py 2021-08-23 18:30:34 -07:00
Rhet Turnbull
94b818b156 Update test_cli.py 2021-08-23 18:09:36 -07:00
Rhet Turnbull
f1cea1498b Update test for #506 2021-08-23 17:57:28 -07:00
Rhet Turnbull
345678577a Updated test for #506 2021-08-23 17:29:38 -07:00
Rhet Turnbull
fb4138cfe6 Updated README [skip ci] 2021-08-23 14:25:13 -07:00
Rhet Turnbull
db5b34d589 Fix for #506 2021-08-23 14:23:39 -07:00
Rhet Turnbull
8963af9229 Updated CHANGELOG.md [skip ci] 2021-08-15 14:14:51 -07:00
Rhet Turnbull
2041789ff4 Updated README.md [skip ci] 2021-08-15 14:12:15 -07:00
Rhet Turnbull
aec86f93ea Added inspect() to repl, closes #501 2021-08-15 13:50:37 -07:00
Rhet Turnbull
57bfb03e05 Updated CHANGELOG.md [skip ci] 2021-08-02 05:55:19 -07:00
Rhet Turnbull
c2b2476e38 Updated docs for Text Detection [skip ci] 2021-08-02 05:52:48 -07:00
Rhet Turnbull
fa2027d453 Improved caching of detected_text results 2021-08-02 05:10:26 -07:00
Rhet Turnbull
9d980e4917 Updated CHANGELOG.md [skip ci] 2021-07-29 21:27:51 -07:00
106 changed files with 1493 additions and 448 deletions

View File

@@ -241,6 +241,15 @@
"contributions": [
"data"
]
},
{
"login": "dssinger",
"name": "David Singer",
"avatar_url": "https://avatars.githubusercontent.com/u/1817903?v=4",
"profile": "https://github.com/dssinger",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

1
.gitignore vendored
View File

@@ -16,3 +16,4 @@ cli.spec
*.pyc
docsrc/_build/
venv/
.python-version

View File

@@ -4,6 +4,112 @@ 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.42.88](https://github.com/RhetTbull/osxphotos/compare/v0.42.87...v0.42.88)
> 26 September 2021
- Performance fix for #239, owner [`14710e3`](https://github.com/RhetTbull/osxphotos/commit/14710e31789d71b2c948a37722fb6054aca4d85e)
- version bump [`06138e1`](https://github.com/RhetTbull/osxphotos/commit/06138e15d0b87e4865a9ef0cc542303edb44c861)
#### [v0.42.87](https://github.com/RhetTbull/osxphotos/compare/v0.42.86...v0.42.87)
> 26 September 2021
#### [v0.42.86](https://github.com/RhetTbull/osxphotos/compare/v0.42.85...v0.42.86)
> 26 September 2021
- Fix for #517, #239 [`ac47df8`](https://github.com/RhetTbull/osxphotos/commit/ac47df8475762fe8c8f63ad5ffa83b1e20d116b8)
- Fixed formatting [`6adafb8`](https://github.com/RhetTbull/osxphotos/commit/6adafb8ce70e95a9f0bec1a3db6362742fcd1b0d)
- Updated docs [skip ci] [`725f7c8`](https://github.com/RhetTbull/osxphotos/commit/725f7c87351353efeee8c43c3c7f8a95acb14490)
#### [v0.42.85](https://github.com/RhetTbull/osxphotos/compare/v0.42.84...v0.42.85)
> 25 September 2021
- Implemented PhotoInfo.owner, AlbumInfo.owner, #216, #239 [`c4b7c26`](https://github.com/RhetTbull/osxphotos/commit/c4b7c2623f077d9964d5d578ce6c01bb83fab088)
- Updated docs [skip ci] [`59ba325`](https://github.com/RhetTbull/osxphotos/commit/59ba325273b2f16935be944fd46c1237ce637bb8)
#### [v0.42.84](https://github.com/RhetTbull/osxphotos/compare/v0.42.83...v0.42.84)
> 25 September 2021
- Fix for #516 [`e3e1da2`](https://github.com/RhetTbull/osxphotos/commit/e3e1da2fd898896595fc851288f905bd4e2150f8)
- Updated docs [skip ci] [`64c226b`](https://github.com/RhetTbull/osxphotos/commit/64c226b85529581e393a2d0604b41c37a8dc2eaf)
- Update docs [`c429a86`](https://github.com/RhetTbull/osxphotos/commit/c429a860b1ebeb77f3c3e36e9660fc9153d85d11)
#### [v0.42.83](https://github.com/RhetTbull/osxphotos/compare/v0.42.82...v0.42.83)
> 15 September 2021
- Fixed detected_text to use image orientation if available [`dd08c7f`](https://github.com/RhetTbull/osxphotos/commit/dd08c7f701335a7e1e30fda251e6ad20ff781652)
- Added twine [`16335a6`](https://github.com/RhetTbull/osxphotos/commit/16335a6bd66eaa53fd1c390901e2fb028059d8e1)
- Added wheel [`e0f6d8e`](https://github.com/RhetTbull/osxphotos/commit/e0f6d8ecf27fe772b748c7b2f3108558fbc23e8a)
#### [v0.42.82](https://github.com/RhetTbull/osxphotos/compare/v0.42.80...v0.42.82)
> 14 September 2021
- Fix for #515 [`93bf0c2`](https://github.com/RhetTbull/osxphotos/commit/93bf0c210cf01f351611427662025c86955ac373)
- Fix for #515, updated tests [`59c31ff`](https://github.com/RhetTbull/osxphotos/commit/59c31ff88d099b251cf1b571279d7a28a0aac138)
- Updated docs [`773dca8`](https://github.com/RhetTbull/osxphotos/commit/773dca849424c61a7447cb1bb87140708ab0a07c)
#### [v0.42.80](https://github.com/RhetTbull/osxphotos/compare/v0.42.79...v0.42.80)
> 29 August 2021
- Bug fix for null title, #512 [`6bcc676`](https://github.com/RhetTbull/osxphotos/commit/6bcc67634ca50e84494539b8a25eb7925dcede62)
- Updated dependencies [`2eb6e70`](https://github.com/RhetTbull/osxphotos/commit/2eb6e70e57ff1dc79907a29618757953f5871145)
- Updated README [skip ci] [`81dd1a7`](https://github.com/RhetTbull/osxphotos/commit/81dd1a753062dacc83aaf4ce8a7667de2cda599b)
#### [v0.42.79](https://github.com/RhetTbull/osxphotos/compare/v0.42.78...v0.42.79)
> 29 August 2021
#### [v0.42.78](https://github.com/RhetTbull/osxphotos/compare/v0.42.77...v0.42.78)
> 29 August 2021
- docs: add dssinger as a contributor for bug [`#514`](https://github.com/RhetTbull/osxphotos/pull/514)
- Fix for newlines in exif tags, #513 [`f0d7496`](https://github.com/RhetTbull/osxphotos/commit/f0d7496bc66aae291337efc570a2e2c4b9b5529c)
#### [v0.42.77](https://github.com/RhetTbull/osxphotos/compare/v0.42.74...v0.42.77)
> 28 August 2021
- Fixed --strip behavior, #511 [`dbb4dbc`](https://github.com/RhetTbull/osxphotos/commit/dbb4dbc0a7f7cb590ab3b2ce532c5c618c7fc249)
- Update test for #506 [`f1cea14`](https://github.com/RhetTbull/osxphotos/commit/f1cea1498b3b973aa500d874126b9668a8743f1f)
- Added {strip} template [`159d110`](https://github.com/RhetTbull/osxphotos/commit/159d1102aabd56def2caf6754747f7a4caa7d374)
#### [v0.42.74](https://github.com/RhetTbull/osxphotos/compare/v0.42.73...v0.42.74)
> 23 August 2021
- Fix for #506 [`db5b34d`](https://github.com/RhetTbull/osxphotos/commit/db5b34d58950c65f95d22a0e81390b9d4fb7ccd7)
- Updated README [skip ci] [`fb4138c`](https://github.com/RhetTbull/osxphotos/commit/fb4138cfe6cfad02fead821b70b4b84d11b027e9)
#### [v0.42.73](https://github.com/RhetTbull/osxphotos/compare/v0.42.72...v0.42.73)
> 15 August 2021
- Added inspect() to repl, closes #501 [`#501`](https://github.com/RhetTbull/osxphotos/issues/501)
- Updated docs for Text Detection [skip ci] [`c2b2476`](https://github.com/RhetTbull/osxphotos/commit/c2b2476e385fcd3773bd8abb942e788be2af8169)
- Updated README.md [skip ci] [`2041789`](https://github.com/RhetTbull/osxphotos/commit/2041789ff4a3979a73712b27a51a77e8a880efb8)
#### [v0.42.72](https://github.com/RhetTbull/osxphotos/compare/v0.42.71...v0.42.72)
> 2 August 2021
- Improved caching of detected_text results [`fa2027d`](https://github.com/RhetTbull/osxphotos/commit/fa2027d45308738d2335d4b5a72c3ef5c478491a)
#### [v0.42.71](https://github.com/RhetTbull/osxphotos/compare/v0.42.70...v0.42.71)
> 29 July 2021
- Updated text_detection to detect macOS version [`7376223`](https://github.com/RhetTbull/osxphotos/commit/7376223eb87a4919fd54cc685a3f263e83626879)
- Updated detected_text docs to make it clear this only works on Catalina+ [`ecd0b8e`](https://github.com/RhetTbull/osxphotos/commit/ecd0b8e22f8bf1f8d1e98d64834bebf0394dd903)
- Fix for #500, check for macOS version before loading Vision [`673243c`](https://github.com/RhetTbull/osxphotos/commit/673243c6cd1c267b6b741b5429cdb63c062648d1)
#### [v0.42.70](https://github.com/RhetTbull/osxphotos/compare/v0.42.69...v0.42.70)
> 29 July 2021

View File

@@ -2,4 +2,5 @@ include README.md
include README.rst
include osxphotos/templates/*
include osxphotos/phototemplate.tx
include osxphotos/phototemplate.md
include osxphotos/phototemplate.md
include osxphotos/queries/*

View File

@@ -4,7 +4,7 @@
[![tests](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/osxphotos)
[![Downloads](https://static.pepy.tech/personalized-badge/osxphotos?period=month&units=international_system&left_color=black&right_color=brightgreen&left_text=downloads/month)](https://pepy.tech/project/osxphotos)
[![All Contributors](https://img.shields.io/badge/all_contributors-25-orange.svg?style=flat)](#contributors)
[![All Contributors](https://img.shields.io/badge/all_contributors-26-orange.svg?style=flat)](#contributors)
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.
@@ -35,6 +35,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
+ [Raw Photos](#raw-photos)
+ [Template System](#template-system)
+ [ExifTool](#exiftoolExifTool)
+ [Text Detection](#textdetection)
+ [Utility Functions](#utility-functions)
* [Examples](#examples)
* [Related Projects](#related-projects)
@@ -450,15 +451,15 @@ For example, to set Finder comment to the photo's title and description:
In the template string above, `{newline}` instructs osxphotos to insert a new line character ("\n") between the title and description. In this example, if `{title}` or `{descr}` is empty, you'll get "title\n" or "\ndescription" which may not be desired so you can use more advanced features of the template system to handle these cases:
`osxphotos export /path/to/export --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}"`
`osxphotos export /path/to/export --xattr-template findercomment "{title,}{title?{descr?{newline},},}{descr,}"`
Explanation of the template string:
```txt
{title}{title?{descr?{newline},},}{descr}
{title,}{title?{descr?{newline},},}{descr,}
│ │ │ │ │ │ │
│ │ │ │ │ │ │
└──> insert title │ │ │ │ │
└──> insert title (or nothing if no title)
│ │ │ │ │ │
└───> is there a title?
│ │ │ │ │
@@ -470,7 +471,8 @@ Explanation of the template string:
│ │
└───> if title is blank, insert nothing
└───> finally, insert description
└───> finally, insert description
(or nothing if no description)
```
In this example, `title?` demonstrates use of the boolean (True/False) feature of the template system. `title?` is read as "Is the title True (or not blank/empty)? If so, then the value immediately following the `?` is used in place of `title`. If `title` is blank, then the value immediately following the comma is used instead. The format for boolean fields is `field?value if true,value if false`. Either `value if true` or `value if false` may be blank, in which case a blank string ("") is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex `if-then-else` statements.
@@ -1700,7 +1702,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.42.71'
{osxphotos_version} The osxphotos version, e.g. '0.42.89'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for
@@ -1777,6 +1779,8 @@ Substitution Description
rendered TEMPLATE value(s) for safe usage in the
shell, e.g. My file.jpeg => 'My file.jpeg'; only adds
quotes if needed.
{strip} Use in form '{strip,TEMPLATE}'; strips whitespace
from begining and end of rendered TEMPLATE value(s).
{function} Execute a python function from an external file and
use return value as template substitution. Use in
format: {function:file.py::function_name} where
@@ -2369,6 +2373,8 @@ For example, in my library, Photos says I have 19,386 photos and 474 movies. Ho
#### <a name="getphoto">`get_photo(uuid)`</A>
Returns a single PhotoInfo instance for photo with UUID matching `uuid` or None if no photo is found matching `uuid`. If you know the UUID of a photo, `get_photo()` is much faster than `photos`. See also [photos()](#photos).
#### `execute(sql)`
Execute sql statement against the Photos database and return a sqlite cursor with the results.
### PhotoInfo
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
@@ -2512,7 +2518,12 @@ Returns a [PlaceInfo](#PlaceInfo) object with reverse geolocation data or None i
#### `shared`
Returns True if photo is in a shared album, otherwise False.
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None instead of True/False.
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
#### `owner`
Returns full name of the photo owner (person who shared the photo) for shared photos or None if photo is not shared. Also returns None if you are the person who shared the photo.
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
#### `comments`
Returns list of [CommentInfo](#commentinfo) objects for comments on shared photos or empty list if no comments.
@@ -2788,7 +2799,8 @@ Some substitutions, notably `album`, `keyword`, and `person` could return multip
See [Template System](#template-system) for additional details.
#### `detected_text(confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD)`
#### <a name="detected_text_method">`detected_text(confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD)`</a>
Detects text in photo and returns lists of results as (detected text, confidence)
@@ -2800,6 +2812,8 @@ Returns: list of (detected text, confidence) tuples.
Note: This is *not* the same as Live Text in macOS Monterey. When using `detected_text()`, osxphotos will use Apple's [Vision framework](https://developer.apple.com/documentation/vision/recognizing_text_in_images?language=objc) to perform text detection on the image. On my circa 2013 MacBook Pro, this takes about 2 seconds per image. `detected_text()` does memoize the results for a given `confidence_threshold` so repeated calls will not re-process the photo. This works only on macOS Catalina (10.15) or later.
See also [Text Detection](#textdetection).
### ExifInfo
[PhotosInfo.exif_info](#exif-info) returns an `ExifInfo` object with some EXIF data about the photo (Photos 5 only). `ExifInfo` contains the following properties:
@@ -2883,6 +2897,11 @@ Photos Library
#### `parent`
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
#### `owner`
Returns full name of the album owner (person who shared the album) for shared albums or None if album is not shared.
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
### ImportInfo
PhotosDB.import_info returns a list of ImportInfo objects. Each ImportInfo object represents an import session in the library. PhotoInfo.import_info returns a single ImportInfo object representing the import session for the photo (or `None` if no associated import session).
@@ -3554,7 +3573,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.71'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.89'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|
@@ -3571,6 +3590,7 @@ The following template field substitutions are availabe for use the templating s
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|{detected_text}|List of text strings found in the image after performing text detection. Using '{detected_text}' will cause osxphotos to perform text detection on your photos using the built-in macOS text detection algorithms which will slow down your export. The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; The default confidence threshold is 0.75. '{detected_text}' works only on macOS Catalina (10.15) or later. Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.|
|{shell_quote}|Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.|
|{strip}|Use in form '{strip,TEMPLATE}'; strips whitespace from begining and end of rendered TEMPLATE value(s).|
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
@@ -3634,6 +3654,14 @@ osxphotos.exiftool also provides an `ExifToolCaching` class which caches all met
`ExifTool()` runs `exiftool` as a subprocess using the `-stay_open True` flag to keep the process running in the background. The subprocess will be cleaned up when your main script terminates. `ExifTool()` uses a singleton pattern to ensure that only one instance of `exiftool` is created. Multiple instances of `ExifTool()` will all use the same `exiftool` subprocess.
### <a name="textdetection">Text Detection</a>
The [PhotoInfo.detected_text()](#detected_text_method) and the `{detected_text}` template will perform text detection on the photos in your library. Text detection is a slow process so to avoid unnecessary re-processing of photos, osxphotos will cache the results of the text detection process as an extended attribute on the photo image file. Extended attributes do not modify the actual file. The extended attribute is named `osxphotos.metadata:detected_text` and can be viewed using the built-in [xattr](https://ss64.com/osx/xattr.html) command or my [osxmetadata](https://github.com/RhetTbull/osxmetadata) tool. If you want to remove the cached attribute, you can do so with osxmetadata as follows:
`osxmetadata --clear osxphotos.metadata:detected_text --walk ~/Pictures/Photos\ Library.photoslibrary/`
### Utility Functions
The following functions are located in osxphotos.utils
@@ -3712,15 +3740,10 @@ if __name__ == "__main__":
## Related Projects
- [rhettbull/photosmeta](https://github.com/rhettbull/photosmeta): uses osxphotos and [exiftool](https://exiftool.org/) to apply metadata from Photos as exif data in the photo files. Can also export photos while preserving metadata and also apply Photos keywords as spotlight tags to make it easier to search for photos using spotlight. This is mostly made obsolete by osxphotos. The one feature that photosmeta has that osxphotos does not is ability to update the metadata of the actual photo files in the Photos library without exporting them. (Use with caution!)
- [rhettbull/exif2findertags](https://github.com/RhetTbull/exif2findertags): Read EXIF metadata from image and video files and convert it to macOS Finder tags and/or Finder comments and other extended attributes.
- [rhettbull/photos_time_warp](https://github.com/RhetTbull/photos_time_warp): Batch adjust the date, time, or timezone of photos in Apple Photos.
- [rhettbull/PhotoScript](https://github.com/RhetTbull/PhotoScript): python wrapper around Photos' applescript API allowing automation of Photos (including creation/deletion of items) from python.
- [patrikhson/photo-export](https://github.com/patrikhson/photo-export): Exports older versions of Photos databases. Provided the inspiration for osxphotos.
- [doersino/apple-photos-export](https://github.com/doersino/apple-photos-export): Photos export script for Mojave.
- [orangeturtle739/photos-export](https://github.com/orangeturtle739/photos-export): Set of scripts to export Photos libraries.
- [ndbroadbent/icloud_photos_downloader](https://github.com/ndbroadbent/icloud_photos_downloader): Download photos from iCloud. Currently unmaintained.
- [AaronVanGeffen/ExportPhotosLibrary](https://github.com/AaronVanGeffen/ExportPhotosLibrary): Another python script for exporting older versions of Photos libraries.
- [MossieurPropre/PhotosAlbumExporter](https://github.com/MossieurPropre/PhotosAlbumExporter): Javascript script to export photos while maintaining album structure.
- [ajslater/magritte](https://github.com/ajslater/magritte): Another python command line script for exporting photos from older versions of Photos libraries.
- [ndbroadbent/icloud_photos_downloader](https://github.com/ndbroadbent/icloud_photos_downloader): Download photos from iCloud.
## Contributing
@@ -3773,6 +3796,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center"><a href="https://github.com/kaduskj"><img src="https://avatars.githubusercontent.com/u/983067?v=4?s=75" width="75px;" alt=""/><br /><sub><b>kaduskj</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Akaduskj" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt=""/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a> <a href="#example-mkirkland4874" title="Examples">💡</a></td>
<td align="center"><a href="https://github.com/jcommisso07"><img src="https://avatars.githubusercontent.com/u/3111054?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Joseph Commisso</b></sub></a><br /><a href="#data-jcommisso07" title="Data">🔣</a></td>
<td align="center"><a href="https://github.com/dssinger"><img src="https://avatars.githubusercontent.com/u/1817903?v=4?s=75" width="75px;" alt=""/><br /><sub><b>David Singer</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Adssinger" title="Bug reports">🐛</a></td>
</tr>
</table>

View File

@@ -3,9 +3,9 @@
# script to help build osxphotos release
# this is unique to my own dev setup
source venv/bin/activate
# source venv/bin/activate
rm -rf dist; rm -rf build
python3 utils/update_readme.py
(cd docsrc && make github && make pdf)
python3 setup.py sdist bdist_wheel
./make_cli_exe.sh
./make_cli_exe.sh

View File

@@ -1,4 +1,4 @@
# Sphinx build info version 1
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
config: 23e7c9cd300c96ffa7fce04034b83f61
config: bae1c1e83e51e3872ee0fb609c28f878
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.42.69 documentation</title>
<title>Overview: module code &#8212; osxphotos 0.42.89 documentation</title>
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../_static/alabaster.css" />
<script data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
@@ -71,7 +71,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="../search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -93,7 +93,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.42.69 documentation</title>
<title>osxphotos.photoinfo._photoinfo_export &#8212; osxphotos 0.42.84 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
@@ -89,7 +89,7 @@
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">RenderOptions</span>
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span>
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">findfiles</span><span class="p">,</span> <span class="n">lineno</span><span class="p">,</span> <span class="n">noop</span>
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">increment_filename</span><span class="p">,</span> <span class="n">increment_filename_with_count</span><span class="p">,</span> <span class="n">lineno</span>
<span class="c1"># retry if use_photos_export fails the first time (which sometimes it does)</span>
<span class="n">MAX_PHOTOSCRIPT_RETRIES</span> <span class="o">=</span> <span class="mi">3</span>
@@ -563,6 +563,7 @@
<span class="n">preview</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">preview_suffix</span><span class="o">=</span><span class="n">DEFAULT_PREVIEW_SUFFIX</span><span class="p">,</span>
<span class="n">render_options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;export photo, like export but with update and dry_run options</span>
<span class="sd"> dest: must be valid destination path or exception raised</span>
@@ -621,6 +622,7 @@
<span class="sd"> preview: if True, also exports preview image</span>
<span class="sd"> preview_suffix: optional string to append to end of filename for preview images</span>
<span class="sd"> render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates</span>
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
<span class="sd"> Returns: ExportResults class</span>
<span class="sd"> ExportResults has attributes:</span>
@@ -714,15 +716,12 @@
<span class="c1"># e.g. exporting sidecar for file1.png and file1.jpeg</span>
<span class="c1"># if file1.png exists and exporting file1.jpeg,</span>
<span class="c1"># dest will be file1 (1).jpeg even though file1.jpeg doesn&#39;t exist to prevent sidecar collision</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">findfiles</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">*&quot;</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest_original</span><span class="o">.</span><span class="n">parent</span><span class="p">))</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span>
<span class="k">while</span> <span class="n">dest_new</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">dest_original</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}{</span><span class="n">dest_original</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">increment_file_count</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">if</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
<span class="n">dest_original</span><span class="p">,</span> <span class="n">increment_file_count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span>
<span class="n">dest_original</span>
<span class="p">)</span>
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest_original</span><span class="p">)</span>
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
<span class="k">if</span> <span class="p">(</span>
@@ -737,17 +736,11 @@
<span class="p">)</span>
<span class="k">if</span> <span class="n">export_edited</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">findfiles</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_edited</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">*&quot;</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest_edited</span><span class="o">.</span><span class="n">parent</span><span class="p">))</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">stem</span>
<span class="k">if</span> <span class="n">count</span><span class="p">:</span>
<span class="c1"># incremented above when checking original destination</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="k">while</span> <span class="n">dest_new</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}{</span><span class="n">dest_edited</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="k">if</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
<span class="n">dest_edited</span><span class="p">,</span> <span class="n">increment_file_count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span>
<span class="n">dest_edited</span><span class="p">,</span> <span class="n">increment_file_count</span>
<span class="p">)</span>
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest_edited</span><span class="p">)</span>
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
<span class="k">if</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">exists</span><span class="p">()</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">increment</span><span class="p">:</span>
@@ -831,20 +824,16 @@
<span class="p">)</span>
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
<span class="c1"># not the right file, find the right one</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">glob_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (*</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">glob_str</span><span class="p">)</span>
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">False</span>
<span class="k">for</span> <span class="n">file_</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
<span class="n">dest_uuid</span> <span class="o">=</span> <span class="n">export_db</span><span class="o">.</span><span class="n">get_uuid_for_file</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">True</span>
<span class="k">break</span>
<span class="k">elif</span> <span class="n">dest_uuid</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">cmp</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">file_</span><span class="p">):</span>
<span class="c1"># files match, update the UUID</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">True</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_data</span><span class="p">(</span>
<span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="p">,</span>
<span class="n">uuid</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
@@ -856,18 +845,14 @@
<span class="n">exif_json</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">break</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">found_match</span><span class="p">:</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># increment the destination file</span>
<span class="n">count</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">glob_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">*&quot;</span><span class="p">)</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">glob_str</span><span class="p">)</span>
<span class="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">stem</span>
<span class="k">while</span> <span class="n">dest_new</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)&quot;</span>
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}{</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">increment_filename</span><span class="p">(</span><span class="n">dest</span><span class="p">))</span>
<span class="k">if</span> <span class="n">export_original</span><span class="p">:</span>
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">dest</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">dest</span>
<span class="c1"># export the dest file</span>
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
@@ -960,6 +945,7 @@
<span class="n">preview_path</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_derivatives</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="n">preview_ext</span> <span class="o">=</span> <span class="n">preview_path</span><span class="o">.</span><span class="n">suffix</span>
<span class="n">preview_name</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">&quot;</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}{</span><span class="n">preview_suffix</span><span class="si">}{</span><span class="n">preview_ext</span><span class="si">}</span><span class="s2">&quot;</span>
<span class="n">preview_name</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">increment_filename</span><span class="p">(</span><span class="n">preview_name</span><span class="p">))</span>
<span class="k">if</span> <span class="n">preview_path</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
<span class="n">preview_path</span><span class="p">,</span>
@@ -1002,6 +988,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span>
@@ -1028,6 +1015,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span>
@@ -1050,6 +1038,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
<span class="p">(</span>
@@ -1120,6 +1109,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">if</span> <span class="n">old_data</span> <span class="o">!=</span> <span class="n">current_data</span><span class="p">:</span>
@@ -1143,6 +1133,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
@@ -1163,6 +1154,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
@@ -1188,6 +1180,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
@@ -1208,6 +1201,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">),</span>
<span class="p">)</span>
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
@@ -1613,6 +1607,7 @@
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;write exif data to image file at filepath</span>
@@ -1626,6 +1621,7 @@
<span class="sd"> persons: if True, write person data to metadata</span>
<span class="sd"> location: if True, write location data to metadata</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> strip: if True, strip any leading or trailing whitespace from rendered templates</span>
<span class="sd"> Returns:</span>
<span class="sd"> (warning, error) of warning and error strings if exiftool produces warnings or errors</span>
@@ -1643,6 +1639,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">flags</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span>
@@ -1668,6 +1665,7 @@
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
@@ -1684,6 +1682,7 @@
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> strip: if True, strip any rendered templates</span>
<span class="sd"> Returns: dict with exiftool tags / values</span>
@@ -1731,6 +1730,8 @@
<span class="p">)</span>
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">description_template</span><span class="p">,</span> <span class="n">options</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">description</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
<span class="n">description</span> <span class="o">=</span> <span class="n">description</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;EXIF:ImageDescription&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;XMP:Description&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
<span class="n">exif</span><span class="p">[</span><span class="s2">&quot;IPTC:Caption-Abstract&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
@@ -1778,6 +1779,9 @@
<span class="p">)</span>
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">rendered_keywords</span><span class="p">]</span>
<span class="c1"># filter out any template values that didn&#39;t match by looking for sentinel</span>
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">keyword</span>
@@ -1884,12 +1888,6 @@
<span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span>
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">&quot;%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S&quot;</span><span class="p">)</span>
<span class="c1"># remove any new lines in any fields</span>
<span class="k">for</span> <span class="n">field</span><span class="p">,</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">exif</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">str</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="o">=</span> <span class="n">val</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="p">,</span> <span class="s2">&quot; &quot;</span><span class="p">)</span>
<span class="k">elif</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">v</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">&quot;</span><span class="se">\n</span><span class="s2">&quot;</span><span class="p">,</span> <span class="s2">&quot; &quot;</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">val</span> <span class="k">if</span> <span class="n">v</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">]</span>
<span class="k">return</span> <span class="n">exif</span>
@@ -1942,6 +1940,7 @@
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
@@ -1959,6 +1958,7 @@
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
<span class="sd"> Returns: dict with exiftool tags / values</span>
@@ -1998,6 +1998,7 @@
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
<span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span>
@@ -2023,6 +2024,7 @@
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
<span class="p">):</span>
<span class="sd">&quot;&quot;&quot;returns string for XMP sidecar</span>
<span class="sd"> use_albums_as_keywords: treat album names as keywords</span>
@@ -2035,6 +2037,7 @@
<span class="sd"> persons: if True, include person data</span>
<span class="sd"> location: if True, include location data</span>
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it&#39;s additive</span>
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="n">xmp_template_file</span> <span class="o">=</span> <span class="p">(</span>
@@ -2052,6 +2055,8 @@
<span class="p">)</span>
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">description_template</span><span class="p">,</span> <span class="n">options</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">description</span> <span class="o">=</span> <span class="s2">&quot; &quot;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
<span class="n">description</span> <span class="o">=</span> <span class="n">description</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">description</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="s2">&quot;&quot;</span>
@@ -2093,6 +2098,9 @@
<span class="p">)</span>
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">rendered_keywords</span><span class="p">]</span>
<span class="c1"># filter out any template values that didn&#39;t match by looking for sentinel</span>
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">keyword</span>
@@ -2180,7 +2188,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="../../../search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -2202,7 +2210,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.42.69 documentation</title>
<title>osxphotos.photoinfo.photoinfo &#8212; osxphotos 0.42.87 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
@@ -47,6 +47,7 @@
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
<span class="kn">import</span> <span class="nn">yaml</span>
<span class="kn">from</span> <span class="nn">osxmetadata</span> <span class="kn">import</span> <span class="n">OSXMetaData</span>
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">_MOVIE_TYPE</span><span class="p">,</span>
@@ -70,6 +71,7 @@
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span>
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span><span class="p">,</span> <span class="n">RenderOptions</span>
<span class="kn">from</span> <span class="nn">..placeinfo</span> <span class="kn">import</span> <span class="n">PlaceInfo4</span><span class="p">,</span> <span class="n">PlaceInfo5</span>
<span class="kn">from</span> <span class="nn">..query_builder</span> <span class="kn">import</span> <span class="n">get_query</span>
<span class="kn">from</span> <span class="nn">..text_detection</span> <span class="kn">import</span> <span class="n">detect_text</span>
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span><span class="p">,</span> <span class="n">get_uti_for_extension</span>
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">_debug</span><span class="p">,</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">findfiles</span>
@@ -596,7 +598,12 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">title</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;name / title of picture&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;name&quot;</span><span class="p">]</span>
<span class="c1"># if user sets then deletes title, Photos sets it to empty string in DB instead of NULL</span>
<span class="c1"># in this case, return None so result is the same as if title had never been set (which returns NULL)</span>
<span class="c1"># issue #512</span>
<span class="n">title</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;name&quot;</span><span class="p">]</span>
<span class="n">title</span> <span class="o">=</span> <span class="kc">None</span> <span class="k">if</span> <span class="n">title</span> <span class="o">==</span> <span class="s2">&quot;&quot;</span> <span class="k">else</span> <span class="n">title</span>
<span class="k">return</span> <span class="n">title</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -1081,15 +1088,15 @@
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;orientation&quot;</span><span class="p">]</span>
<span class="c1"># For Photos 5+, try to get the adjusted orientation</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">adj_orientation</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># can&#39;t reliably determine orientation for edited photo if adjustmentinfo not available</span>
<span class="k">return</span> <span class="mi">0</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;orientation&quot;</span><span class="p">]</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">adj_orientation</span>
<span class="k">else</span><span class="p">:</span>
<span class="c1"># can&#39;t reliably determine orientation for edited photo if adjustmentinfo not available</span>
<span class="k">return</span> <span class="mi">0</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">original_height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;returns height of the original photo version in pixels&quot;&quot;&quot;</span>
@@ -1125,6 +1132,26 @@
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Did not find signature for </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> in _db_signatures&quot;</span><span class="p">)</span>
<span class="k">return</span> <span class="n">duplicates</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">owner</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Return name of photo owner for shared photos (Photos 5+ only), or None if not shared&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">&lt;=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
<span class="k">return</span> <span class="kc">None</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_owner</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">personid</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">&quot;cloudownerhashedpersonid&quot;</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_owner</span> <span class="o">=</span> <span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_hashed_person_id</span><span class="p">[</span><span class="n">personid</span><span class="p">][</span><span class="s2">&quot;full_name&quot;</span><span class="p">]</span>
<span class="k">if</span> <span class="n">personid</span>
<span class="k">else</span> <span class="kc">None</span>
<span class="p">)</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_owner</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_owner</span>
<div class="viewcode-block" id="PhotoInfo.render_template"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotoInfo.render_template">[docs]</a> <span class="k">def</span> <span class="nf">render_template</span><span class="p">(</span>
<span class="bp">self</span><span class="p">,</span> <span class="n">template_str</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
<span class="p">):</span>
@@ -1151,6 +1178,28 @@
<span class="sd"> Returns: list of (detected text, confidence) tuples</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span>
<span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="ne">AttributeError</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">()</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Error detecting text in photo </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">confidence</span><span class="p">)</span>
<span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">confidence</span> <span class="ow">in</span> <span class="n">detected_text</span>
<span class="k">if</span> <span class="n">confidence</span> <span class="o">&gt;=</span> <span class="n">confidence_threshold</span>
<span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span></div>
<span class="k">def</span> <span class="nf">_detected_text</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;detect text in photo, either from cached extended attribute or by attempting text detection&quot;&quot;&quot;</span>
<span class="n">path</span> <span class="o">=</span> <span class="p">(</span>
<span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span>
<span class="p">)</span>
@@ -1158,23 +1207,13 @@
<span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="p">:</span>
<span class="k">return</span> <span class="p">[]</span>
<span class="k">try</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span>
<span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="ne">AttributeError</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="p">[]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span> <span class="o">=</span> <span class="p">[</span>
<span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">confidence</span><span class="p">)</span>
<span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">confidence</span> <span class="ow">in</span> <span class="n">detected_text</span>
<span class="k">if</span> <span class="n">confidence</span> <span class="o">&gt;=</span> <span class="n">confidence_threshold</span>
<span class="p">]</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span></div>
<span class="n">md</span> <span class="o">=</span> <span class="n">OSXMetaData</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">md</span><span class="o">.</span><span class="n">get_attribute</span><span class="p">(</span><span class="s2">&quot;osxphotos_detected_text&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">detected_text</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
<span class="n">orientation</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">orientation</span> <span class="ow">or</span> <span class="kc">None</span>
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">orientation</span><span class="p">)</span>
<span class="n">md</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">&quot;osxphotos_detected_text&quot;</span><span class="p">,</span> <span class="n">detected_text</span><span class="p">)</span>
<span class="k">return</span> <span class="n">detected_text</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">_longitude</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
@@ -1433,7 +1472,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="../../../search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -1455,7 +1494,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.42.66 documentation</title>
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.42.87 documentation</title>
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.css" />
<script data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
@@ -363,6 +363,8 @@
<span class="k">else</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_process_database5</span><span class="p">()</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_connection</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_db_connection</span><span class="p">()</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">keywords_as_dict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;return keywords as dict of keyword, count in reverse sorted order (descending)&quot;&quot;&quot;</span>
@@ -823,8 +825,8 @@
<span class="s2">&quot;creation_date&quot;</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">8</span><span class="p">],</span>
<span class="s2">&quot;start_date&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
<span class="s2">&quot;end_date&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
<span class="s2">&quot;customsortascending&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
<span class="s2">&quot;customsortkey&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
<span class="s2">&quot;customsortascending&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
<span class="s2">&quot;customsortkey&quot;</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
<span class="p">}</span>
<span class="c1"># get details about folders</span>
@@ -1137,7 +1139,9 @@
<span class="c1"># get info on special types</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;specialType&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">25</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;masterModelID&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">26</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;pk&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">26</span><span class="p">]</span> <span class="c1"># same as masterModelID, to match Photos 5</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;pk&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span>
<span class="mi">26</span>
<span class="p">]</span> <span class="c1"># same as masterModelID, to match Photos 5</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;panorama&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">25</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span> <span class="k">else</span> <span class="kc">False</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;slow_mo&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">25</span><span class="p">]</span> <span class="o">==</span> <span class="mi">2</span> <span class="k">else</span> <span class="kc">False</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;time_lapse&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">25</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span> <span class="k">else</span> <span class="kc">False</span>
@@ -1228,6 +1232,9 @@
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;import_uuid&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">44</span><span class="p">]</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;fok_import_session&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
<span class="c1"># photos 5+ only, for shared photos</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;cloudownerhashedpersonid&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
<span class="c1"># compute signatures for finding possible duplicates</span>
<span class="n">signature</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_duplicate_signature</span><span class="p">(</span><span class="n">uuid</span><span class="p">)</span>
<span class="k">try</span><span class="p">:</span>
@@ -1956,7 +1963,8 @@
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZTRASHEDDATE,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZSAVEDASSETTYPE,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZADDEDDATE,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.Z_PK</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.Z_PK,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZCLOUDOWNERHASHEDPERSONID</span>
<span class="s2"> FROM </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2"> </span>
<span class="s2"> JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.Z_PK </span>
<span class="s2"> ORDER BY </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID &quot;&quot;&quot;</span>
@@ -2006,6 +2014,7 @@
<span class="c1"># 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported</span>
<span class="c1"># 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library</span>
<span class="c1"># 42 ZGENERICASSET.Z_PK -- primary key</span>
<span class="c1"># 43 ZGENERICASSET.ZCLOUDOWNERHASHEDPERSONID -- used to look up owner name (for shared photos)</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
@@ -2191,6 +2200,7 @@
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;added_date&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">(</span><span class="mi">1970</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;pk&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">42</span><span class="p">]</span>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;cloudownerhashedpersonid&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">43</span><span class="p">]</span>
<span class="c1"># initialize import session info which will be filled in later</span>
<span class="c1"># not every photo has an import session so initialize all records now</span>
@@ -3387,6 +3397,10 @@
<span class="k">return</span> <span class="n">photos</span></div>
<div class="viewcode-block" id="PhotosDB.execute"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.execute">[docs]</a> <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">sql</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Execute sql statement and return cursor&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql</span><span class="p">)</span></div>
<span class="k">def</span> <span class="nf">_duplicate_signature</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Compute a signature for finding possible duplicates&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="p">(</span>
@@ -3412,7 +3426,11 @@
<span class="sd">&quot;&quot;&quot;Returns number of photos in the database</span>
<span class="sd"> Includes recently deleted photos and non-selected burst images</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">)</span></div>
<span class="k">return</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">)</span>
<span class="k">def</span> <span class="fm">__del__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">if</span> <span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">&quot;_db_connection&quot;</span><span class="p">,</span> <span class="kc">None</span><span class="p">):</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_connection</span><span class="o">.</span><span class="n">close</span><span class="p">()</span></div>
<span class="k">def</span> <span class="nf">_get_photos_by_attribute</span><span class="p">(</span><span class="n">photos</span><span class="p">,</span> <span class="n">attribute</span><span class="p">,</span> <span class="n">values</span><span class="p">,</span> <span class="n">ignore_case</span><span class="p">):</span>
@@ -3477,7 +3495,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="../../../search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -3499,7 +3517,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

View File

@@ -819,7 +819,7 @@ div.code-block-caption code {
table.highlighttable td.linenos,
span.linenos,
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
-webkit-user-select: text; /* Safari fallback only */
-webkit-user-select: none; /* Chrome/Safari */

View File

@@ -301,12 +301,14 @@ var Documentation = {
window.location.href = prevHref;
return false;
}
break;
case 39: // right
var nextHref = $('link[rel="next"]').prop('href');
if (nextHref) {
window.location.href = nextHref;
return false;
}
break;
}
}
});

View File

@@ -1,6 +1,6 @@
var DOCUMENTATION_OPTIONS = {
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
VERSION: '0.42.69',
VERSION: '0.42.89',
LANGUAGE: 'None',
COLLAPSE_INDEX: false,
BUILDER: 'html',

View File

@@ -282,7 +282,10 @@ var Search = {
complete: function(jqxhr, textstatus) {
var data = jqxhr.responseText;
if (data !== '' && data !== undefined) {
listItem.append(Search.makeSearchSummary(data, searchterms, hlterms));
var summary = Search.makeSearchSummary(data, searchterms, hlterms);
if (summary) {
listItem.append(summary);
}
}
Search.output.append(listItem);
setTimeout(function() {
@@ -498,6 +501,9 @@ var Search = {
*/
makeSearchSummary : function(htmlText, keywords, hlwords) {
var text = Search.htmlToText(htmlText);
if (text == "") {
return null;
}
var textLower = text.toLowerCase();
var start = 0;
$.each(keywords, function() {

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.42.69 documentation</title>
<title>osxphotos command line interface (CLI) &#8212; osxphotos 0.42.89 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -1644,7 +1644,7 @@ if more than one option is provided, they are treated as “AND”
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -1666,7 +1666,7 @@ if more than one option is provided, they are treated as “AND”
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Index &#8212; osxphotos 0.42.69 documentation</title>
<title>Index &#8212; osxphotos 0.42.89 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -1333,14 +1333,16 @@
<h2 id="E">E</h2>
<table style="width: 100%" class="indextable genindextable"><tr>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotosDB.execute">execute() (osxphotos.PhotosDB method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.exif_info">exif_info (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.exiftool">exiftool (osxphotos.PhotoInfo property)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.export">export() (osxphotos.PhotoInfo method)</a>
</li>
</ul></td>
<td style="width: 33%; vertical-align: top;"><ul>
<li><a href="reference.html#osxphotos.PhotoInfo.export">export() (osxphotos.PhotoInfo method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.export2">export2() (osxphotos.PhotoInfo method)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.ExifInfo.exposure_bias">exposure_bias (osxphotos.PhotoInfo.ExifInfo attribute)</a>
@@ -2112,6 +2114,8 @@
</li>
</ul></li>
<li><a href="reference.html#osxphotos.PhotoInfo.ScoreInfo.overall">overall (osxphotos.PhotoInfo.ScoreInfo attribute)</a>
</li>
<li><a href="reference.html#osxphotos.PhotoInfo.owner">owner (osxphotos.PhotoInfo property)</a>
</li>
</ul></td>
</tr></table>
@@ -2395,7 +2399,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -2417,7 +2421,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.42.69 documentation</title>
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.42.89 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -351,7 +351,7 @@ Alternatively, you can also run the command line utility like this: <code class=
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -373,7 +373,7 @@ Alternatively, you can also run the command line utility like this: <code class=
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos &#8212; osxphotos 0.42.69 documentation</title>
<title>osxphotos &#8212; osxphotos 0.42.89 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -69,7 +69,7 @@
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -91,7 +91,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|

Binary file not shown.

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos package &#8212; osxphotos 0.42.69 documentation</title>
<title>osxphotos package &#8212; osxphotos 0.42.89 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
@@ -90,6 +90,12 @@ valid only on Photos 5; on Photos &lt;= 4, prints warning and returns empty dict
<dd><p>return the database version as stored in LiGlobals table</p>
</dd></dl>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.execute">
<span class="sig-name descname"><span class="pre">execute</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">sql</span></span></em><span class="sig-paren">)</span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB.execute"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB.execute" title="Permalink to this definition"></a></dt>
<dd><p>Execute sql statement and return cursor</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.folder_info">
<em class="property"><span class="pre">property</span> </em><span class="sig-name descname"><span class="pre">folder_info</span></span><a class="headerlink" href="#osxphotos.PhotosDB.folder_info" title="Permalink to this definition"></a></dt>
@@ -256,7 +262,7 @@ Returns photos regardless of intrash state.</p>
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotosDB.query">
<span class="sig-name descname"><span class="pre">query</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">options</span></span><span class="p"><span class="pre">:</span></span> <span class="n"><span class="pre">osxphotos.queryoptions.QueryOptions</span></span></em><span class="sig-paren">)</span> &#x2192; <span class="pre">List</span><span class="p"><span class="pre">[</span></span><a class="reference internal" href="#osxphotos.PhotoInfo" title="osxphotos.photoinfo.photoinfo.PhotoInfo"><span class="pre">osxphotos.photoinfo.photoinfo.PhotoInfo</span></a><span class="p"><span class="pre">]</span></span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB.query"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB.query" title="Permalink to this definition"></a></dt>
<span class="sig-name descname"><span class="pre">query</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">options</span></span><span class="p"><span class="pre">:</span></span> <span class="n"><span class="pre">osxphotos.queryoptions.QueryOptions</span></span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">&#x2192;</span> <span class="sig-return-typehint"><span class="pre">List</span><span class="p"><span class="pre">[</span></span><a class="reference internal" href="#osxphotos.PhotoInfo" title="osxphotos.photoinfo.photoinfo.PhotoInfo"><span class="pre">osxphotos.photoinfo.photoinfo.PhotoInfo</span></a><span class="p"><span class="pre">]</span></span></span></span><a class="reference internal" href="_modules/osxphotos/photosdb/photosdb.html#PhotosDB.query"><span class="viewcode-link"><span class="pre">[source]</span></span></a><a class="headerlink" href="#osxphotos.PhotosDB.query" title="Permalink to this definition"></a></dt>
<dd><p>Run a query against PhotosDB to extract the photos based on user supplied options</p>
<dl class="field-list simple">
<dt class="field-odd">Parameters</dt>
@@ -845,7 +851,7 @@ render_options: an optional osxphotos.phototemplate.RenderOptions instance with
<dl class="py method">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.export2">
<span class="sig-name descname"><span class="pre">export2</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">dest</span></em>, <em class="sig-param"><span class="pre">original=True</span></em>, <em class="sig-param"><span class="pre">original_filename=None</span></em>, <em class="sig-param"><span class="pre">edited=False</span></em>, <em class="sig-param"><span class="pre">edited_filename=None</span></em>, <em class="sig-param"><span class="pre">live_photo=False</span></em>, <em class="sig-param"><span class="pre">raw_photo=False</span></em>, <em class="sig-param"><span class="pre">export_as_hardlink=False</span></em>, <em class="sig-param"><span class="pre">overwrite=False</span></em>, <em class="sig-param"><span class="pre">increment=True</span></em>, <em class="sig-param"><span class="pre">sidecar=0</span></em>, <em class="sig-param"><span class="pre">sidecar_drop_ext=False</span></em>, <em class="sig-param"><span class="pre">use_photos_export=False</span></em>, <em class="sig-param"><span class="pre">timeout=120</span></em>, <em class="sig-param"><span class="pre">exiftool=False</span></em>, <em class="sig-param"><span class="pre">use_albums_as_keywords=False</span></em>, <em class="sig-param"><span class="pre">use_persons_as_keywords=False</span></em>, <em class="sig-param"><span class="pre">keyword_template=None</span></em>, <em class="sig-param"><span class="pre">description_template=None</span></em>, <em class="sig-param"><span class="pre">update=False</span></em>, <em class="sig-param"><span class="pre">ignore_signature=False</span></em>, <em class="sig-param"><span class="pre">export_db=None</span></em>, <em class="sig-param"><span class="pre">fileutil=&lt;class</span> <span class="pre">'osxphotos.fileutil.FileUtil'&gt;</span></em>, <em class="sig-param"><span class="pre">dry_run=False</span></em>, <em class="sig-param"><span class="pre">touch_file=False</span></em>, <em class="sig-param"><span class="pre">convert_to_jpeg=False</span></em>, <em class="sig-param"><span class="pre">jpeg_quality=1.0</span></em>, <em class="sig-param"><span class="pre">ignore_date_modified=False</span></em>, <em class="sig-param"><span class="pre">use_photokit=False</span></em>, <em class="sig-param"><span class="pre">verbose=None</span></em>, <em class="sig-param"><span class="pre">exiftool_flags=None</span></em>, <em class="sig-param"><span class="pre">merge_exif_keywords=False</span></em>, <em class="sig-param"><span class="pre">merge_exif_persons=False</span></em>, <em class="sig-param"><span class="pre">jpeg_ext=None</span></em>, <em class="sig-param"><span class="pre">persons=True</span></em>, <em class="sig-param"><span class="pre">location=True</span></em>, <em class="sig-param"><span class="pre">replace_keywords=False</span></em>, <em class="sig-param"><span class="pre">preview=False</span></em>, <em class="sig-param"><span class="pre">preview_suffix='_preview'</span></em>, <em class="sig-param"><span class="pre">render_options:</span> <span class="pre">Optional[osxphotos.phototemplate.RenderOptions]</span> <span class="pre">=</span> <span class="pre">None</span></em><span class="sig-paren">)</span><a class="headerlink" href="#osxphotos.PhotoInfo.export2" title="Permalink to this definition"></a></dt>
<span class="sig-name descname"><span class="pre">export2</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="pre">dest</span></em>, <em class="sig-param"><span class="pre">original=True</span></em>, <em class="sig-param"><span class="pre">original_filename=None</span></em>, <em class="sig-param"><span class="pre">edited=False</span></em>, <em class="sig-param"><span class="pre">edited_filename=None</span></em>, <em class="sig-param"><span class="pre">live_photo=False</span></em>, <em class="sig-param"><span class="pre">raw_photo=False</span></em>, <em class="sig-param"><span class="pre">export_as_hardlink=False</span></em>, <em class="sig-param"><span class="pre">overwrite=False</span></em>, <em class="sig-param"><span class="pre">increment=True</span></em>, <em class="sig-param"><span class="pre">sidecar=0</span></em>, <em class="sig-param"><span class="pre">sidecar_drop_ext=False</span></em>, <em class="sig-param"><span class="pre">use_photos_export=False</span></em>, <em class="sig-param"><span class="pre">timeout=120</span></em>, <em class="sig-param"><span class="pre">exiftool=False</span></em>, <em class="sig-param"><span class="pre">use_albums_as_keywords=False</span></em>, <em class="sig-param"><span class="pre">use_persons_as_keywords=False</span></em>, <em class="sig-param"><span class="pre">keyword_template=None</span></em>, <em class="sig-param"><span class="pre">description_template=None</span></em>, <em class="sig-param"><span class="pre">update=False</span></em>, <em class="sig-param"><span class="pre">ignore_signature=False</span></em>, <em class="sig-param"><span class="pre">export_db=None</span></em>, <em class="sig-param"><span class="pre">fileutil=&lt;class</span> <span class="pre">'osxphotos.fileutil.FileUtil'&gt;</span></em>, <em class="sig-param"><span class="pre">dry_run=False</span></em>, <em class="sig-param"><span class="pre">touch_file=False</span></em>, <em class="sig-param"><span class="pre">convert_to_jpeg=False</span></em>, <em class="sig-param"><span class="pre">jpeg_quality=1.0</span></em>, <em class="sig-param"><span class="pre">ignore_date_modified=False</span></em>, <em class="sig-param"><span class="pre">use_photokit=False</span></em>, <em class="sig-param"><span class="pre">verbose=None</span></em>, <em class="sig-param"><span class="pre">exiftool_flags=None</span></em>, <em class="sig-param"><span class="pre">merge_exif_keywords=False</span></em>, <em class="sig-param"><span class="pre">merge_exif_persons=False</span></em>, <em class="sig-param"><span class="pre">jpeg_ext=None</span></em>, <em class="sig-param"><span class="pre">persons=True</span></em>, <em class="sig-param"><span class="pre">location=True</span></em>, <em class="sig-param"><span class="pre">replace_keywords=False</span></em>, <em class="sig-param"><span class="pre">preview=False</span></em>, <em class="sig-param"><span class="pre">preview_suffix='_preview'</span></em>, <em class="sig-param"><span class="pre">render_options:</span> <span class="pre">Optional[osxphotos.phototemplate.RenderOptions]</span> <span class="pre">=</span> <span class="pre">None</span></em>, <em class="sig-param"><span class="pre">strip=False</span></em><span class="sig-paren">)</span><a class="headerlink" href="#osxphotos.PhotoInfo.export2" title="Permalink to this definition"></a></dt>
<dd><p>export photo, like export but with update and dry_run options
dest: must be valid destination path or exception raised
filename: (optional): name of exported picture; if not provided, will use current filename</p>
@@ -914,7 +920,8 @@ location: if True, include location in exported metadata
replace_keywords: if True, keyword_template replaces any keywords, otherwise its additive
preview: if True, also exports preview image
preview_suffix: optional string to append to end of filename for preview images
render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates</p>
render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
strip: if True, strip whitespace from rendered templates</p>
<dl class="simple">
<dt>Returns: ExportResults class</dt><dd><p>ExportResults has attributes:
“exported”,
@@ -1142,6 +1149,12 @@ Photos 5 mangles filenames upon import</p>
<dd><p>returns width of the original photo version in pixels</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.owner">
<em class="property"><span class="pre">property</span> </em><span class="sig-name descname"><span class="pre">owner</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.owner" title="Permalink to this definition"></a></dt>
<dd><p>Return name of photo owner for shared photos (Photos 5+ only), or None if not shared</p>
</dd></dl>
<dl class="py property">
<dt class="sig sig-object py" id="osxphotos.PhotoInfo.panorama">
<em class="property"><span class="pre">property</span> </em><span class="sig-name descname"><span class="pre">panorama</span></span><a class="headerlink" href="#osxphotos.PhotoInfo.panorama" title="Permalink to this definition"></a></dt>
@@ -1396,7 +1409,7 @@ Returns None if no associated RAW image</p>
<h3 id="searchlabel">Quick search</h3>
<div class="searchformwrapper">
<form class="search" action="search.html" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" />
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="Go" />
</form>
</div>
@@ -1418,7 +1431,7 @@ Returns None if no associated RAW image</p>
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|

View File

@@ -5,7 +5,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.42.69 documentation</title>
<title>Search &#8212; osxphotos 0.42.89 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
@@ -38,13 +38,14 @@
<h1 id="search-documentation">Search</h1>
<div id="fallback" class="admonition warning">
<script>$('#fallback').hide();</script>
<noscript>
<div class="admonition warning">
<p>
Please activate JavaScript to enable the search
functionality.
</p>
</div>
</noscript>
<p>
@@ -54,7 +55,7 @@
<form action="" method="get">
<input type="text" name="q" aria-labelledby="search-documentation" value="" />
<input type="text" name="q" aria-labelledby="search-documentation" value="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="search" />
<span id="search-progress" style="padding-left: 10px"></span>
</form>
@@ -110,7 +111,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

File diff suppressed because one or more lines are too long

View File

@@ -94,6 +94,7 @@ _TESTED_OS_VERSIONS = [
("11", "2"),
("11", "3"),
("11", "4"),
("11", "5"),
]
# Photos 5 has persons who are empty string if unidentified face

View File

@@ -1,3 +1,4 @@
""" version info """
__version__ = "0.42.71"
__version__ = "0.42.89"

View File

@@ -22,6 +22,7 @@ from ._constants import (
AlbumSortOrder,
)
from .datetime_utils import get_local_tz
from .query_builder import get_query
def sort_list_by_keys(values, sort_keys):
@@ -131,6 +132,28 @@ class AlbumInfoBaseClass:
def photos(self):
return []
@property
def owner(self):
"""Return name of photo owner for shared album (Photos 5+ only), or None if not shared"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return None
try:
return self._owner
except AttributeError:
try:
personid = self._db._dbalbum_details[self.uuid][
"cloudownerhashedpersonid"
]
self._owner = (
self._db._db_hashed_person_id[personid]["full_name"]
if personid
else None
)
except KeyError:
self._owner = None
return self._owner
def __len__(self):
"""return number of photos contained in album"""
return len(self.photos)

View File

@@ -2784,9 +2784,7 @@ def _render_suffix_template(
return ""
try:
options = RenderOptions(
filename=True, strip=strip, export_dir=dest, exportdb=export_db
)
options = RenderOptions(filename=True, export_dir=dest, exportdb=export_db)
rendered_suffix, unmatched = photo.render_template(suffix_template, options)
except ValueError as e:
raise click.BadOptionUsage(
@@ -2803,6 +2801,10 @@ def _render_suffix_template(
var_name,
f"Invalid template for {option_name}: may not use multi-valued templates: '{suffix_template}': results={rendered_suffix}",
)
if strip:
rendered_suffix[0] = rendered_suffix[0].strip()
return rendered_suffix[0]
@@ -3033,7 +3035,6 @@ def get_filenames_from_template(
options = RenderOptions(
path_sep="_",
filename=True,
strip=strip,
edited_version=edited,
export_dir=export_dir,
dest_path=dest_path,
@@ -3057,7 +3058,10 @@ def get_filenames_from_template(
else [photo.filename]
)
if strip:
filenames = [filename.strip() for filename in filenames]
filenames = [sanitize_filename(filename) for filename in filenames]
return filenames
@@ -3101,7 +3105,7 @@ def get_dirnames_from_template(
# got a directory template, render it and check results are valid
try:
options = RenderOptions(
dirname=True, strip=strip, edited_version=edited, exportdb=export_db
dirname=True, edited_version=edited, exportdb=export_db
)
dirnames, unmatched = photo.render_template(directory, options)
except ValueError as e:
@@ -3116,6 +3120,8 @@ def get_dirnames_from_template(
dest_paths = []
for dirname in dirnames:
if strip:
dirname = dirname.strip()
dirname = sanitize_filepath(dirname)
dest_path = os.path.join(dest, dirname)
if not is_valid_filepath(dest_path):
@@ -3429,7 +3435,6 @@ def write_finder_tags(
options = RenderOptions(
none_str=_OSXPHOTOS_NONE_SENTINEL,
path_sep="/",
strip=strip,
export_dir=export_dir,
exportdb=export_db,
)
@@ -3451,6 +3456,9 @@ def write_finder_tags(
rendered_tags.extend(rendered)
# filter out any template values that didn't match by looking for sentinel
if strip:
rendered_tags = [value.strip() for value in rendered_tags]
rendered_tags = [
value.replace(_OSXPHOTOS_NONE_SENTINEL, "") for value in rendered_tags
]
@@ -3496,7 +3504,6 @@ def write_extended_attributes(
options = RenderOptions(
none_str=_OSXPHOTOS_NONE_SENTINEL,
path_sep="/",
strip=strip,
export_dir=export_dir,
exportdb=export_db,
)
@@ -3516,6 +3523,9 @@ def write_extended_attributes(
)
# filter out any template values that didn't match by looking for sentinel
if strip:
rendered = [value.strip() for value in rendered]
rendered = [value.replace(_OSXPHOTOS_NONE_SENTINEL, "") for value in rendered]
try:
@@ -4074,6 +4084,10 @@ def _get_selected(photosdb):
@click.pass_context
def repl(ctx, cli_obj, db):
"""Run interactive osxphotos shell"""
from osxphotos import PhotosDB, PhotoInfo, ExifTool
from rich import inspect as _inspect
pretty.install()
print(f"python version: {sys.version}")
print(f"osxphotos version: {osxphotos._version.__version__}")
@@ -4089,12 +4103,34 @@ def repl(ctx, cli_obj, db):
get_photo = photosdb.get_photo
show = _show_photo
get_selected = _get_selected(photosdb)
try:
selected = get_selected()
except Exception:
# get_selected sometimes fails
selected = []
def inspect(obj):
"""inspect object"""
return _inspect(obj, methods=True)
class ReprQuit:
def __repr__(self):
sys.exit(0)
def __call__(self):
sys.exit(0)
quit = ReprQuit()
q = ReprQuit()
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds")
print("The following variables are defined:")
print(f"- photosdb: PhotosDB() instance for {photosdb.library_path}")
print(
f"- photos: list of PhotoInfo objects for all photos in photosdb, including those in the trash"
f"- photos: list of PhotoInfo objects for all photos in photosdb, including those in the trash (len={len(photos)})"
)
print(
f"- selected: list of PhotoInfo objects for any photos selected in Photos (len={len(selected)})"
)
print(f"\nThe following functions may be helpful:")
print(f"- get_photo(uuid): return a PhotoInfo object for photo with uuid")
@@ -4105,5 +4141,8 @@ def repl(ctx, cli_obj, db):
print(
f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
)
print(f"- quit(): exit this interactive shell\n")
print(
f"- inspect(object): print information about an object; for example inspect(photosdb)"
)
print(f"- q, quit, or quit(): exit this interactive shell\n")
code.interact(banner="", local=locals())

View File

@@ -7,6 +7,7 @@
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
import atexit
import html
import json
import logging
import os
@@ -24,16 +25,34 @@ EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
EXIFTOOL_PROCESSES = []
def escape_str(s):
"""escape string for use with exiftool -E"""
if type(s) != str:
return s
s = html.escape(s)
s = s.replace("\n", "&#xa;")
s = s.replace("\t", "&#x9;")
s = s.replace("\r", "&#xd;")
return s
def unescape_str(s):
"""unescape an HTML string returned by exiftool -E"""
if type(s) != str:
return s
return html.unescape(s)
@atexit.register
def terminate_exiftool():
"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool """
"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool"""
for proc in EXIFTOOL_PROCESSES:
proc._stop_proc()
@lru_cache(maxsize=1)
def get_exiftool_path():
""" return path of exiftool, cache result """
"""return path of exiftool, cache result"""
exiftool_path = shutil.which("exiftool")
if exiftool_path:
return exiftool_path.rstrip()
@@ -49,7 +68,7 @@ class _ExifToolProc:
Creates a singleton object"""
def __new__(cls, *args, **kwargs):
""" create new object or return instance of already created singleton """
"""create new object or return instance of already created singleton"""
if not hasattr(cls, "instance") or not cls.instance:
cls.instance = super().__new__(cls)
@@ -74,7 +93,7 @@ class _ExifToolProc:
@property
def process(self):
""" return the exiftool subprocess """
"""return the exiftool subprocess"""
if self._process_running:
return self._process
else:
@@ -83,16 +102,16 @@ class _ExifToolProc:
@property
def pid(self):
""" return process id (PID) of the exiftool process """
"""return process id (PID) of the exiftool process"""
return self._process.pid
@property
def exiftool(self):
""" return path to exiftool process """
"""return path to exiftool process"""
return self._exiftool
def _start_proc(self):
""" start exiftool in batch mode """
"""start exiftool in batch mode"""
if self._process_running:
logging.warning("exiftool already running: {self._process}")
@@ -110,6 +129,7 @@ class _ExifToolProc:
"-n", # no print conversion (e.g. print tag values in machine readable format)
"-P", # Preserve file modification date/time
"-G", # print group name for each tag
"-E", # escape tag values for HTML (allows use of HTML &#xa; for newlines)
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
@@ -120,7 +140,7 @@ class _ExifToolProc:
EXIFTOOL_PROCESSES.append(self)
def _stop_proc(self):
""" stop the exiftool process if it's running, otherwise, do nothing """
"""stop the exiftool process if it's running, otherwise, do nothing"""
if not self._process_running:
return
@@ -143,7 +163,7 @@ class _ExifToolProc:
class ExifTool:
""" Basic exiftool interface for reading and writing EXIF tags """
"""Basic exiftool interface for reading and writing EXIF tags"""
def __init__(self, filepath, exiftool=None, overwrite=True, flags=None):
"""Create ExifTool object
@@ -189,6 +209,7 @@ class ExifTool:
if value is None:
value = ""
value = escape_str(value)
command = [f"-{tag}={value}"]
if self.overwrite and not self._context_mgr:
command.append("-overwrite_original")
@@ -233,6 +254,7 @@ class ExifTool:
for value in values:
if value is None:
raise ValueError("Can't add None value to tag")
value = escape_str(value)
command.append(f"-{tag}+={value}")
if self.overwrite and not self._context_mgr:
@@ -315,12 +337,12 @@ class ExifTool:
@property
def pid(self):
""" return process id (PID) of the exiftool process """
"""return process id (PID) of the exiftool process"""
return self._process.pid
@property
def version(self):
""" returns exiftool version """
"""returns exiftool version"""
ver, _, _ = self.run_commands("-ver", no_file=True)
return ver.decode("utf-8")
@@ -335,6 +357,7 @@ class ExifTool:
json_str, _, _ = self.run_commands("-json")
if not json_str:
return dict()
json_str = unescape_str(json_str.decode("utf-8"))
try:
exifdict = json.loads(json_str)
@@ -342,7 +365,6 @@ class ExifTool:
# will fail with some commands, e.g --ext AVI which produces
# 'No file with specified extension' instead of json
return dict()
exifdict = exifdict[0]
if not tag_groups:
# strip tag groups
@@ -358,12 +380,13 @@ class ExifTool:
return exifdict
def json(self):
""" returns JSON string containing all EXIF tags and values from exiftool """
"""returns JSON string containing all EXIF tags and values from exiftool"""
json, _, _ = self.run_commands("-json")
json = unescape_str(json.decode("utf-8"))
return json
def _read_exif(self):
""" read exif data from file """
"""read exif data from file"""
data = self.asdict()
self.data = {k: v for k, v in data.items()}
@@ -384,15 +407,15 @@ class ExifTool:
class ExifToolCaching(ExifTool):
""" Basic exiftool interface for reading and writing EXIF tags, with caching.
Use this only when you know the file's EXIF data will not be changed by any external process.
Creates a singleton cached ExifTool instance """
"""Basic exiftool interface for reading and writing EXIF tags, with caching.
Use this only when you know the file's EXIF data will not be changed by any external process.
Creates a singleton cached ExifTool instance"""
_singletons = {}
def __new__(cls, filepath, exiftool=None):
""" create new object or return instance of already created singleton """
"""create new object or return instance of already created singleton"""
if filepath not in cls._singletons:
cls._singletons[filepath] = _ExifToolCaching(filepath, exiftool=exiftool)
return cls._singletons[filepath]
@@ -448,7 +471,6 @@ class _ExifToolCaching(ExifTool):
return self._asdict_cache[tag_groups][normalized]
def flush_cache(self):
""" Clear cached data so that calls to json or asdict return fresh data """
"""Clear cached data so that calls to json or asdict return fresh data"""
self._json_cache = None
self._asdict_cache = {}

View File

@@ -56,7 +56,7 @@ from ..photokit import (
)
from ..phototemplate import RenderOptions
from ..uti import get_preferred_uti_extension
from ..utils import findfiles, lineno, noop
from ..utils import increment_filename, increment_filename_with_count, lineno
# retry if use_photos_export fails the first time (which sometimes it does)
MAX_PHOTOSCRIPT_RETRIES = 3
@@ -530,6 +530,7 @@ def export2(
preview=False,
preview_suffix=DEFAULT_PREVIEW_SUFFIX,
render_options: Optional[RenderOptions] = None,
strip=False,
):
"""export photo, like export but with update and dry_run options
dest: must be valid destination path or exception raised
@@ -588,6 +589,7 @@ def export2(
preview: if True, also exports preview image
preview_suffix: optional string to append to end of filename for preview images
render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
strip: if True, strip whitespace from rendered templates
Returns: ExportResults class
ExportResults has attributes:
@@ -681,15 +683,12 @@ def export2(
# e.g. exporting sidecar for file1.png and file1.jpeg
# if file1.png exists and exporting file1.jpeg,
# dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision
count = 0
if not update and increment and not overwrite:
dest_files = findfiles(f"{dest_original.stem}*", str(dest_original.parent))
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
dest_new = dest_original.stem
while dest_new.lower() in dest_files:
count += 1
dest_new = f"{dest_original.stem} ({count})"
dest_original = dest_original.parent / f"{dest_new}{dest_original.suffix}"
increment_file_count = 0
if increment and not update and not overwrite:
dest_original, increment_file_count = increment_filename_with_count(
dest_original
)
dest_original = pathlib.Path(dest_original)
# if overwrite==False and #increment==False, export should fail if file exists
if (
@@ -704,17 +703,11 @@ def export2(
)
if export_edited:
if not update and increment and not overwrite:
dest_files = findfiles(f"{dest_edited.stem}*", str(dest_edited.parent))
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
dest_new = dest_edited.stem
if count:
# incremented above when checking original destination
dest_new = f"{dest_new} ({count})"
while dest_new.lower() in dest_files:
count += 1
dest_new = f"{dest.stem} ({count})"
dest_edited = dest_edited.parent / f"{dest_new}{dest_edited.suffix}"
if increment and not update and not overwrite:
dest_edited, increment_file_count = increment_filename_with_count(
dest_edited, increment_file_count
)
dest_edited = pathlib.Path(dest_edited)
# if overwrite==False and #increment==False, export should fail if file exists
if dest_edited.exists() and not update and not overwrite and not increment:
@@ -798,20 +791,16 @@ def export2(
)
if dest_uuid != self.uuid:
# not the right file, find the right one
count = 1
glob_str = str(dest.parent / f"{dest.stem} (*{dest.suffix}")
dest_files = glob.glob(glob_str)
found_match = False
for file_ in dest_files:
dest_uuid = export_db.get_uuid_for_file(file_)
if dest_uuid == self.uuid:
dest = pathlib.Path(file_)
found_match = True
break
elif dest_uuid is None and fileutil.cmp(src, file_):
# files match, update the UUID
dest = pathlib.Path(file_)
found_match = True
export_db.set_data(
filename=dest,
uuid=self.uuid,
@@ -823,18 +812,14 @@ def export2(
exif_json=None,
)
break
if not found_match:
else:
# increment the destination file
count = 1
glob_str = str(dest.parent / f"{dest.stem}*")
dest_files = glob.glob(glob_str)
dest_files = [pathlib.Path(f).stem for f in dest_files]
dest_new = dest.stem
while dest_new in dest_files:
dest_new = f"{dest.stem} ({count})"
count += 1
dest = dest.parent / f"{dest_new}{dest.suffix}"
dest = pathlib.Path(increment_filename(dest))
if export_original:
dest_original = dest
else:
dest_edited = dest
# export the dest file
results = self._export_photo(
@@ -927,6 +912,7 @@ def export2(
preview_path = pathlib.Path(self.path_derivatives[0])
preview_ext = preview_path.suffix
preview_name = dest.parent / f"{dest.stem}{preview_suffix}{preview_ext}"
preview_name = pathlib.Path(increment_filename(preview_name))
if preview_path is not None:
results = self._export_photo(
preview_path,
@@ -969,6 +955,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
sidecars.append(
(
@@ -995,6 +982,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
sidecars.append(
(
@@ -1017,6 +1005,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
sidecars.append(
(
@@ -1087,6 +1076,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
)[0]
if old_data != current_data:
@@ -1110,6 +1100,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
if warning_:
all_results.exiftool_warning.append((exported_file, warning_))
@@ -1130,6 +1121,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
),
)
export_db.set_stat_exif_for_file(
@@ -1155,6 +1147,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
if warning_:
all_results.exiftool_warning.append((exported_file, warning_))
@@ -1175,6 +1168,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
),
)
export_db.set_stat_exif_for_file(
@@ -1580,6 +1574,7 @@ def _write_exif_data(
persons=True,
location=True,
replace_keywords=False,
strip=False,
):
"""write exif data to image file at filepath
@@ -1593,6 +1588,7 @@ def _write_exif_data(
persons: if True, write person data to metadata
location: if True, write location data to metadata
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
strip: if True, strip any leading or trailing whitespace from rendered templates
Returns:
(warning, error) of warning and error strings if exiftool produces warnings or errors
@@ -1610,6 +1606,7 @@ def _write_exif_data(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
with ExifTool(filepath, flags=flags, exiftool=self._db._exiftool_path) as exiftool:
@@ -1635,6 +1632,7 @@ def _exiftool_dict(
persons=True,
location=True,
replace_keywords=False,
strip=False,
):
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
Does not include all the EXIF fields as those are likely already in the image.
@@ -1651,6 +1649,7 @@ def _exiftool_dict(
persons: if True, include person data
location: if True, include location data
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
strip: if True, strip any rendered templates
Returns: dict with exiftool tags / values
@@ -1698,6 +1697,8 @@ def _exiftool_dict(
)
rendered = self.render_template(description_template, options)[0]
description = " ".join(rendered) if rendered else ""
if strip:
description = description.strip()
exif["EXIF:ImageDescription"] = description
exif["XMP:Description"] = description
exif["IPTC:Caption-Abstract"] = description
@@ -1745,6 +1746,9 @@ def _exiftool_dict(
)
rendered_keywords.extend(rendered)
if strip:
rendered_keywords = [keyword.strip() for keyword in rendered_keywords]
# filter out any template values that didn't match by looking for sentinel
rendered_keywords = [
keyword
@@ -1851,12 +1855,6 @@ def _exiftool_dict(
self.date_modified
).strftime("%Y:%m:%d %H:%M:%S")
# remove any new lines in any fields
for field, val in exif.items():
if type(val) == str:
exif[field] = val.replace("\n", " ")
elif type(val) == list:
exif[field] = [str(v).replace("\n", " ") for v in val if v is not None]
return exif
@@ -1909,6 +1907,7 @@ def _exiftool_json_sidecar(
persons=True,
location=True,
replace_keywords=False,
strip=False,
):
"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.
Does not include all the EXIF fields as those are likely already in the image.
@@ -1926,6 +1925,7 @@ def _exiftool_json_sidecar(
persons: if True, include person data
location: if True, include location data
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
strip: if True, strip whitespace from rendered templates
Returns: dict with exiftool tags / values
@@ -1965,6 +1965,7 @@ def _exiftool_json_sidecar(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
if not tag_groups:
@@ -1990,6 +1991,7 @@ def _xmp_sidecar(
persons=True,
location=True,
replace_keywords=False,
strip=False,
):
"""returns string for XMP sidecar
use_albums_as_keywords: treat album names as keywords
@@ -2002,6 +2004,7 @@ def _xmp_sidecar(
persons: if True, include person data
location: if True, include location data
replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive
strip: if True, strip whitespace from rendered templates
"""
xmp_template_file = (
@@ -2019,6 +2022,8 @@ def _xmp_sidecar(
)
rendered = self.render_template(description_template, options)[0]
description = " ".join(rendered) if rendered else ""
if strip:
description = description.strip()
else:
description = self.description if self.description is not None else ""
@@ -2060,6 +2065,9 @@ def _xmp_sidecar(
)
rendered_keywords.extend(rendered)
if strip:
rendered_keywords = [keyword.strip() for keyword in rendered_keywords]
# filter out any template values that didn't match by looking for sentinel
rendered_keywords = [
keyword

View File

@@ -14,6 +14,7 @@ from datetime import timedelta, timezone
from typing import Optional
import yaml
from osxmetadata import OSXMetaData
from .._constants import (
_MOVIE_TYPE,
@@ -37,6 +38,7 @@ from ..albuminfo import AlbumInfo, ImportInfo
from ..personinfo import FaceInfo, PersonInfo
from ..phototemplate import PhotoTemplate, RenderOptions
from ..placeinfo import PlaceInfo4, PlaceInfo5
from ..query_builder import get_query
from ..text_detection import detect_text
from ..uti import get_preferred_uti_extension, get_uti_for_extension
from ..utils import _debug, _get_resource_loc, findfiles
@@ -563,7 +565,12 @@ class PhotoInfo:
@property
def title(self):
"""name / title of picture"""
return self._info["name"]
# if user sets then deletes title, Photos sets it to empty string in DB instead of NULL
# in this case, return None so result is the same as if title had never been set (which returns NULL)
# issue #512
title = self._info["name"]
title = None if title == "" else title
return title
@property
def uuid(self):
@@ -1048,15 +1055,15 @@ class PhotoInfo:
return self._info["orientation"]
# For Photos 5+, try to get the adjusted orientation
if self.hasadjustments:
if self.adjustments:
return self.adjustments.adj_orientation
else:
# can't reliably determine orientation for edited photo if adjustmentinfo not available
return 0
else:
if not self.hasadjustments:
return self._info["orientation"]
if self.adjustments:
return self.adjustments.adj_orientation
else:
# can't reliably determine orientation for edited photo if adjustmentinfo not available
return 0
@property
def original_height(self):
"""returns height of the original photo version in pixels"""
@@ -1092,6 +1099,26 @@ class PhotoInfo:
logging.warning(f"Did not find signature for {self.uuid} in _db_signatures")
return duplicates
@property
def owner(self):
"""Return name of photo owner for shared photos (Photos 5+ only), or None if not shared"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return None
try:
return self._owner
except AttributeError:
try:
personid = self._info["cloudownerhashedpersonid"]
self._owner = (
self._db._db_hashed_person_id[personid]["full_name"]
if personid
else None
)
except KeyError:
self._owner = None
return self._owner
def render_template(
self, template_str: str, options: Optional[RenderOptions] = None
):
@@ -1118,6 +1145,28 @@ class PhotoInfo:
Returns: list of (detected text, confidence) tuples
"""
try:
return self._detected_text_cache[confidence_threshold]
except (AttributeError, KeyError) as e:
if isinstance(e, AttributeError):
self._detected_text_cache = {}
try:
detected_text = self._detected_text()
except Exception as e:
logging.warning(f"Error detecting text in photo {self.uuid}: {e}")
detected_text = []
self._detected_text_cache[confidence_threshold] = [
(text, confidence)
for text, confidence in detected_text
if confidence >= confidence_threshold
]
return self._detected_text_cache[confidence_threshold]
def _detected_text(self):
"""detect text in photo, either from cached extended attribute or by attempting text detection"""
path = (
self.path_edited if self.hasadjustments and self.path_edited else self.path
)
@@ -1125,24 +1174,13 @@ class PhotoInfo:
if not path:
return []
try:
return self._detected_text[(path, confidence_threshold)]
except (AttributeError, KeyError) as e:
if isinstance(e, AttributeError):
self._detected_text = {}
try:
detected_text = detect_text(path)
except Exception as e:
logging.warning(f"Error detecting text in photo {self.uuid} at {path}: {e}")
detected_text = []
self._detected_text[(path, confidence_threshold)] = [
(text, confidence)
for text, confidence in detected_text
if confidence >= confidence_threshold
]
return self._detected_text[(path, confidence_threshold)]
md = OSXMetaData(path)
detected_text = md.get_attribute("osxphotos_detected_text")
if detected_text is None:
orientation = self.orientation or None
detected_text = detect_text(path, orientation)
md.set_attribute("osxphotos_detected_text", detected_text)
return detected_text
@property
def _longitude(self):

View File

@@ -70,12 +70,24 @@ def _process_comments_5(photosdb):
results = conn.execute(
"""
SELECT DISTINCT
ZINVITEEHASHEDPERSONID,
ZINVITEEFIRSTNAME,
ZINVITEELASTNAME,
ZINVITEEFULLNAME
FROM
ZCLOUDSHAREDALBUMINVITATIONRECORD
ZINVITEEHASHEDPERSONID AS HASHEDPERSONID,
ZINVITEEFIRSTNAME AS FIRSTNAME,
ZINVITEELASTNAME AS LASTNAME,
ZINVITEEFULLNAME AS FULLNAME
FROM ZCLOUDSHAREDALBUMINVITATIONRECORD
WHERE HASHEDPERSONID IS NOT NULL
AND HASHEDPERSONID != ""
AND NOT (FIRSTNAME IS NULL AND LASTNAME IS NULL)
UNION
SELECT DISTINCT
ZCLOUDOWNERHASHEDPERSONID AS HASHEDPERSONID,
ZCLOUDOWNERFIRSTNAME AS FIRSTNAME,
ZCLOUDOWNERLASTNAME AS LASTNAME,
ZCLOUDOWNERFULLNAME AS FULLNAME
FROM ZGENERICALBUM
WHERE HASHEDPERSONID IS NOT NULL
AND HASHEDPERSONID != ""
AND NOT (FIRSTNAME IS NULL AND LASTNAME IS NULL)
"""
)
@@ -148,10 +160,10 @@ def _process_comments_5(photosdb):
db_comments["comments"].append(CommentInfo(dt, user_name, ismine, text))
# sort results
for uuid in photosdb._db_comments_uuid:
for uuid, value in photosdb._db_comments_uuid.items():
if photosdb._db_comments_uuid[uuid]["likes"]:
photosdb._db_comments_uuid[uuid]["likes"].sort(key=lambda x: x.datetime)
if photosdb._db_comments_uuid[uuid]["comments"]:
photosdb._db_comments_uuid[uuid]["comments"].sort(key=lambda x: x.datetime)
value["comments"].sort(key=lambda x: x.datetime)
conn.close()

View File

@@ -330,6 +330,8 @@ class PhotosDB:
else:
self._process_database5()
self._db_connection, _ = self.get_db_connection()
@property
def keywords_as_dict(self):
"""return keywords as dict of keyword, count in reverse sorted order (descending)"""
@@ -790,8 +792,8 @@ class PhotosDB:
"creation_date": album[8],
"start_date": None, # Photos 5 only
"end_date": None, # Photos 5 only
"customsortascending": None, # Photos 5 only
"customsortkey": None, # Photos 5 only
"customsortascending": None, # Photos 5 only
"customsortkey": None, # Photos 5 only
}
# get details about folders
@@ -1104,7 +1106,9 @@ class PhotosDB:
# get info on special types
self._dbphotos[uuid]["specialType"] = row[25]
self._dbphotos[uuid]["masterModelID"] = row[26]
self._dbphotos[uuid]["pk"] = row[26] # same as masterModelID, to match Photos 5
self._dbphotos[uuid]["pk"] = row[
26
] # same as masterModelID, to match Photos 5
self._dbphotos[uuid]["panorama"] = True if row[25] == 1 else False
self._dbphotos[uuid]["slow_mo"] = True if row[25] == 2 else False
self._dbphotos[uuid]["time_lapse"] = True if row[25] == 3 else False
@@ -1195,6 +1199,9 @@ class PhotosDB:
self._dbphotos[uuid]["import_uuid"] = row[44]
self._dbphotos[uuid]["fok_import_session"] = None
# photos 5+ only, for shared photos
self._dbphotos[uuid]["cloudownerhashedpersonid"] = None
# compute signatures for finding possible duplicates
signature = self._duplicate_signature(uuid)
try:
@@ -1923,7 +1930,8 @@ class PhotosDB:
{asset_table}.ZTRASHEDDATE,
{asset_table}.ZSAVEDASSETTYPE,
{asset_table}.ZADDEDDATE,
{asset_table}.Z_PK
{asset_table}.Z_PK,
{asset_table}.ZCLOUDOWNERHASHEDPERSONID
FROM {asset_table}
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
ORDER BY {asset_table}.ZUUID """
@@ -1973,6 +1981,7 @@ class PhotosDB:
# 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported
# 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library
# 42 ZGENERICASSET.Z_PK -- primary key
# 43 ZGENERICASSET.ZCLOUDOWNERHASHEDPERSONID -- used to look up owner name (for shared photos)
for row in c:
uuid = row[0]
@@ -2158,6 +2167,7 @@ class PhotosDB:
info["added_date"] = datetime(1970, 1, 1)
info["pk"] = row[42]
info["cloudownerhashedpersonid"] = row[43]
# initialize import session info which will be filled in later
# not every photo has an import session so initialize all records now
@@ -3354,6 +3364,10 @@ class PhotosDB:
return photos
def execute(self, sql):
"""Execute sql statement and return cursor"""
return self._db_connection.cursor().execute(sql)
def _duplicate_signature(self, uuid):
"""Compute a signature for finding possible duplicates"""
return (
@@ -3381,6 +3395,10 @@ class PhotosDB:
"""
return len(self._dbphotos)
def __del__(self):
if getattr(self, "_db_connection", None):
self._db_connection.close()
def _get_photos_by_attribute(photos, attribute, values, ignore_case):
"""Search for photos based on values being in PhotoInfo.attribute

View File

@@ -209,6 +209,7 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
+ "'{detected_text}' works only on macOS Catalina (10.15) or later. "
+ "Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.",
"{shell_quote}": "Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.",
"{strip}": "Use in form '{strip,TEMPLATE}'; strips whitespace from begining and end of rendered TEMPLATE value(s).",
"{function}": "Execute a python function from an external file and use return value as template substitution. "
+ "Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. "
+ "The function will be passed the PhotoInfo object for the photo. "
@@ -574,7 +575,7 @@ class PhotoTemplate:
if self.expand_inplace or delim is not None:
sep = delim if delim is not None else self.inplace_sep
vals = [sep.join(sorted(vals))]
vals = [sep.join(sorted(vals))] if vals else []
for filter_ in filters:
vals = self.get_template_value_filter(filter_, vals)
@@ -1003,6 +1004,9 @@ class PhotoTemplate:
elif self.dirname:
value = sanitize_dirname(value)
# ensure no empty strings in value (see #512)
value = None if value == "" else value
return [value]
def get_template_value_pathlib(self, field):
@@ -1162,6 +1166,8 @@ class PhotoTemplate:
)
elif field == "shell_quote":
values = [shlex.quote(v) for v in default if v]
elif field == "strip":
values = [v.strip() for v in default]
elif field.startswith("photo"):
# provide access to PhotoInfo object
properties = field.split(".")
@@ -1445,25 +1451,8 @@ def _get_detected_text(photo, exportdb, confidence=TEXT_DETECTION_CONFIDENCE_THR
else TEXT_DETECTION_CONFIDENCE_THRESHOLD
)
detected_text = exportdb.get_detected_text_for_uuid(photo.uuid)
if detected_text is not None:
detected_text = json.loads(detected_text)
else:
path = (
photo.path_edited
if photo.hasadjustments and photo.path_edited
else photo.path
)
path = path or photo.path_derivatives[0] if photo.path_derivatives else None
if not path:
detected_text = []
else:
try:
detected_text = detect_text(path)
except Exception as e:
logging.warning(
f"Error detecting text in image {photo.uuid} at {path}: {e}"
)
return []
exportdb.set_detected_text_for_uuid(photo.uuid, json.dumps(detected_text))
# _detected_text caches the text detection results in an extended attribute
# so the first time this gets called is slow but repeated accesses are fast
detected_text = photo._detected_text()
exportdb.set_detected_text_for_uuid(photo.uuid, json.dumps(detected_text))
return [text for text, conf in detected_text if conf >= confidence]

View File

@@ -0,0 +1,5 @@
# Query templates
This folder contains sql query templates for getting various photo properties
The query templates must be rendered with mako (see query_builder.py)

View File

@@ -0,0 +1,4 @@
-- Get owner name for shared iCloud album
SELECT ZGENERICALBUM.ZCLOUDOWNERFULLNAME AS OWNER_FULLNAME
FROM ZGENERICALBUM
WHERE ZGENERICALBUM.ZUUID = '${uuid}'

View File

@@ -0,0 +1,23 @@
-- Get the owner name of person who owns a photo in a shared album
--
-- Case where someone has invited you to a shared album
-- Need to get the owner of the shared album
SELECT DISTINCT
ZGENERICALBUM.ZCLOUDOWNERFULLNAME as OWNER_FULLNAME
FROM ZGENERICALBUM
JOIN ${asset_table} ON ${asset_table}.ZCLOUDOWNERHASHEDPERSONID = ZGENERICALBUM.ZCLOUDOWNERHASHEDPERSONID
WHERE ${asset_table}.ZUUID = "${uuid}"
AND ZGENERICALBUM.ZCLOUDOWNERHASHEDPERSONID IS NOT NULL
AND ZGENERICALBUM.ZCLOUDOWNERHASHEDPERSONID != ""
AND OWNER_FULLNAME != "(null) (null)"
UNION
-- Case where you have invited someone to a shared album
-- Need to get the data for person who was invited to the album
SELECT DISTINCT
ZCLOUDSHAREDALBUMINVITATIONRECORD.ZINVITEEFULLNAME AS OWNER_FULLNAME
FROM ZCLOUDSHAREDALBUMINVITATIONRECORD
JOIN ${asset_table} ON ${asset_table}.ZCLOUDOWNERHASHEDPERSONID = ZCLOUDSHAREDALBUMINVITATIONRECORD.ZINVITEEHASHEDPERSONID
WHERE ${asset_table}.ZUUID = "${uuid}"
AND ZCLOUDSHAREDALBUMINVITATIONRECORD.ZINVITEEHASHEDPERSONID IS NOT NULL
AND ZCLOUDSHAREDALBUMINVITATIONRECORD.ZINVITEEHASHEDPERSONID != ""
AND OWNER_FULLNAME != "(null) (null)"

View File

@@ -0,0 +1,6 @@
-- Get title of a photo with given UUID
SELECT
ZADDITIONALASSETATTRIBUTES.ZTITLE
FROM ZADDITIONALASSETATTRIBUTES
JOIN ${asset_table} ON ${asset_table}.Z_PK = ZADDITIONALASSETATTRIBUTES.ZASSET
WHERE ${asset_table}.ZUUID = "${uuid}"

View File

@@ -0,0 +1,36 @@
"""Build sql queries from template to retrieve info from the database"""
import os.path
import pathlib
from functools import lru_cache
from mako.template import Template
from ._constants import _DB_TABLE_NAMES
QUERY_DIR = os.path.join(os.path.dirname(__file__), "queries")
def get_query(query_name, photos_ver, **kwargs):
"""Return sqlite query string for an attribute and a given database version"""
# there can be a single query for multiple database versions or separate queries for each version
# try generic version first (most common case), if that fails, look for version specific query
query_string = _get_query_string(query_name, photos_ver)
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
query_template = Template(query_string)
return query_template.render(asset_table=asset_table, **kwargs)
@lru_cache(maxsize=None)
def _get_query_string(query_name, photos_ver):
"""Return sqlite query string for an attribute and a given database version"""
query_file = pathlib.Path(QUERY_DIR) / f"{query_name}.sql.mako"
if not query_file.is_file():
query_file = pathlib.Path(QUERY_DIR) / f"{query_name}_{photos_ver}.sql.mako"
if not query_file.is_file():
raise FileNotFoundError(f"Query file '{query_file}' not found")
with open(query_file, "r") as f:
query_string = f.read()
return query_string

View File

@@ -1,7 +1,7 @@
""" Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """
import logging
from typing import List
from typing import List, Optional
import objc
import Quartz
@@ -22,8 +22,13 @@ else:
vision = True
def detect_text(img_path: str) -> List:
"""process image at img_path with VNRecognizeTextRequest and return list of results"""
def detect_text(img_path: str, orientation: Optional[int] = None) -> List:
"""process image at img_path with VNRecognizeTextRequest and return list of results
Args:
img_path: path to the image file
orientation: optional EXIF orientation (if known, passing orientation may improve quality of results)
"""
if not vision:
logging.warning(f"detect_text not implemented for this version of macOS")
return []
@@ -40,9 +45,18 @@ def detect_text(img_path: str) -> List:
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
vision_options = NSDictionary.dictionaryWithDictionary_({})
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
input_image, vision_options
)
if orientation is not None:
if not 1 <= orientation <= 8:
raise ValueError("orientation must be between 1 and 8")
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_orientation_options_(
input_image, orientation, vision_options
)
else:
vision_handler = (
Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
input_image, vision_options
)
)
results = []
handler = make_request_handler(results)
vision_request = (
@@ -52,6 +66,9 @@ def detect_text(img_path: str) -> List:
vision_request.dealloc()
vision_handler.dealloc()
for result in results:
result[0] = str(result[0])
return results

View File

@@ -278,15 +278,15 @@ For example, to set Finder comment to the photo's title and description:
In the template string above, `{newline}` instructs osxphotos to insert a new line character ("\n") between the title and description. In this example, if `{title}` or `{descr}` is empty, you'll get "title\n" or "\ndescription" which may not be desired so you can use more advanced features of the template system to handle these cases:
`osxphotos export /path/to/export --xattr-template findercomment "{title}{title?{descr?{newline},},}{descr}"`
`osxphotos export /path/to/export --xattr-template findercomment "{title,}{title?{descr?{newline},},}{descr,}"`
Explanation of the template string:
```txt
{title}{title?{descr?{newline},},}{descr}
{title,}{title?{descr?{newline},},}{descr,}
│ │ │ │ │ │ │
│ │ │ │ │ │ │
└──> insert title │ │ │ │ │
└──> insert title (or nothing if no title)
│ │ │ │ │ │
└───> is there a title?
│ │ │ │ │
@@ -298,7 +298,8 @@ Explanation of the template string:
│ │
└───> if title is blank, insert nothing
└───> finally, insert description
└───> finally, insert description
(or nothing if no description)
```
In this example, `title?` demonstrates use of the boolean (True/False) feature of the template system. `title?` is read as "Is the title True (or not blank/empty)? If so, then the value immediately following the `?` is used in place of `title`. If `title` is blank, then the value immediately following the comma is used instead. The format for boolean fields is `field?value if true,value if false`. Either `value if true` or `value if false` may be blank, in which case a blank string ("") is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex `if-then-else` statements.

View File

@@ -16,9 +16,11 @@ import sys
import unicodedata
import urllib.parse
from plistlib import load as plistload
from typing import Callable
from typing import Callable, Union
import CoreFoundation
import objc
from Foundation import NSString
from ._constants import UNICODE_FORMAT
@@ -263,6 +265,13 @@ def list_photo_libraries():
return lib_list
def normalize_fs_path(path: str) -> str:
"""Normalize filesystem paths with unicode in them"""
with objc.autorelease_pool():
normalized_path = NSString.fileSystemRepresentation(path)
return normalized_path.decode("utf8")
def findfiles(pattern, path_):
"""Returns list of filenames from path_ matched by pattern
shell pattern. Matching is case-insensitive.
@@ -271,8 +280,11 @@ def findfiles(pattern, path_):
return []
# See: https://gist.github.com/techtonik/5694830
# paths need to be normalized for unicode as filesystem returns unicode in NFD form
pattern = normalize_fs_path(pattern)
rule = re.compile(fnmatch.translate(pattern), re.IGNORECASE)
return [name for name in os.listdir(path_) if rule.match(name)]
files = [normalize_fs_path(p) for p in os.listdir(path_)]
return [name for name in files if rule.match(name)]
def _open_sql_file(dbname):
@@ -353,30 +365,50 @@ def normalize_unicode(value):
return None
def increment_filename(filepath):
def increment_filename_with_count(filepath: Union[str,pathlib.Path], count: int = 0) -> str:
"""Return filename (1).ext, etc if filename.ext exists
If file exists in filename's parent folder with same stem as filename,
add (1), (2), etc. until a non-existing filename is found.
Args:
filepath: str; full path, including file name
filepath: str or pathlib.Path; full path, including file name
count: int; starting increment value
Returns:
tuple of new filepath (or same if not incremented), count
Note: This obviously is subject to race condition so using with caution.
"""
dest = filepath if isinstance(filepath, pathlib.Path) else pathlib.Path(filepath)
dest_files = findfiles(f"{dest.stem}*", str(dest.parent))
dest_files = [normalize_fs_path(pathlib.Path(f).stem.lower()) for f in dest_files]
dest_new = dest.stem
if count:
dest_new = f"{dest.stem} ({count})"
while normalize_fs_path(dest_new.lower()) in dest_files:
count += 1
dest_new = f"{dest.stem} ({count})"
dest = dest.parent / f"{dest_new}{dest.suffix}"
return str(dest), count
def increment_filename(filepath: Union[str, pathlib.Path]) -> str:
"""Return filename (1).ext, etc if filename.ext exists
If file exists in filename's parent folder with same stem as filename,
add (1), (2), etc. until a non-existing filename is found.
Args:
filepath: str or pathlib.Path; full path, including file name
Returns:
new filepath (or same if not incremented)
Note: This obviously is subject to race condition so using with caution.
"""
dest = pathlib.Path(str(filepath))
count = 1
dest_files = findfiles(f"{dest.stem}*", str(dest.parent))
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
dest_new = dest.stem
while dest_new.lower() in dest_files:
dest_new = f"{dest.stem} ({count})"
count += 1
dest = dest.parent / f"{dest_new}{dest.suffix}"
return str(dest)
new_filepath, _ = increment_filename_with_count(filepath)
return new_filepath
def expand_and_validate_filepath(path: str) -> str:

View File

@@ -1,23 +1,23 @@
pyobjc-core>=7.2
pyobjc-framework-AppleScriptKit>=7.2
pyobjc-framework-AppleScriptObjC>=7.2
pyobjc-framework-Photos>=7.2
pyobjc-framework-Quartz>=7.2
pyobjc-framework-AVFoundation>=7.2
pyobjc-framework-CoreServices>=7.2
pyobjc-framework-Metal>=7.2
pyobjc-framework-Vision>=7.2
Click==8.0.1
PyYAML==5.4.1
Mako==1.1.4
pyobjc-core>=7.2,<8.0
pyobjc-framework-AppleScriptKit>=7.2,<8.0
pyobjc-framework-AppleScriptObjC>=7.2,<8.0
pyobjc-framework-Photos>=7.2,<8.0
pyobjc-framework-Quartz>=7.2,<8.0
pyobjc-framework-AVFoundation>=7.2,<8.0
pyobjc-framework-CoreServices>=7.2,<8.0
pyobjc-framework-Metal>=7.2,<8.0
pyobjc-framework-Vision>=7.2,<8.0
Click>=8.0.1,<9.0
PyYAML>=5.4.1<5.5.0
Mako>=1.1.4,<1.2.0
bpylist2==3.0.2
pathvalidate==2.4.1
pathvalidate>=2.4.1,<2.5.0
dataclasses==0.7;python_version<'3.7'
wurlitzer==2.1.0
photoscript==0.1.4
toml==0.10.2
osxmetadata==0.99.26
textx==2.3.0
rich==10.6.0
bitmath==1.3.3.1
more-itertools==8.8.0
wurlitzer>=2.1.0,<2.2.0
photoscript>=0.1.4,<0.2.0
toml>=0.10.2,<0.11.0
osxmetadata>=0.99.33,<1.0.0
textx>=2.3.0,<2.4.0
rich>=10.6.0,<11.0.0
bitmath>=1.3.3.1,<1.4.0.0
more-itertools>=8.8.0,<9.0.0

View File

@@ -3,4 +3,6 @@ pytest==6.2.4
pytest-mock
m2r2
pyinstaller==4.4
sphinx_rtd_theme
wheel
twine

View File

@@ -73,29 +73,29 @@ setup(
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=[
"pyobjc-core",
"pyobjc-framework-AppleScriptKit",
"pyobjc-framework-AppleScriptObjC",
"pyobjc-framework-Photos",
"pyobjc-framework-Quartz",
"pyobjc-framework-AVFoundation",
"pyobjc-framework-CoreServices",
"pyobjc-framework-Metal",
"pyobjc-framework-Vision",
"Click==8.0.1",
"PyYAML==5.4.1",
"Mako==1.1.4",
"pyobjc-core>=7.2,<8.0",
"pyobjc-framework-AppleScriptKit>=7.2,<8.0",
"pyobjc-framework-AppleScriptObjC>=7.2,<8.0",
"pyobjc-framework-Photos>=7.2,<8.0",
"pyobjc-framework-Quartz>=7.2,<8.0",
"pyobjc-framework-AVFoundation>=7.2,<8.0",
"pyobjc-framework-CoreServices>=7.2,<8.0",
"pyobjc-framework-Metal>=7.2,<8.0",
"pyobjc-framework-Vision>=7.2,<8.0",
"Click>=8.0.1,<9.0",
"PyYAML>=5.4.1,<5.5.0",
"Mako>=1.1.4,<1.2.0",
"bpylist2==3.0.2",
"pathvalidate==2.4.1",
"pathvalidate>=2.4.1,<2.5.0",
"dataclasses==0.7;python_version<'3.7'",
"wurlitzer==2.1.0",
"photoscript==0.1.4",
"toml==0.10.2",
"osxmetadata==0.99.26",
"textx==2.3.0",
"rich==10.6.0",
"bitmath==1.3.3.1",
"more-itertools==8.8.0",
"wurlitzer>=2.1.0,<2.2.0",
"photoscript>=0.1.4,<0.2.0",
"toml>=0.10.2,<0.11.0",
"osxmetadata>=0.99.33,<1.0.0",
"textx>=2.3.0,<2.4.0",
"rich>=10.6.0,<11.0.0",
"bitmath>=1.3.3.1,<1.4.0.0",
"more-itertools>=8.8.0,<9.0.0",
],
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
include_package_data=True,

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2021-07-20T05:48:01Z</date>
<date>2021-09-14T04:40:42Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2021-07-20T05:48:00Z</date>
<date>2021-09-14T04:40:42Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2021-07-20T07:05:31Z</date>
<date>2021-09-14T04:40:42Z</date>
<key>BackgroundJobSearch</key>
<date>2021-07-20T05:48:01Z</date>
<date>2021-09-14T04:40:42Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2021-07-20T05:48:00Z</date>
<date>2021-09-14T04:40:41Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2021-07-20T05:48:01Z</date>
<date>2021-09-14T04:40:42Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2021-07-20T05:48:08Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2021-07-20T05:47:59Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2021-07-20T05:48:01Z</date>
<date>2021-09-14T04:40:43Z</date>
<key>SiriPortraitDonation</key>
<date>2021-07-20T05:48:01Z</date>
<date>2021-09-14T04:40:42Z</date>
</dict>
</plist>

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>FaceIDModelLastGenerationKey</key>
<date>2021-07-20T05:48:02Z</date>
<date>2021-09-14T04:49:52Z</date>
<key>LastContactClassificationKey</key>
<date>2021-07-20T05:48:05Z</date>
<date>2021-09-14T04:51:05Z</date>
</dict>
</plist>

View File

@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>PVClustererBringUpState</key>
<integer>50</integer>
<integer>40</integer>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 KiB

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>adjustmentBaseVersion</key>
<integer>0</integer>
<key>adjustmentData</key>
<data>
bZHNTsMwEITfZc8hcn4aaG5wabkUiSKKhDhs602zEDuRvemlyrtjt2pBiKN3v5mdkY9w
IOe5t4+26aE+wnbkTq9GsyUHNWTzZTaDBHAYXs9cHFZZqtIsV2Hhdy0ZfKYDn5dZAkOH
0vTOBPJp/QZTAoYENQpGf4NeyG1YSwt1qYo8CHigji39XAi6tAzuZ3hJvG8F6kLlZQK9
Y7KCciKr4B5voVzFIQHqz9GLCZiH+v34D0EWtx1pqMWNFFqQCNu9jwHZDqM8dLj7urd6
07IQ1DcqVYUqqlKVVTHPbmd5pe5ClKYJyoVDDq7q8l6LI7uP9a6jFY3isFugMXga+1jA
C+/iyemCLUf6JXrpbXyGLetQhRs+fcnaoPuTb/qYvgE=
</data>
<key>adjustmentEditorBundleID</key>
<string>com.apple.Photos</string>
<key>adjustmentFormatIdentifier</key>
<string>com.apple.photo</string>
<key>adjustmentFormatVersion</key>
<string>1.4</string>
<key>adjustmentTimestamp</key>
<date>2021-09-14T04:49:50Z</date>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>adjustmentBaseVersion</key>
<integer>0</integer>
<key>adjustmentData</key>
<data>
bZFNb8IwDIb/S86oSgp0o7dxgV2YNKYxadrBEJd6a9Iqcbmg/vc5VLBp2jH2835YOasT
hkitf/RVq8qz2vfU2E3v9hhUqcxibeZqoqDrXkcuDQuT6czkWhbxUKODZzzRuDQT1TXA
VRuckE/bNzVMlEMGCwzJ30FkDDuyXAut77UIqMOGPP4kiC6bifsIr5GONauy0OLeBkLP
wGOamKco4JtWELCffWQnWFTl+/kfAj3sG7Sq5NCjHIHM5I8x9SPf9bxs4PD14O2uJkZV
6qyY52aWTxfFQt/lpphNpUhViW4VgMRTX99bDuiP6bbbaIM9B2hW4BxcxjHVj0yHFDhc
sXWPv0QvrU9P2ZKVQ6iiy39sHYQ/7YaP4Rs=
</data>
<key>adjustmentEditorBundleID</key>
<string>com.apple.Photos</string>
<key>adjustmentFormatIdentifier</key>
<string>com.apple.photo</string>
<key>adjustmentFormatVersion</key>
<string>1.4</string>
<key>adjustmentTimestamp</key>
<date>2021-09-14T04:50:39Z</date>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

File diff suppressed because one or more lines are too long

View File

@@ -337,7 +337,7 @@ def test_attributes(photosdb):
def test_attributes_2(photosdb):
""" Test attributes including height, width, etc """
"""Test attributes including height, width, etc"""
import datetime
photos = photosdb.photos(uuid=[UUID_DICT["has_adjustments"]])
@@ -517,39 +517,39 @@ def test_count(photosdb):
def test_photos_intrash_1(photosdb):
""" test PhotosDB.photos(intrash=True) """
"""test PhotosDB.photos(intrash=True)"""
photos = photosdb.photos(intrash=True)
assert len(photos) == PHOTOS_IN_TRASH_LEN
def test_photos_intrash_2(photosdb):
""" test PhotosDB.photos(intrash=True) """
"""test PhotosDB.photos(intrash=True)"""
photos = photosdb.photos(intrash=True)
for p in photos:
assert p.intrash
def test_photos_intrash_3(photosdb):
""" test PhotosDB.photos(intrash=False) """
"""test PhotosDB.photos(intrash=False)"""
photos = photosdb.photos(intrash=False)
for p in photos:
assert not p.intrash
def test_photoinfo_intrash_1(photosdb):
""" Test PhotoInfo.intrash """
"""Test PhotoInfo.intrash"""
p = photosdb.photos(uuid=[UUID_DICT["intrash"]], intrash=True)[0]
assert p.intrash
def test_photoinfo_intrash_2(photosdb):
""" Test PhotoInfo.intrash and intrash=default"""
"""Test PhotoInfo.intrash and intrash=default"""
p = photosdb.photos(uuid=[UUID_DICT["intrash"]])
assert not p
def test_photoinfo_intrash_3(photosdb):
""" Test PhotoInfo.intrash and photo has keyword and person """
"""Test PhotoInfo.intrash and photo has keyword and person"""
p = photosdb.photos(uuid=[UUID_DICT["intrash_person_keywords"]], intrash=True)[0]
assert p.intrash
assert "Maria" in p.persons
@@ -557,7 +557,7 @@ def test_photoinfo_intrash_3(photosdb):
def test_photoinfo_intrash_4(photosdb):
""" Test PhotoInfo.intrash and photo has keyword and person """
"""Test PhotoInfo.intrash and photo has keyword and person"""
p = photosdb.photos(persons=["Maria"], intrash=True)[0]
assert p.intrash
assert "Maria" in p.persons
@@ -565,7 +565,7 @@ def test_photoinfo_intrash_4(photosdb):
def test_photoinfo_intrash_5(photosdb):
""" Test PhotoInfo.intrash and photo has keyword and person """
"""Test PhotoInfo.intrash and photo has keyword and person"""
p = photosdb.photos(keywords=["wedding"], intrash=True)[0]
assert p.intrash
assert "Maria" in p.persons
@@ -573,7 +573,7 @@ def test_photoinfo_intrash_5(photosdb):
def test_photoinfo_not_intrash(photosdb):
""" Test PhotoInfo.intrash """
"""Test PhotoInfo.intrash"""
p = photosdb.photos(uuid=[UUID_DICT["not_intrash"]])[0]
assert not p.intrash
@@ -594,7 +594,7 @@ def test_keyword_not_in_album(photosdb):
def test_album_folder_name(photosdb):
"""Test query with album name same as a folder name """
"""Test query with album name same as a folder name"""
photos = photosdb.photos(albums=["Pumpkin Farm"])
assert sorted(p.uuid for p in photos) == sorted(UUID_PUMPKIN_FARM)
@@ -617,7 +617,7 @@ def test_get_library_path(photosdb):
def test_get_db_connection(photosdb):
""" Test PhotosDB.get_db_connection """
"""Test PhotosDB.get_db_connection"""
import sqlite3
conn, cursor = photosdb.get_db_connection()
@@ -926,7 +926,7 @@ def test_export_14(caplog, photosdb):
def test_eq(photosdb):
""" Test equality of two PhotoInfo objects """
"""Test equality of two PhotoInfo objects"""
import osxphotos
photosdb2 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
@@ -936,7 +936,7 @@ def test_eq(photosdb):
def test_eq_2(photosdb):
""" Test equality of two PhotoInfo objects when one has memoized property """
"""Test equality of two PhotoInfo objects when one has memoized property"""
import osxphotos
photosdb2 = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
@@ -960,7 +960,7 @@ def test_photosdb_repr():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photosdb2 = eval(repr(photosdb))
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name"]
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name", "_db_connection"]
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
}
@@ -999,7 +999,7 @@ def test_from_to_date(photosdb):
def test_date_invalid():
""" Test date is invalid """
"""Test date is invalid"""
# doesn't run correctly with the module-level fixture
from datetime import datetime, timedelta, timezone
import osxphotos
@@ -1016,7 +1016,7 @@ def test_date_invalid():
def test_date_modified_invalid(photosdb):
""" Test date modified is invalid """
"""Test date modified is invalid"""
from datetime import datetime, timedelta, timezone
# UUID_DICT["date_invalid"] has an invalid modified date that's
@@ -1028,7 +1028,7 @@ def test_date_modified_invalid(photosdb):
def test_uti(photosdb):
""" test uti """
"""test uti"""
for uuid, uti in UTI_DICT.items():
photo = photosdb.get_photo(uuid)
@@ -1037,7 +1037,7 @@ def test_uti(photosdb):
def test_raw(photosdb):
""" Test various raw properties """
"""Test various raw properties"""
for uuid, rawinfo in RAW_DICT.items():
photo = photosdb.get_photo(uuid)
@@ -1050,7 +1050,7 @@ def test_raw(photosdb):
def test_is_reference(photosdb):
""" test isreference """
"""test isreference"""
photo = photosdb.get_photo(UUID_IS_REFERENCE)
assert photo.isreference
@@ -1059,7 +1059,7 @@ def test_is_reference(photosdb):
def test_adjustments(photosdb):
""" test adjustments/AdjustmentsInfo """
"""test adjustments/AdjustmentsInfo"""
from osxphotos.adjustmentsinfo import AdjustmentsInfo
photo = photosdb.get_photo(UUID_DICT["adjustments_info"])
@@ -1121,7 +1121,7 @@ def test_adjustments(photosdb):
def test_no_adjustments(photosdb):
""" test adjustments when photo has no adjusments"""
"""test adjustments when photo has no adjusments"""
photo = photosdb.get_photo(UUID_DICT["no_adjustments"])
assert photo.adjustments is None

View File

@@ -23,10 +23,10 @@ PHOTOS_DB = "tests/Test-10.15.7.photoslibrary/database/photos.db"
PHOTOS_DB_PATH = "/Test-10.15.7.photoslibrary/database/photos.db"
PHOTOS_LIBRARY_PATH = "/Test-10.15.7.photoslibrary"
PHOTOS_DB_LEN = 21
PHOTOS_NOT_IN_TRASH_LEN = 19
PHOTOS_DB_LEN = 25
PHOTOS_NOT_IN_TRASH_LEN = 23
PHOTOS_IN_TRASH_LEN = 2
PHOTOS_DB_IMPORT_SESSIONS = 15
PHOTOS_DB_IMPORT_SESSIONS = 17
KEYWORDS = [
"Kids",
@@ -45,6 +45,15 @@ KEYWORDS = [
"Val d'Isère",
"Wine",
"Wine Bottle",
"Food",
"Furniture",
"Pizza",
"Table",
"Cloudy",
"Cord",
"Outdoor",
"Sky",
"Sunset Sunrise",
]
# Photos 5 includes blank person for detected face
PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
@@ -80,6 +89,15 @@ KEYWORDS_DICT = {
"flowers": 1,
"foo/bar": 1,
"wedding": 3,
"Food": 2,
"Furniture": 2,
"Pizza": 2,
"Table": 2,
"Cloudy": 2,
"Cord": 2,
"Outdoor": 2,
"Sky": 2,
"Sunset Sunrise": 2,
}
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 2, _UNKNOWN_PERSON: 1}
ALBUM_DICT = {
@@ -165,7 +183,6 @@ UTI_ORIGINAL_DICT = {
"1EB2B765-0765-43BA-A90C-0D0580E6172C": "public.jpeg",
}
RawInfo = namedtuple(
"RawInfo",
[
@@ -1049,7 +1066,7 @@ def test_photosdb_repr():
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
photosdb2 = eval(repr(photosdb))
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name"]
ignore_keys = ["_tmp_db", "_tempdir", "_tempdir_name", "_db_connection"]
assert {k: v for k, v in photosdb.__dict__.items() if k not in ignore_keys} == {
k: v for k, v in photosdb2.__dict__.items() if k not in ignore_keys
}
@@ -1073,7 +1090,7 @@ def test_from_to_date(photosdb):
time.tzset()
photos = photosdb.photos(from_date=datetime.datetime(2018, 10, 28))
assert len(photos) == 12
assert len(photos) == 16
photos = photosdb.photos(to_date=datetime.datetime(2018, 10, 28))
assert len(photos) == 7
@@ -1376,12 +1393,12 @@ def test_no_adjustments(photosdb):
def test_exiftool_newlines_in_description(photosdb):
"""Test that exiftool code removes newlines embedded in description, issue #393"""
"""Test that exiftool handles newlines embedded in description, issue #393"""
photo = photosdb.get_photo(UUID_DICT["description_newlines"])
exif = photo._exiftool_dict()
assert photo.description.find("\n") > 0
assert exif["EXIF:ImageDescription"].find("\n") == -1
assert exif["EXIF:ImageDescription"].find("\n") > 0
@pytest.mark.skip(SKIP_TEST, reason="Not yet implemented")

View File

@@ -93,6 +93,12 @@ CLI_EXPORT_FILENAMES = [
"screenshot-really-a-png.jpeg",
"winebottle.jpeg",
"winebottle (1).jpeg",
"Frítest.jpg",
"Frítest (1).jpg",
"Frítest (2).jpg",
"Frítest (3).jpg",
"Frítest_edited.jpeg",
"Frítest_edited (1).jpeg",
]
@@ -120,6 +126,8 @@ CLI_EXPORT_FILENAMES_DRY_RUN = [
"screenshot-really-a-png.jpeg",
"winebottle.jpeg",
"winebottle.jpeg",
"Frítest.jpg",
"Frítest_edited.jpeg",
]
CLI_EXPORT_IGNORE_SIGNATURE_FILENAMES = ["Tulips.jpg", "wedding.jpg"]
@@ -160,6 +168,12 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX = [
"screenshot-really-a-png.jpeg",
"winebottle.jpeg",
"winebottle (1).jpeg",
"Frítest.jpg",
"Frítest (1).jpg",
"Frítest (2).jpg",
"Frítest (3).jpg",
"Frítest_bearbeiten.jpeg",
"Frítest_bearbeiten (1).jpeg",
]
CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = [
@@ -186,6 +200,12 @@ CLI_EXPORT_FILENAMES_EDITED_SUFFIX_TEMPLATE = [
"screenshot-really-a-png.jpeg",
"winebottle.jpeg",
"winebottle (1).jpeg",
"Frítest.jpg",
"Frítest (1).jpg",
"Frítest (2).jpg",
"Frítest (3).jpg",
"Frítest_edited.jpeg",
"Frítest_edited (1).jpeg",
]
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [
@@ -212,6 +232,12 @@ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX = [
"screenshot-really-a-png_original.jpeg",
"winebottle_original.jpeg",
"winebottle_original (1).jpeg",
"Frítest_original.jpg",
"Frítest_original (1).jpg",
"Frítest_original (2).jpg",
"Frítest_original (3).jpg",
"Frítest_edited.jpeg",
"Frítest_edited (1).jpeg",
]
CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = [
@@ -238,6 +264,12 @@ CLI_EXPORT_FILENAMES_ORIGINAL_SUFFIX_TEMPLATE = [
"screenshot-really-a-png.jpeg",
"winebottle.jpeg",
"winebottle (1).jpeg",
"Frítest.jpg",
"Frítest (1).jpg",
"Frítest_original.jpg",
"Frítest_edited.jpeg",
"Frítest_original (1).jpg",
"Frítest_edited (1).jpeg",
]
CLI_EXPORT_FILENAMES_CURRENT = [
@@ -264,6 +296,12 @@ CLI_EXPORT_FILENAMES_CURRENT = [
"D1359D09-1373-4F3B-B0E3-1A4DE573E4A3.mp4",
"E2078879-A29C-4D6F-BACB-E3BBE6C3EB91.jpeg",
"52083079-73D5-4921-AC1B-FE76F279133F.jpeg",
"B13F4485-94E0-41CD-AF71-913095D62E31.jpeg", # Frítest.jpg
"1793FAAB-DE75-4E25-886C-2BD66C780D6A.jpeg", # Frítest.jpg
"1793FAAB-DE75-4E25-886C-2BD66C780D6A_edited.jpeg", # Frítest.jpg
"A8266C97-9BAF-4AF4-99F3-0013832869B8.jpeg", # Frítest.jpg
"D1D4040D-D141-44E8-93EA-E403D9F63E07.jpeg", # Frítest.jpg
"D1D4040D-D141-44E8-93EA-E403D9F63E07_edited.jpeg", # Frítest.jpg
]
CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = [
@@ -290,6 +328,12 @@ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG = [
"screenshot-really-a-png.jpeg",
"winebottle.jpeg",
"winebottle (1).jpeg",
"Frítest.jpg",
"Frítest (1).jpg",
"Frítest (2).jpg",
"Frítest (3).jpg",
"Frítest_edited (1).jpeg",
"Frítest_edited.jpeg",
]
CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = [
@@ -314,6 +358,12 @@ CLI_EXPORT_FILENAMES_CONVERT_TO_JPEG_SKIP_RAW = [
"screenshot-really-a-png.jpeg",
"winebottle.jpeg",
"winebottle (1).jpeg",
"Frítest.jpg",
"Frítest (1).jpg",
"Frítest (2).jpg",
"Frítest (3).jpg",
"Frítest_edited.jpeg",
"Frítest_edited (1).jpeg",
]
CLI_EXPORT_CONVERT_TO_JPEG_LARGE_FILE = "DSC03584.jpeg"
@@ -429,6 +479,7 @@ CLI_EXPORT_UUID = "D79B8D77-BFFC-460B-9312-034F2877D35B"
CLI_EXPORT_UUID_STATUE = "3DD2C897-F19E-4CA6-8C22-B027D5A71907"
CLI_EXPORT_UUID_KEYWORD_PATHSEP = "7783E8E6-9CAC-40F3-BE22-81FB7051C266"
CLI_EXPORT_UUID_LONG_DESCRIPTION = "8846E3E6-8AC8-4857-8448-E3D025784410"
CLI_EXPORT_UUID_MISSING = "8E1D7BC9-9321-44F9-8CFB-4083F6B9232A" # IMG_2000.JPG
CLI_EXPORT_UUID_FILENAME = "Pumkins2.jpg"
CLI_EXPORT_UUID_FILENAME_PREVIEW = "Pumkins2_preview.jpeg"
@@ -486,10 +537,10 @@ PHOTOS_NOT_IN_TRASH_LEN_14_6 = 12
PHOTOS_IN_TRASH_LEN_14_6 = 1
PHOTOS_MISSING_14_6 = 1
PHOTOS_NOT_IN_TRASH_LEN_15_7 = 19
PHOTOS_NOT_IN_TRASH_LEN_15_7 = 23
PHOTOS_IN_TRASH_LEN_15_7 = 2
PHOTOS_MISSING_15_7 = 2
PHOTOS_EDITED_15_7 = 4
PHOTOS_EDITED_15_7 = 6
CLI_PLACES_JSON = """{"places": {"_UNKNOWN_": 1, "Maui, Wailea, Hawai'i, United States": 1, "Washington, District of Columbia, United States": 1}}"""
@@ -645,6 +696,15 @@ KEYWORDS_JSON = {
"Val d'Isère": 2,
"Drink": 2,
"Wine Bottle": 2,
"Food": 2,
"Furniture": 2,
"Pizza": 2,
"Table": 2,
"Cloudy": 2,
"Cord": 2,
"Outdoor": 2,
"Sky": 2,
"Sunset Sunrise": 2,
}
}
@@ -762,6 +822,10 @@ UUID_NOT_IN_ALBUM = [
"8846E3E6-8AC8-4857-8448-E3D025784410",
"7F74DD34-5920-4DA3-B284-479887A34F66",
"52083079-73D5-4921-AC1B-FE76F279133F",
"B13F4485-94E0-41CD-AF71-913095D62E31", # Frítest.jpg
"1793FAAB-DE75-4E25-886C-2BD66C780D6A", # Frítest.jpg
"A8266C97-9BAF-4AF4-99F3-0013832869B8", # Frítest.jpg
"D1D4040D-D141-44E8-93EA-E403D9F63E07", # Frítest.jpg
]
UUID_DUPLICATES = [
@@ -793,6 +857,28 @@ UUID_DICT_FOLDER_ALBUM_SEQ = {
},
}
UUID_EMPTY_TITLE = "7783E8E6-9CAC-40F3-BE22-81FB7051C266" # IMG_3092.heic
FILENAME_EMPTY_TITLE = "IMG_3092.heic"
DESCRIPTION_TEMPLATE_EMPTY_TITLE = "{title,No Title} and {descr,No Descr}"
DESCRIPTION_VALUE_EMPTY_TITLE = "No Title and No Descr"
DESCRIPTION_TEMPLATE_TITLE_CONDITIONAL = "{title?true,false}"
DESCRIPTION_VALUE_TITLE_CONDITIONAL = "false"
UUID_UNICODE_TITLE = [
"B13F4485-94E0-41CD-AF71-913095D62E31", # Frítest.jpg
"1793FAAB-DE75-4E25-886C-2BD66C780D6A", # Frítest.jpg
"A8266C97-9BAF-4AF4-99F3-0013832869B8", # Frítest.jpg
"D1D4040D-D141-44E8-93EA-E403D9F63E07", # Frítest.jpg
]
EXPORT_UNICODE_TITLE_FILENAMES = [
"Frítest.jpg",
"Frítest (1).jpg",
"Frítest (2).jpg",
"Frítest (3).jpg",
]
def modify_file(filename):
"""appends data to a file to modify it"""
@@ -1290,6 +1376,48 @@ def test_export_preview():
assert CLI_EXPORT_UUID_FILENAME_PREVIEW in files
def test_export_preview_file_exists():
"""test export with --preview when preview images already exist, issue #516"""
import glob
import os
import os.path
import osxphotos
from osxphotos.cli import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"-V",
"--preview",
"--uuid",
CLI_EXPORT_UUID_MISSING,
],
)
assert result.exit_code == 0
# export again
result = runner.invoke(
export,
[
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"-V",
"--preview",
"--uuid",
CLI_EXPORT_UUID_MISSING,
],
)
assert result.exit_code == 0
assert "Error exporting photo" not in result.output
def test_export_preview_suffix():
"""test export with --preview and --preview-suffix"""
import glob
@@ -2146,6 +2274,51 @@ def test_export_duplicate():
assert len(files) == len(UUID_DUPLICATES)
def test_export_duplicate_unicode_filenames():
# test issue #515
import glob
import os
import os.path
import osxphotos
from osxphotos.cli import export
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
uuid = []
for u in UUID_UNICODE_TITLE:
uuid.append("--uuid")
uuid.append(u)
with runner.isolated_filesystem():
result = runner.invoke(
export,
[
os.path.join(cwd, CLI_PHOTOS_DB),
".",
"--convert-to-jpeg",
"--edited-suffix",
"",
"--filename",
"{title,{original_name}}",
"--jpeg-ext",
"jpg",
"--person-keyword",
"--skip-bursts",
"--skip-live",
"--skip-original-if-edited",
"--touch-file",
"--strip",
*uuid,
"-V",
],
)
assert result.exit_code == 0
assert "exported: 4" in result.output
files = glob.glob("*")
assert sorted(files) == sorted(EXPORT_UNICODE_TITLE_FILENAMES)
def test_query_date_1():
"""Test --from-date and --to-date"""
import json
@@ -4230,7 +4403,7 @@ def test_export_update_basic():
)
assert result.exit_code == 0
assert (
"Processed: 19 photos, exported: 0, updated: 0, skipped: 23, updated EXIF data: 0, missing: 2, error: 0"
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 2, error: 0"
in result.output
)
@@ -4314,7 +4487,7 @@ def test_export_update_exiftool():
)
assert result.exit_code == 0
assert (
"Processed: 19 photos, exported: 0, updated: 23, skipped: 0, updated EXIF data: 23, missing: 2, error: 1"
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, skipped: 0, updated EXIF data: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, missing: 2, error: 1"
in result.output
)
@@ -4324,7 +4497,7 @@ def test_export_update_exiftool():
)
assert result.exit_code == 0
assert (
"Processed: 19 photos, exported: 0, updated: 0, skipped: 23, updated EXIF data: 0, missing: 2, error: 0"
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: 0, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, updated EXIF data: 0, missing: 2, error: 0"
in result.output
)
@@ -4361,7 +4534,7 @@ def test_export_update_hardlink():
)
assert result.exit_code == 0
assert (
"Processed: 19 photos, exported: 0, updated: 23, skipped: 0, updated EXIF data: 0, missing: 2, error: 0"
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, skipped: 0, updated EXIF data: 0, missing: 2, error: 0"
in result.output
)
assert not os.path.samefile(CLI_EXPORT_UUID_FILENAME, photo.path)
@@ -4400,7 +4573,7 @@ def test_export_update_hardlink_exiftool():
)
assert result.exit_code == 0
assert (
"Processed: 19 photos, exported: 0, updated: 23, skipped: 0, updated EXIF data: 23, missing: 2, error: 1"
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, skipped: 0, updated EXIF data: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, missing: 2, error: 1"
in result.output
)
assert not os.path.samefile(CLI_EXPORT_UUID_FILENAME, photo.path)
@@ -4486,7 +4659,7 @@ def test_export_update_only_new():
],
)
assert result.exit_code == 0
assert "exported: 1" in result.output
assert "exported: 7" in result.output
# --update with --only-new
result = runner.invoke(
@@ -4494,7 +4667,7 @@ def test_export_update_only_new():
[os.path.join(cwd, PHOTOS_DB_15_7), ".", "-V", "--update", "--only-new"],
)
assert result.exit_code == 0
assert "exported: 1" in result.output
assert "exported: 7" in result.output
# --update with --only-new, should export nothing
result = runner.invoke(
@@ -4536,7 +4709,7 @@ def test_export_update_no_db():
# edited files will be re-exported because there won't be an edited signature
# in the database
assert (
"Processed: 19 photos, exported: 0, updated: 4, skipped: 19, updated EXIF data: 0, missing: 2, error: 0"
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: 0, updated: {PHOTOS_EDITED_15_7}, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7}, updated EXIF data: 0, missing: 2, error: 0"
in result.output
)
assert os.path.isfile(OSXPHOTOS_EXPORT_DB)
@@ -4576,7 +4749,8 @@ def test_export_then_hardlink():
)
assert result.exit_code == 0
assert (
"Processed: 19 photos, exported: 23, missing: 2, error: 0" in result.output
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, missing: 2, error: 0"
in result.output
)
assert os.path.samefile(CLI_EXPORT_UUID_FILENAME, photo.path)
@@ -4589,6 +4763,7 @@ def test_export_dry_run():
import osxphotos
from osxphotos.cli import export
from osxphotos.utils import normalize_fs_path
runner = CliRunner()
cwd = os.getcwd()
@@ -4599,11 +4774,12 @@ def test_export_dry_run():
)
assert result.exit_code == 0
assert (
"Processed: 19 photos, exported: 23, missing: 2, error: 0" in result.output
f"Processed: {PHOTOS_NOT_IN_TRASH_LEN_15_7} photos, exported: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}, missing: 2, error: 0"
in result.output
)
for filepath in CLI_EXPORT_FILENAMES_DRY_RUN:
assert re.search(r"Exported.*" + f"{filepath}", result.output)
assert not os.path.isfile(filepath)
assert not os.path.isfile(normalize_fs_path(filepath))
def test_export_update_edits_dry_run():
@@ -4680,7 +4856,10 @@ def test_export_directory_template_1_dry_run():
],
)
assert result.exit_code == 0
assert "exported: 23" in result.output
assert (
f"exported: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}"
in result.output
)
workdir = os.getcwd()
for filepath in CLI_EXPORTED_DIRECTORY_TEMPLATE_FILENAMES1:
assert re.search(r"Exported.*" + f"{filepath}", result.output)
@@ -4716,8 +4895,14 @@ def test_export_touch_files():
)
assert result.exit_code == 0
assert "exported: 23" in result.output
assert "touched date: 21" in result.output
assert (
f"exported: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}"
in result.output
)
assert (
f"touched date: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7-2}"
in result.output
)
for fname, mtime in zip(CLI_EXPORT_BY_DATE, CLI_EXPORT_BY_DATE_TOUCH_TIMES):
st = os.stat(fname)
@@ -4749,7 +4934,10 @@ def test_export_touch_files_update():
)
assert result.exit_code == 0
assert "exported: 23" in result.output
assert (
f"exported: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}"
in result.output
)
assert not pathlib.Path(CLI_EXPORT_BY_DATE[0]).is_file()
@@ -4759,7 +4947,10 @@ def test_export_touch_files_update():
)
assert result.exit_code == 0
assert "exported: 23" in result.output
assert (
f"exported: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}"
in result.output
)
assert pathlib.Path(CLI_EXPORT_BY_DATE[0]).is_file()
@@ -4770,7 +4961,10 @@ def test_export_touch_files_update():
)
assert result.exit_code == 0
assert "skipped: 23" in result.output
assert (
f"skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}"
in result.output
)
# --update --touch-file --dry-run
result = runner.invoke(
@@ -4785,8 +4979,14 @@ def test_export_touch_files_update():
],
)
assert result.exit_code == 0
assert "skipped: 23" in result.output
assert "touched date: 21" in result.output
assert (
f"skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}"
in result.output
)
assert (
f"touched date: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7-2}"
in result.output
)
for fname, mtime in zip(
CLI_EXPORT_BY_DATE_NEED_TOUCH, CLI_EXPORT_BY_DATE_NEED_TOUCH_TIMES
@@ -4806,8 +5006,14 @@ def test_export_touch_files_update():
],
)
assert result.exit_code == 0
assert "skipped: 23" in result.output
assert "touched date: 21" in result.output
assert (
f"skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}"
in result.output
)
assert (
f"touched date: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7-2}"
in result.output
)
for fname, mtime in zip(
CLI_EXPORT_BY_DATE_NEED_TOUCH, CLI_EXPORT_BY_DATE_NEED_TOUCH_TIMES
@@ -4830,7 +5036,10 @@ def test_export_touch_files_update():
],
)
assert result.exit_code == 0
assert "updated: 1, skipped: 22" in result.output
assert (
f"updated: 1, skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7-1}"
in result.output
)
assert "touched date: 1" in result.output
for fname, mtime in zip(CLI_EXPORT_BY_DATE, CLI_EXPORT_BY_DATE_TOUCH_TIMES):
@@ -4844,7 +5053,10 @@ def test_export_touch_files_update():
)
assert result.exit_code == 0
assert "skipped: 23" in result.output
assert (
f"skipped: {PHOTOS_NOT_IN_TRASH_LEN_15_7+PHOTOS_EDITED_15_7}"
in result.output
)
@pytest.mark.skip("TODO: This fails on some machines but not all")
@@ -6484,7 +6696,7 @@ def test_query_min_size_1():
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 2
assert len(json_got) == 4
def test_query_min_size_2():
@@ -6511,7 +6723,7 @@ def test_query_min_size_2():
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 2
assert len(json_got) == 4
def test_query_max_size_1():
@@ -6532,7 +6744,7 @@ def test_query_max_size_1():
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
assert len(json_got) == 3
def test_query_max_size_2():
@@ -6553,7 +6765,7 @@ def test_query_max_size_2():
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 1
assert len(json_got) == 3
def test_query_min_max_size():
@@ -7121,3 +7333,77 @@ def test_export_album_seq():
f"{UUID_DICT_FOLDER_ALBUM_SEQ[uuid]['album']}/{UUID_DICT_FOLDER_ALBUM_SEQ[uuid]['result']}"
in files
)
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
def test_export_description_template():
"""Test for issue #506"""
import json
import os
import os.path
import osxphotos
from osxphotos.cli import cli
from osxphotos.exiftool import ExifTool
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
cli,
[
"export",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"--sidecar=json",
f"--uuid={UUID_EMPTY_TITLE}",
"-V",
"--description-template",
DESCRIPTION_TEMPLATE_EMPTY_TITLE,
"--exiftool",
],
)
assert result.exit_code == 0
exif = ExifTool(FILENAME_EMPTY_TITLE).asdict()
assert exif["EXIF:ImageDescription"] == DESCRIPTION_VALUE_EMPTY_TITLE
def test_export_description_template_conditional():
"""Test for issue #506"""
import json
import os
import os.path
import osxphotos
from osxphotos.cli import cli
from osxphotos.exiftool import ExifTool
import json
runner = CliRunner()
cwd = os.getcwd()
# pylint: disable=not-context-manager
with runner.isolated_filesystem():
result = runner.invoke(
cli,
[
"export",
"--db",
os.path.join(cwd, PHOTOS_DB_15_7),
".",
"--sidecar=json",
f"--uuid={UUID_EMPTY_TITLE}",
"-V",
"--description-template",
DESCRIPTION_TEMPLATE_TITLE_CONDITIONAL,
"--sidecar",
"JSON",
],
)
assert result.exit_code == 0
with open(f"{FILENAME_EMPTY_TITLE}.json", "r") as fp:
json_got = json.load(fp)[0]
assert (
json_got["EXIF:ImageDescription"] == DESCRIPTION_VALUE_TITLE_CONDITIONAL
)

View File

@@ -0,0 +1,43 @@
# Test cloud photos and album owner
import pytest
import osxphotos
PHOTOS_DB_CLOUD = "./tests/Test-Cloud-10.15.6.photoslibrary/"
PHOTOS_DB_NOT_CLOUD = "./tests/Test-10.15.6.photoslibrary/"
UUID_DICT = {
"not_cloudasset": "6191423D-8DB8-4D4C-92BE-9BBBA308AAC4",
"owner": "7572C53E-1D6A-410C-A2B1-18CCA3B5AD9F",
}
@pytest.fixture(scope="module")
def photosdb_cloud():
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_CLOUD)
@pytest.fixture(scope="module")
def photosdb_nocloud():
return osxphotos.PhotosDB(dbfile=PHOTOS_DB_NOT_CLOUD)
def test_album_owner_cloud(photosdb_cloud):
album = [a for a in photosdb_cloud.album_info_shared if a.title == "osxphotos"][0]
assert album.owner == "Rhet Turnbull"
def test_album_owner_not_cloud(photosdb_nocloud):
album = [a for a in photosdb_nocloud.album_info if a.title == "Test Album"][0]
assert album.owner is None
def test_photo_owner_cloud(photosdb_cloud):
photo = photosdb_cloud.get_photo(UUID_DICT["owner"])
assert photo.owner == "Rhet Turnbull"
def test_photo_owner_nocloud(photosdb_nocloud):
photo = photosdb_nocloud.get_photo(UUID_DICT["not_cloudasset"])
assert photo.owner is None

Some files were not shown because too many files have changed in this diff Show More