Compare commits

...

158 Commits

Author SHA1 Message Date
Rhet Turnbull
42426b95ee Bug fix for #559 2021-12-31 09:35:12 -08:00
Rhet Turnbull
262a6f31e7 Updated CHANGELOG.md [skip ci] 2021-12-31 08:39:18 -08:00
Rhet Turnbull
04930c3644 Added --skip-uuid, --skip-uuid-from-file, #563 2021-12-31 08:35:26 -08:00
Rhet Turnbull
44594a8e43 Added support for projects, implements #559 2021-12-31 07:30:20 -08:00
Rhet Turnbull
690d981f31 Fixed test for #561 2021-12-30 15:45:32 -08:00
Rhet Turnbull
06ea8d1e6c Updated CHANGELOG.md [skip ci] 2021-12-29 11:24:29 -08:00
Rhet Turnbull
c4e3c5a8be Updated docs [skip ci] 2021-12-28 17:37:00 -08:00
Rhet Turnbull
03f4e7cc34 Fix for accented characters in album names, #561 2021-12-28 17:25:50 -08:00
Rhet Turnbull
0e54a08ae0 Updated docs [skip ci] 2021-12-26 20:09:10 -08:00
Rhet Turnbull
b71c752e9d Added get_photos_library_version 2021-12-26 19:57:33 -08:00
Rhet Turnbull
521848f955 Added export test for --exif 2021-12-25 05:53:26 -08:00
Rhet Turnbull
debb17c952 Implement #323 2021-12-25 05:41:37 -08:00
Rhet Turnbull
7819740f70 Fixed #463 2021-12-24 18:12:02 -08:00
Rhet Turnbull
b9ffb0d8de Fixed helped text, #493 2021-12-24 18:08:18 -08:00
Rhet Turnbull
d59852f594 Updated tests 2021-12-24 17:21:13 -08:00
Rhet Turnbull
085f482820 Added install/uninstall commands, #531 2021-12-24 17:05:01 -08:00
Rhet Turnbull
1cb8da96ce Updated dev_requirements.txt 2021-12-22 19:08:25 -08:00
Rhet Turnbull
50016a9eca Updated requirements_dev.txt 2021-12-22 18:10:15 -08:00
Rhet Turnbull
924f7325b4 Removed redundant dev_requirements.txt 2021-12-22 18:08:44 -08:00
Rhet Turnbull
181f678d9e Updated docs [skip ci] 2021-12-22 08:22:02 -08:00
Rhet Turnbull
6ce1b83ca2 Version bump 2021-12-21 09:39:05 -08:00
Rhet Turnbull
a08a653f20 Partial fix for #556 2021-12-21 09:36:42 -08:00
Rhet Turnbull
e1f1772080 Updated all-contributors 2021-12-16 22:11:52 -08:00
Andrew Louis
d2a1f792e9 Adds missing f-string to retry message (#553) 2021-12-16 17:23:33 -08:00
Rhet Turnbull
e7bd80e05f Update issue templates 2021-12-11 22:24:38 -08:00
Rhet Turnbull
9089c0323c Updated CHANGELOG.md [skip ci] 2021-12-10 20:40:40 -08:00
Rhet Turnbull
197e5663df Updated docs 2021-12-10 20:26:00 -08:00
Rhet Turnbull
f6dedaa619 Updated docs [skip ci] 2021-12-10 20:09:20 -08:00
Rhet Turnbull
0906dbe637 Fixed error for missing photo path, #547 2021-12-10 19:36:25 -08:00
Rhet Turnbull
0629b3f6d6 Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-12-10 19:26:29 -08:00
Rhet Turnbull
55dfc0ec7d Fixed typo, thanks to @hyfen 2021-12-10 19:26:21 -08:00
Andrew Louis
b7f8b26f1d Fixes typo in README (#548) 2021-12-10 19:23:56 -08:00
Rhet Turnbull
9ca7dd50bc Updated all-contributors 2021-12-10 19:23:12 -08:00
allcontributors[bot]
0e73d57bdf docs: add alandefreitas as a contributor for bug (#551)
* 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-12-10 19:08:32 -08:00
Rhet Turnbull
9de2c17e47 Added additional try/except in _photoinfo_export.py 2021-12-06 06:16:42 -08:00
Rhet Turnbull
1bae6d33f1 Updated to moment processing for consistency 2021-12-06 06:07:55 -08:00
Rhet Turnbull
a52b4d2f43 Added MomentInfo for Photos 5+, #71 2021-12-05 19:12:37 -08:00
Rhet Turnbull
3e038bf124 Added test library for Monterey on M1 2021-12-05 08:41:08 -08:00
Rhet Turnbull
870ed9c435 Updated README.md for Monterey 2021-12-04 10:46:06 -08:00
allcontributors[bot]
68e7ca3277 docs: add dgleich as a contributor for code (#541)
* 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-11-25 10:06:35 -08:00
Rhet Turnbull
c9142c2156 Updated CHANGELOG.md [skip ci] 2021-11-25 10:01:54 -08:00
Rhet Turnbull
7d923590ae Updated dependencies for pyobjc 8.0 2021-11-25 08:34:55 -08:00
Rhet Turnbull
5383ced1ca Updated CHANGELOG.md [skip ci] 2021-11-11 11:05:19 -08:00
Rhet Turnbull
0e6c92dbd9 Fix for --use-photokit with --skip-live, #537 2021-11-11 10:54:48 -08:00
Rhet Turnbull
b00978c61a Updated CHANGELOG.md [skip ci] 2021-11-07 21:35:46 -08:00
Rhet Turnbull
fb583e28e0 Updated docs [skip ci] 2021-11-07 21:31:27 -08:00
Rhet Turnbull
760386e3d7 Updated tested versions 2021-11-07 21:21:50 -08:00
Rhet Turnbull
51ba54971a Test fixes for Monterey/M1 2021-11-07 08:33:08 -08:00
Rhet Turnbull
2ffcf1e82b Updated OTL to MTL 2021-11-06 07:13:02 -07:00
Rhet Turnbull
818f4f45a4 Dependency update for Monterey 2021-10-30 07:37:23 -07:00
Rhet Turnbull
2cf19f6af1 Updated docs [skip ci] 2021-10-30 07:25:26 -07:00
Rhet Turnbull
ef82c6e32b Updated for Monterey 12.0.1 release 2021-10-28 22:05:17 -07:00
Rhet Turnbull
0e9b9d6251 Updated docs [skip ci] 2021-10-15 05:45:17 -07:00
Rhet Turnbull
419b34ea73 Fix for #526 with --update 2021-10-15 05:36:41 -07:00
Rhet Turnbull
f64c4ed374 Fixed FileUtil to use correct import 2021-10-14 21:29:45 -07:00
Rhet Turnbull
1677f404d2 Updated CHANGELOG.md [skip ci] 2021-10-11 18:07:36 -07:00
allcontributors[bot]
a612a363ed docs: add spencerc99 as a contributor for bug (#527)
* 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-10-11 18:03:50 -07:00
Rhet Turnbull
202bc1144b Fix for #526 2021-10-11 17:50:07 -07:00
Rhet Turnbull
a0c654e43f Updated README.md [skip ci] 2021-10-11 17:03:45 -07:00
Rhet Turnbull
2bb677dc19 Updated docs [skip ci] 2021-10-11 16:52:22 -07:00
Rhet Turnbull
e33805fe42 Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-10-11 16:00:05 -07:00
Rhet Turnbull
04ac0a1121 Fix for #524 2021-10-11 15:59:40 -07:00
Rhet Turnbull
d2b0bd4e28 Fix for #525 2021-10-11 15:59:02 -07:00
allcontributors[bot]
d754899563 docs: add oPromessa as a contributor for bug (#525)
* 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-10-11 15:55:49 -07:00
Rhet Turnbull
4a81e643a7 Updated CHANGELOG.md [skip ci] 2021-10-11 07:35:51 -07:00
Rhet Turnbull
b23e74f8f5 Updated docs [skip ci] 2021-10-11 07:32:15 -07:00
Rhet Turnbull
5dc766249a Updated dependencies 2021-10-10 23:05:53 -07:00
Rhet Turnbull
a895833c7f Updated dependencies 2021-10-10 22:27:00 -07:00
Rhet Turnbull
3f81a3c179 Added python 3.10 to supported versions 2021-10-09 09:39:23 -07:00
Rhet Turnbull
f1235f745f Updated CHANGELOG.md [skip ci] 2021-09-30 06:13:40 -07:00
Rhet Turnbull
1ddb1de998 Updated docs [skip ci] 2021-09-30 06:11:00 -07:00
Rhet Turnbull
c472698b1d Updated REPL, now with more cowbell 2021-09-30 05:59:03 -07:00
Rhet Turnbull
4e021a0731 Updated CHANGELOG.md [skip ci] 2021-09-26 15:57:07 -07:00
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
Rhet Turnbull
673243c6cd Fix for #500, check for macOS version before loading Vision 2021-07-29 21:16:33 -07:00
Rhet Turnbull
7376223eb8 Updated text_detection to detect macOS version 2021-07-29 07:39:01 -07:00
Rhet Turnbull
ecd0b8e22f Updated detected_text docs to make it clear this only works on Catalina+ 2021-07-29 07:03:04 -07:00
Rhet Turnbull
c4a608b5bd Updated CHANGELOG.md [skip ci] 2021-07-29 07:02:38 -07:00
Rhet Turnbull
4d9e21ea16 Added error logging to PhotoInfo.detected_text 2021-07-29 06:32:07 -07:00
Rhet Turnbull
1ee3e035c4 Updated README.md [skip ci] 2021-07-29 06:25:59 -07:00
Rhet Turnbull
b1c0fb3e82 Added error logging to {detected_text} processing, #499 2021-07-29 06:23:02 -07:00
Rhet Turnbull
de715d2afd Updated CHANGELOG.md [skip ci] 2021-07-28 06:36:10 -07:00
Rhet Turnbull
607cf80dda Removed unneeded test file [skip ci] 2021-07-28 06:27:44 -07:00
Rhet Turnbull
0c8fbd69af Updated dependencies 2021-07-28 06:21:21 -07:00
Rhet Turnbull
c2335236be Added {detected_text} template 2021-07-27 06:08:49 -07:00
Rhet Turnbull
123340eada Added PhotoInfo.detected_text() 2021-07-25 18:34:59 -07:00
Rhet Turnbull
852a06f99b Updated docs 2021-07-24 21:30:52 -07:00
Rhet Turnbull
9f8da5c623 Updated CHANGELOG.md [skip ci] 2021-07-24 21:30:03 -07:00
Rhet Turnbull
077d577c98 Fixed {album_seq} and {folder_album_seq} help text 2021-07-24 20:53:59 -07:00
Rhet Turnbull
12f39dbaf5 Added {album_seq} and {folder_album_seq}, #496 2021-07-24 20:41:31 -07:00
Rhet Turnbull
6e9f709279 Updated CHANGELOG.md [skip ci] 2021-07-23 06:14:35 -07:00
Rhet Turnbull
666b6cac33 Updated docs 2021-07-23 06:11:29 -07:00
Rhet Turnbull
e95c096784 Added {id} sequence number template, #154 2021-07-23 05:57:07 -07:00
Rhet Turnbull
745161fbd1 Updated CHANGELOG.md [skip ci] 2021-07-20 06:26:32 -07:00
Rhet Turnbull
8216c33b59 Updated example [skip ci] 2021-07-20 06:25:50 -07:00
539 changed files with 10093 additions and 6756 deletions

View File

@@ -241,6 +241,69 @@
"contributions": [
"data"
]
},
{
"login": "dssinger",
"name": "David Singer",
"avatar_url": "https://avatars.githubusercontent.com/u/1817903?v=4",
"profile": "https://github.com/dssinger",
"contributions": [
"bug"
]
},
{
"login": "oPromessa",
"name": "oPromessa",
"avatar_url": "https://avatars.githubusercontent.com/u/21261491?v=4",
"profile": "https://github.com/oPromessa",
"contributions": [
"bug"
]
},
{
"login": "spencerc99",
"name": "Spencer Chang",
"avatar_url": "https://avatars.githubusercontent.com/u/14796580?v=4",
"profile": "http://spencerchang.me",
"contributions": [
"bug"
]
},
{
"login": "dgleich",
"name": "David Gleich",
"avatar_url": "https://avatars.githubusercontent.com/u/33995?v=4",
"profile": "https://www.cs.purdue.edu/homes/dgleich",
"contributions": [
"code"
]
},
{
"login": "alandefreitas",
"name": "Alan de Freitas",
"avatar_url": "https://avatars.githubusercontent.com/u/5369819?v=4",
"profile": "https://alandefreitas.github.io/alandefreitas/",
"contributions": [
"bug"
]
},
{
"login": "hyfen",
"name": "Andrew Louis",
"avatar_url": "https://avatars.githubusercontent.com/u/6291?v=4",
"profile": "https://hyfen.net",
"contributions": [
"doc", "code"
]
},
{
"login": "neebah",
"name": "neebah",
"avatar_url": "https://avatars.githubusercontent.com/u/71442026?v=4",
"profile": "https://github.com/neebah",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
** Before submitting a bug report, please ensure you are running the most recent version of osxphotos and that the bug is reproducible on the latest version **
- If you installed with pipx: `pipx upgrade osxphotos`
- If you installed with pip: `pip install --upgrade osxphotos`
- If you installed the pre-built binary, download and install the latest [release](https://github.com/RhetTbull/osxphotos/releases)
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. What' the full command line you used with osxphotos?
2. What was the error output?
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. which version of macOS]
- osxphotos version (`osxphotos --version`)
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

1
.gitignore vendored
View File

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

View File

@@ -4,6 +4,286 @@ 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.44.1](https://github.com/RhetTbull/osxphotos/compare/v0.44.0...v0.44.1)
> 31 December 2021
- Added --skip-uuid, --skip-uuid-from-file, #563 [`04930c3`](https://github.com/RhetTbull/osxphotos/commit/04930c3644da99c1923c4e3aaa9213902aeadfd1)
#### [v0.44.0](https://github.com/RhetTbull/osxphotos/compare/v0.43.9...v0.44.0)
> 31 December 2021
- Added support for projects, implements #559 [`44594a8`](https://github.com/RhetTbull/osxphotos/commit/44594a8e437c20bae6fd8eecb74075d49da4b91f)
- Updated docs [skip ci] [`c4e3c5a`](https://github.com/RhetTbull/osxphotos/commit/c4e3c5a8beac1db00533f7820ab8249cf351aef0)
- Fixed test for #561 [`690d981`](https://github.com/RhetTbull/osxphotos/commit/690d981f310b083f5f58407cc879bca494730765)
#### [v0.43.9](https://github.com/RhetTbull/osxphotos/compare/v0.43.8...v0.43.9)
> 28 December 2021
- Fix for accented characters in album names, #561 [`03f4e7c`](https://github.com/RhetTbull/osxphotos/commit/03f4e7cc3473c276dfd7c7e6ad64e4dfe5b32011)
#### [v0.43.8](https://github.com/RhetTbull/osxphotos/compare/v0.43.7...v0.43.8)
> 26 December 2021
- Fixed #463 [`#463`](https://github.com/RhetTbull/osxphotos/issues/463)
- Updated docs [skip ci] [`181f678`](https://github.com/RhetTbull/osxphotos/commit/181f678d9eda8bc8acca11b4ebd470900f30bdcb)
- Added install/uninstall commands, #531 [`085f482`](https://github.com/RhetTbull/osxphotos/commit/085f482820af2d51f0d411c7e8a7a27329bf0722)
- Implement #323 [`debb17c`](https://github.com/RhetTbull/osxphotos/commit/debb17c9520bec25d725426feaa512745e9d4ec0)
- Updated docs [skip ci] [`0e54a08`](https://github.com/RhetTbull/osxphotos/commit/0e54a08ae07853c4cdb2c548bdba27335cfc32ba)
- Added get_photos_library_version [`b71c752`](https://github.com/RhetTbull/osxphotos/commit/b71c752e9d2c59412baf812bfc50e6358ea3f02e)
#### [v0.43.7](https://github.com/RhetTbull/osxphotos/compare/v0.43.6...v0.43.7)
> 21 December 2021
- Adds missing f-string to retry message [`#553`](https://github.com/RhetTbull/osxphotos/pull/553)
- Update issue templates [`e7bd80e`](https://github.com/RhetTbull/osxphotos/commit/e7bd80e05f94238fd41e478e32c1709b442eb361)
- Partial fix for #556 [`a08a653`](https://github.com/RhetTbull/osxphotos/commit/a08a653f202a49853780ab4a686bf3dfbc32a491)
- Updated all-contributors [`e1f1772`](https://github.com/RhetTbull/osxphotos/commit/e1f1772080d24373ceb5791683615451cd390874)
- Version bump [`6ce1b83`](https://github.com/RhetTbull/osxphotos/commit/6ce1b83ca2c7f0c6f9c86757602b81df1d9bf453)
#### [v0.43.6](https://github.com/RhetTbull/osxphotos/compare/v0.43.5...v0.43.6)
> 10 December 2021
- Fixes typo in README [`#548`](https://github.com/RhetTbull/osxphotos/pull/548)
- docs: add alandefreitas as a contributor for bug [`#551`](https://github.com/RhetTbull/osxphotos/pull/551)
- docs: add dgleich as a contributor for code [`#541`](https://github.com/RhetTbull/osxphotos/pull/541)
- Updated docs [`197e566`](https://github.com/RhetTbull/osxphotos/commit/197e5663df058a013ce2d6f8c5fd7ff71a5cc46e)
- Added test library for Monterey on M1 [`3e038bf`](https://github.com/RhetTbull/osxphotos/commit/3e038bf124b98d6b74f19dd4db0f8f1e3c48e787)
- Updated docs [skip ci] [`f6dedaa`](https://github.com/RhetTbull/osxphotos/commit/f6dedaa6197dc244616c5b4e9e8ce42ce6b7a252)
- Added MomentInfo for Photos 5+, #71 [`a52b4d2`](https://github.com/RhetTbull/osxphotos/commit/a52b4d2f43970086bf25659bd58dc8479b841704)
- Fixed error for missing photo path, #547 [`0906dbe`](https://github.com/RhetTbull/osxphotos/commit/0906dbe6370922b4c9649350014ed8a21d29c4fd)
#### [v0.43.5](https://github.com/RhetTbull/osxphotos/compare/v0.43.4...v0.43.5)
> 25 November 2021
- Updated dependencies for pyobjc 8.0 [`7d92359`](https://github.com/RhetTbull/osxphotos/commit/7d923590ae4df941b1b9d35c21937c03eb7b4284)
#### [v0.43.4](https://github.com/RhetTbull/osxphotos/compare/v0.43.3...v0.43.4)
> 11 November 2021
- Fix for --use-photokit with --skip-live, #537 [`0e6c92d`](https://github.com/RhetTbull/osxphotos/commit/0e6c92dbd951dd0e63cfb8b6d64e6ab96ece5955)
#### [v0.43.3](https://github.com/RhetTbull/osxphotos/compare/v0.43.1...v0.43.3)
> 7 November 2021
- Updated docs [skip ci] [`fb583e2`](https://github.com/RhetTbull/osxphotos/commit/fb583e28e0fc2c23bf24052db8a5ee669d8c92f5)
- Updated OTL to MTL [`2ffcf1e`](https://github.com/RhetTbull/osxphotos/commit/2ffcf1e82bfc013a4a9e0e7a709a7c1395c074ce)
- Test fixes for Monterey/M1 [`51ba549`](https://github.com/RhetTbull/osxphotos/commit/51ba54971a874cfce00368aa5be5380b3439c254)
#### [v0.43.1](https://github.com/RhetTbull/osxphotos/compare/v0.43.0...v0.43.1)
> 30 October 2021
- Dependency update for Monterey [`818f4f4`](https://github.com/RhetTbull/osxphotos/commit/818f4f45a4ce520b0ba1c688eabd2f4311be9540)
- Updated docs [skip ci] [`2cf19f6`](https://github.com/RhetTbull/osxphotos/commit/2cf19f6af1a03767e4d53eee556c4d3ed9af1776)
#### [v0.43.0](https://github.com/RhetTbull/osxphotos/compare/v0.42.94...v0.43.0)
> 28 October 2021
- Updated for Monterey 12.0.1 release [`ef82c6e`](https://github.com/RhetTbull/osxphotos/commit/ef82c6e32b536b0677530133892f95b852c6dce0)
#### [v0.42.94](https://github.com/RhetTbull/osxphotos/compare/v0.42.93...v0.42.94)
> 15 October 2021
- docs: add spencerc99 as a contributor for bug [`#527`](https://github.com/RhetTbull/osxphotos/pull/527)
- Fix for #526 with --update [`419b34e`](https://github.com/RhetTbull/osxphotos/commit/419b34ea73f15ccbe29f51896e11e9735ea5786b)
- Updated docs [skip ci] [`0e9b9d6`](https://github.com/RhetTbull/osxphotos/commit/0e9b9d625190b94c1dd68276e3b0e5367002d87c)
- Fixed FileUtil to use correct import [`f64c4ed`](https://github.com/RhetTbull/osxphotos/commit/f64c4ed374c120a95fe8adea26bd44852ca67e31)
#### [v0.42.93](https://github.com/RhetTbull/osxphotos/compare/v0.42.92...v0.42.93)
> 11 October 2021
- Fix for #526 [`202bc11`](https://github.com/RhetTbull/osxphotos/commit/202bc1144bc842ddec825eef0745830d56170aba)
- Updated README.md [skip ci] [`a0c654e`](https://github.com/RhetTbull/osxphotos/commit/a0c654e43f4aa5389a96c3c84fd7037c33d23404)
#### [v0.42.92](https://github.com/RhetTbull/osxphotos/compare/v0.42.91...v0.42.92)
> 11 October 2021
- docs: add oPromessa as a contributor for bug [`#525`](https://github.com/RhetTbull/osxphotos/pull/525)
- Fix for #524 [`04ac0a1`](https://github.com/RhetTbull/osxphotos/commit/04ac0a11215b275178013e60c6a61b9f1b3603c9)
- Fix for #525 [`d2b0bd4`](https://github.com/RhetTbull/osxphotos/commit/d2b0bd4e28cfdf3c930aa6ae3317549327b0e29c)
- Updated docs [skip ci] [`2bb677d`](https://github.com/RhetTbull/osxphotos/commit/2bb677dc19abaf254bc66e2cd788676e0613e548)
#### [v0.42.91](https://github.com/RhetTbull/osxphotos/compare/v0.42.90...v0.42.91)
> 11 October 2021
- Updated docs [skip ci] [`b23e74f`](https://github.com/RhetTbull/osxphotos/commit/b23e74f8f5a8387564108c330c3f8ac11189860d)
- Added python 3.10 to supported versions [`3f81a3c`](https://github.com/RhetTbull/osxphotos/commit/3f81a3c179dde37e9811ef19c847920bb3bd514c)
- Updated dependencies [`a895833`](https://github.com/RhetTbull/osxphotos/commit/a895833c7f0a264488e671f1735f9e10d2618e2d)
#### [v0.42.90](https://github.com/RhetTbull/osxphotos/compare/v0.42.89...v0.42.90)
> 30 September 2021
- Updated REPL, now with more cowbell [`c472698`](https://github.com/RhetTbull/osxphotos/commit/c472698b1d0d8ff9f4d1bde715859bf766f99290)
- Updated docs [skip ci] [`1ddb1de`](https://github.com/RhetTbull/osxphotos/commit/1ddb1de99841e65b690ffc1cbcc5e42e6e25f727)
#### [v0.42.89](https://github.com/RhetTbull/osxphotos/compare/v0.42.88...v0.42.89)
> 26 September 2021
- Updated docs [skip ci] [`bfbc156`](https://github.com/RhetTbull/osxphotos/commit/bfbc156821d2d262b7bd9c4437e23e310da10769)
- Updated docs [skip ci] [`3abaa5a`](https://github.com/RhetTbull/osxphotos/commit/3abaa5ae84ca44cd900f1e3af4532ab405d41a09)
- Fixed AlbumInfo.owner, #239 [`bfd6274`](https://github.com/RhetTbull/osxphotos/commit/bfd627460255c65f870bca6d036401e8792d29d5)
#### [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
- Added error logging to {detected_text} processing, #499 [`b1c0fb3`](https://github.com/RhetTbull/osxphotos/commit/b1c0fb3e8284600394ddbfdd7dfa94916a843c81)
- Updated README.md [skip ci] [`1ee3e03`](https://github.com/RhetTbull/osxphotos/commit/1ee3e035c42d687158f7cf73382f0f263516dc37)
- Removed unneeded test file [skip ci] [`607cf80`](https://github.com/RhetTbull/osxphotos/commit/607cf80dda37ad529edd91fe92af3885b04b9a37)
#### [v0.42.69](https://github.com/RhetTbull/osxphotos/compare/v0.42.67...v0.42.69)
> 28 July 2021
- Added {detected_text} template [`c233523`](https://github.com/RhetTbull/osxphotos/commit/c2335236be7a1eecf4f25a9dcb844df4d6372b5c)
- Added PhotoInfo.detected_text() [`123340e`](https://github.com/RhetTbull/osxphotos/commit/123340eadabb0fb07209c4207ccad13a53de3619)
- Updated dependencies [`0c8fbd6`](https://github.com/RhetTbull/osxphotos/commit/0c8fbd69af7a0d696de5224bf3c302e0c240905f)
#### [v0.42.67](https://github.com/RhetTbull/osxphotos/compare/v0.42.66...v0.42.67)
> 24 July 2021
- Added {album_seq} and {folder_album_seq}, #496 [`12f39db`](https://github.com/RhetTbull/osxphotos/commit/12f39dbaf520ad767e3da667257ce00af60fdd7e)
- Fixed {album_seq} and {folder_album_seq} help text [`077d577`](https://github.com/RhetTbull/osxphotos/commit/077d577c9890c4840a60c3e450dcd4167aa669ea)
#### [v0.42.66](https://github.com/RhetTbull/osxphotos/compare/v0.42.65...v0.42.66)
> 23 July 2021
- Updated docs [`666b6ca`](https://github.com/RhetTbull/osxphotos/commit/666b6cac33fb8a2d0fc602609f11e190e11c538f)
- Added {id} sequence number template, #154 [`e95c096`](https://github.com/RhetTbull/osxphotos/commit/e95c0967846106f6da2adaa0b85520df8b351bb0)
- Updated example [skip ci] [`8216c33`](https://github.com/RhetTbull/osxphotos/commit/8216c33b596dba35007168cda4e8de34d9f4b2ea)
#### [v0.42.65](https://github.com/RhetTbull/osxphotos/compare/v0.42.64...v0.42.65)
> 20 July 2021
- Fixed album sort order for custom sort, #497 [`e27c40c`](https://github.com/RhetTbull/osxphotos/commit/e27c40c7724dc47a7c95d1a417808c2b1f13adb0)
- Updated test data [`a05e7be`](https://github.com/RhetTbull/osxphotos/commit/a05e7be14e080af0cef80831c3ff7fa0a897a1b2)
- Updated example [skip ci] [`6f4cab6`](https://github.com/RhetTbull/osxphotos/commit/6f4cab6721ca3091031d8010e29d959e3afdecb2)
#### [v0.42.64](https://github.com/RhetTbull/osxphotos/compare/v0.42.63...v0.42.64)
> 18 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/*

474
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,10 @@
# 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
# python3 setup.py sdist bdist_wheel
python3 -m build
./make_cli_exe.sh

View File

@@ -1,7 +1,10 @@
build
m2r2
pyinstaller==4.4
pytest-mock
pytest==6.2.4
pytest-mock==3.6.1
Sphinx==4.0.2
sphinx-rtd-theme==0.5.2
wheel==0.36.2
twine==3.4.1
pyinstaller==4.3
sphinx_click
sphinx_rtd_theme
twine
wheel
Sphinx

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: 210ecd9d654dea5d4c21627449ca1d63
config: 58505ca56d322ccc59c38bc440ccc347
tags: 645f666f9bcd5a90fca523b33c5a78b7

View File

@@ -5,10 +5,10 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Overview: module code &#8212; osxphotos 0.42.20 documentation</title>
<link rel="stylesheet" href="../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../" src="../_static/documentation_options.js"></script>
<title>Overview: module code &#8212; osxphotos 0.43.9 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>
<script src="../_static/jquery.js"></script>
<script src="../_static/underscore.js"></script>
<script src="../_static/doctools.js"></script>
@@ -31,7 +31,11 @@
<div class="body" role="main">
<h1>All modules for which code is available</h1>
<ul><li><a href="osxphotos/photoinfo/photoinfo.html">osxphotos.photoinfo.photoinfo</a></li>
<ul><li><a href="osxphotos/photoinfo/_photoinfo_exifinfo.html">osxphotos.photoinfo._photoinfo_exifinfo</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_export.html">osxphotos.photoinfo._photoinfo_export</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_scoreinfo.html">osxphotos.photoinfo._photoinfo_scoreinfo</a></li>
<li><a href="osxphotos/photoinfo/_photoinfo_searchinfo.html">osxphotos.photoinfo._photoinfo_searchinfo</a></li>
<li><a href="osxphotos/photoinfo/photoinfo.html">osxphotos.photoinfo.photoinfo</a></li>
<li><a href="osxphotos/photosdb/photosdb.html">osxphotos.photosdb.photosdb</a></li>
</ul>
@@ -67,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>
@@ -89,7 +93,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,10 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.42.19 documentation</title>
<link rel="stylesheet" href="../../../_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="../../../_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="../../../" src="../../../_static/documentation_options.js"></script>
<title>osxphotos.photosdb.photosdb &#8212; osxphotos 0.43.8 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>
<script src="../../../_static/jquery.js"></script>
<script src="../../../_static/underscore.js"></script>
<script src="../../../_static/doctools.js"></script>
@@ -44,11 +44,15 @@
<span class="kn">import</span> <span class="nn">re</span>
<span class="kn">import</span> <span class="nn">sys</span>
<span class="kn">import</span> <span class="nn">tempfile</span>
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span>
<span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Iterable</span>
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
<span class="kn">from</span> <span class="nn">pprint</span> <span class="kn">import</span> <span class="n">pformat</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span>
<span class="kn">import</span> <span class="nn">bitmath</span>
<span class="kn">import</span> <span class="nn">photoscript</span>
<span class="kn">from</span> <span class="nn">rich</span> <span class="kn">import</span> <span class="nb">print</span>
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">_DB_TABLE_NAMES</span><span class="p">,</span>
@@ -76,6 +80,7 @@
<span class="kn">from</span> <span class="nn">..fileutil</span> <span class="kn">import</span> <span class="n">FileUtil</span>
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">PersonInfo</span>
<span class="kn">from</span> <span class="nn">..photoinfo</span> <span class="kn">import</span> <span class="n">PhotoInfo</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">..queryoptions</span> <span class="kn">import</span> <span class="n">QueryOptions</span>
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">_check_file_exists</span><span class="p">,</span>
@@ -95,29 +100,29 @@
<div class="viewcode-block" id="PhotosDB"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB">[docs]</a><span class="k">class</span> <span class="nc">PhotosDB</span><span class="p">:</span>
<span class="sd">&quot;&quot;&quot; Processes a Photos.app library database to extract information about photos &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;Processes a Photos.app library database to extract information about photos&quot;&quot;&quot;</span>
<span class="c1"># import additional methods</span>
<span class="kn">from</span> <span class="nn">._photosdb_process_comments</span> <span class="kn">import</span> <span class="n">_process_comments</span>
<span class="kn">from</span> <span class="nn">._photosdb_process_exif</span> <span class="kn">import</span> <span class="n">_process_exifinfo</span>
<span class="kn">from</span> <span class="nn">._photosdb_process_faceinfo</span> <span class="kn">import</span> <span class="n">_process_faceinfo</span>
<span class="kn">from</span> <span class="nn">._photosdb_process_scoreinfo</span> <span class="kn">import</span> <span class="n">_process_scoreinfo</span>
<span class="kn">from</span> <span class="nn">._photosdb_process_searchinfo</span> <span class="kn">import</span> <span class="p">(</span>
<span class="n">_process_searchinfo</span><span class="p">,</span>
<span class="n">labels</span><span class="p">,</span>
<span class="n">labels_normalized</span><span class="p">,</span>
<span class="n">labels_as_dict</span><span class="p">,</span>
<span class="n">labels_normalized</span><span class="p">,</span>
<span class="n">labels_normalized_as_dict</span><span class="p">,</span>
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">._photosdb_process_scoreinfo</span> <span class="kn">import</span> <span class="n">_process_scoreinfo</span>
<span class="kn">from</span> <span class="nn">._photosdb_process_comments</span> <span class="kn">import</span> <span class="n">_process_comments</span>
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">dbfile</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">verbose</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Create a new PhotosDB object.</span>
<span class="sd">&quot;&quot;&quot;Create a new PhotosDB object.</span>
<span class="sd"> Args:</span>
<span class="sd"> dbfile: specify full path to photos library or photos.db; if None, will attempt to locate last library opened by Photos.</span>
<span class="sd"> verbose: optional callable function to use for printing verbose text during processing; if None (default), does not print output.</span>
<span class="sd"> exiftool: optional path to exiftool for methods that require this (e.g. PhotoInfo.exiftool); if not provided, will search PATH</span>
<span class="sd"> </span>
<span class="sd"> Raises:</span>
<span class="sd"> FileNotFoundError if dbfile is not a valid Photos library.</span>
<span class="sd"> TypeError if verbose is not None and not callable.</span>
@@ -273,6 +278,17 @@
<span class="c1"># Will hold the primary key of root folder</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_folder_root_pk</span> <span class="o">=</span> <span class="kc">None</span>
<span class="c1"># Dict to hold signatures for finding possible duplicates</span>
<span class="c1"># key is tuple of (original_filesize, date) and value is list of uuids that match that signature</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_signatures</span> <span class="o">=</span> <span class="p">{}</span>
<span class="c1"># Dict to hold information on volume names (Photos 5+)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_filesystem_volumes</span> <span class="o">=</span> <span class="p">{}</span>
<span class="c1"># Dict to hold information on moments (Photos 5+)</span>
<span class="c1"># key is Z_PK of ZMOMENT table and values are the moment info</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">&quot;</span><span class="p">)</span>
@@ -353,9 +369,11 @@
<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>
<span class="sd">&quot;&quot;&quot;return keywords as dict of keyword, count in reverse sorted order (descending)&quot;&quot;&quot;</span>
<span class="n">keywords</span> <span class="o">=</span> <span class="p">{</span>
<span class="n">k</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="p">[</span><span class="n">k</span><span class="p">])</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="o">.</span><span class="n">keys</span><span class="p">()</span>
<span class="p">}</span>
@@ -365,7 +383,7 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">persons_as_dict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return persons as dict of person, count in reverse sorted order (descending) &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return persons as dict of person, count in reverse sorted order (descending)&quot;&quot;&quot;</span>
<span class="n">persons</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">pk</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_pk</span><span class="p">:</span>
<span class="n">fullname</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">][</span><span class="s2">&quot;fullname&quot;</span><span class="p">]</span>
@@ -378,7 +396,7 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">albums_as_dict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return albums as dict of albums, count in reverse sorted order (descending) &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return albums as dict of albums, count in reverse sorted order (descending)&quot;&quot;&quot;</span>
<span class="n">albums</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">album_keys</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_album_uuids</span><span class="p">(</span><span class="n">shared</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>
<span class="k">for</span> <span class="n">album</span> <span class="ow">in</span> <span class="n">album_keys</span><span class="p">:</span>
@@ -395,8 +413,8 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">albums_shared_as_dict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns shared albums as dict of albums, count in reverse sorted order (descending)</span>
<span class="sd"> valid only on Photos 5; on Photos &lt;= 4, prints warning and returns empty dict &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;returns shared albums as dict of albums, count in reverse sorted order (descending)</span>
<span class="sd"> valid only on Photos 5; on Photos &lt;= 4, prints warning and returns empty dict&quot;&quot;&quot;</span>
<span class="n">albums</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">album_keys</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_album_uuids</span><span class="p">(</span><span class="n">shared</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
@@ -414,19 +432,19 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">keywords</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of keywords found in photos database &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list of keywords found in photos database&quot;&quot;&quot;</span>
<span class="n">keywords</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbkeywords_keyword</span><span class="o">.</span><span class="n">keys</span><span class="p">()</span>
<span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">keywords</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">persons</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of persons found in photos database &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list of persons found in photos database&quot;&quot;&quot;</span>
<span class="n">persons</span> <span class="o">=</span> <span class="p">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">k</span><span class="p">][</span><span class="s2">&quot;fullname&quot;</span><span class="p">]</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbfaces_pk</span><span class="p">}</span>
<span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="n">persons</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">person_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of PersonInfo objects for each person in the photos database &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list of PersonInfo objects for each person in the photos database&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">_person_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
@@ -437,7 +455,7 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">folder_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list FolderInfo objects representing top-level folders in the photos database &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list FolderInfo objects representing top-level folders in the photos database&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</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="n">folders</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">FolderInfo</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="o">=</span><span class="n">folder</span><span class="p">)</span>
@@ -458,7 +476,7 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">folders</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of top-level folder names in the photos database &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list of top-level folder names in the photos database&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</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="n">folder_names</span> <span class="o">=</span> <span class="p">[</span>
<span class="n">folder</span><span class="p">[</span><span class="s2">&quot;name&quot;</span><span class="p">]</span>
@@ -479,7 +497,7 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">album_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of AlbumInfo objects for each album in the photos database &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list of AlbumInfo objects for each album in the photos database&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">_album_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
@@ -491,8 +509,8 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">album_info_shared</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of AlbumInfo objects for each shared album in the photos database</span>
<span class="sd"> only valid for Photos 5; on Photos &lt;= 4, prints warning and returns empty list &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list of AlbumInfo objects for each shared album in the photos database</span>
<span class="sd"> only valid for Photos 5; on Photos &lt;= 4, prints warning and returns empty list&quot;&quot;&quot;</span>
<span class="c1"># if _dbalbum_details[key][&quot;cloudownerhashedpersonid&quot;] is not None, then it&#39;s a shared album</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">_album_info_shared</span>
@@ -505,7 +523,7 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">albums</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of albums found in photos database &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list of albums found in photos database&quot;&quot;&quot;</span>
<span class="c1"># Could be more than one album with same name</span>
<span class="c1"># Right now, they are treated as same album and photos are combined from albums with same name</span>
@@ -518,8 +536,8 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">albums_shared</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of shared albums found in photos database</span>
<span class="sd"> only valid for Photos 5; on Photos &lt;= 4, prints warning and returns empty list &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list of shared albums found in photos database</span>
<span class="sd"> only valid for Photos 5; on Photos &lt;= 4, prints warning and returns empty list&quot;&quot;&quot;</span>
<span class="c1"># Could be more than one album with same name</span>
<span class="c1"># Right now, they are treated as same album and photos are combined from albums with same name</span>
@@ -534,7 +552,7 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">import_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return list of ImportInfo objects for each import session in the database &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return list of ImportInfo objects for each import session in the database&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">_import_info</span>
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
@@ -546,21 +564,21 @@
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">db_version</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return the database version as stored in LiGlobals table &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return the database version as stored in LiGlobals table&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">db_path</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns path to the Photos library database PhotosDB was initialized with &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;returns path to the Photos library database PhotosDB was initialized with&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">abspath</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span><span class="p">)</span>
<span class="nd">@property</span>
<span class="k">def</span> <span class="nf">library_path</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; returns path to the Photos library PhotosDB was initialized with &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;returns path to the Photos library PhotosDB was initialized with&quot;&quot;&quot;</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_library_path</span>
<div class="viewcode-block" id="PhotosDB.get_db_connection"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.get_db_connection">[docs]</a> <span class="k">def</span> <span class="nf">get_db_connection</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Get connection to the working copy of the Photos database </span>
<span class="sd">&quot;&quot;&quot;Get connection to the working copy of the Photos database</span>
<span class="sd"> Returns:</span>
<span class="sd"> tuple of (connection, cursor) to sqlite3 database</span>
@@ -568,7 +586,7 @@
<span class="k">return</span> <span class="n">_open_sql_file</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span><span class="p">)</span></div>
<span class="k">def</span> <span class="nf">_copy_db_file</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">fname</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; copies the sqlite database file to a temp file &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;copies the sqlite database file to a temp file&quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot; returns the name of the temp file &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot; If sqlite shared memory and write-ahead log files exist, those are copied too &quot;&quot;&quot;</span>
<span class="c1"># required because python&#39;s sqlite3 implementation can&#39;t read a locked file</span>
@@ -620,13 +638,15 @@
<span class="c1"># return dest_path</span>
<span class="k">def</span> <span class="nf">_process_database4</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; process the Photos database to extract info</span>
<span class="sd"> works on Photos version &lt;= 4.0 &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;process the Photos database to extract info</span>
<span class="sd"> works on Photos version &lt;= 4.0&quot;&quot;&quot;</span>
<span class="n">verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing database.&quot;</span><span class="p">)</span>
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Database version: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="si">}</span><span class="s2">.&quot;</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="mi">4</span> <span class="c1"># only used in Photos 5+</span>
<span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">c</span><span class="p">)</span> <span class="o">=</span> <span class="n">_open_sql_file</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span><span class="p">)</span>
<span class="c1"># get info to associate persons with photos</span>
@@ -811,6 +831,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="p">}</span>
<span class="c1"># get details about folders</span>
@@ -1123,6 +1145,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;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>
@@ -1213,6 +1238,16 @@
<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>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_signatures</span><span class="p">[</span><span class="n">signature</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">uuid</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">_db_signatures</span><span class="p">[</span><span class="n">signature</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">uuid</span><span class="p">]</span>
<span class="c1"># get additional details from RKMaster, needed for RAW processing</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing additional photo details.&quot;</span><span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
@@ -1565,15 +1600,15 @@
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_build_album_folder_hierarchy_4</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="n">folders</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; recursively build folder/album hierarchy</span>
<span class="sd"> uuid: parent uuid of the album being processed </span>
<span class="sd"> (parent uuid is a folder in RKFolders)</span>
<span class="sd"> folders: dict holding the folder hierarchy </span>
<span class="sd"> NOTE: This implementation is different than _build_album_folder_hierarchy_5 </span>
<span class="sd"> which takes the uuid of the album being processed. Here uuid is the parent uuid</span>
<span class="sd"> of the parent folder album because in Photos &lt;=4, folders are in RKFolders and </span>
<span class="sd"> albums in RKAlbums. In Photos 5, folders are just special albums </span>
<span class="sd"> with kind = _PHOTOS_5_FOLDER_KIND &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;recursively build folder/album hierarchy</span>
<span class="sd"> uuid: parent uuid of the album being processed</span>
<span class="sd"> (parent uuid is a folder in RKFolders)</span>
<span class="sd"> folders: dict holding the folder hierarchy</span>
<span class="sd"> NOTE: This implementation is different than _build_album_folder_hierarchy_5</span>
<span class="sd"> which takes the uuid of the album being processed. Here uuid is the parent uuid</span>
<span class="sd"> of the parent folder album because in Photos &lt;=4, folders are in RKFolders and</span>
<span class="sd"> albums in RKAlbums. In Photos 5, folders are just special albums</span>
<span class="sd"> with kind = _PHOTOS_5_FOLDER_KIND&quot;&quot;&quot;</span>
<span class="n">parent_uuid</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbfolder_details</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;parentFolderUuid&quot;</span><span class="p">]</span>
@@ -1596,11 +1631,11 @@
<span class="k">return</span> <span class="n">folders</span>
<span class="k">def</span> <span class="nf">_process_database5</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; process the Photos database to extract info </span>
<span class="sd"> works on Photos version 5 and version 6</span>
<span class="sd">&quot;&quot;&quot;process the Photos database to extract info</span>
<span class="sd"> works on Photos version 5 and version 6</span>
<span class="sd"> This is a big hairy 700 line function that should probably be refactored</span>
<span class="sd"> but it works so don&#39;t touch it.</span>
<span class="sd"> This is a big hairy 700 line function that should probably be refactored</span>
<span class="sd"> but it works so don&#39;t touch it.</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
@@ -1615,10 +1650,14 @@
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Database version: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="si">}</span><span class="s2">, </span><span class="si">{</span><span class="n">photos_ver</span><span class="si">}</span><span class="s2">.&quot;</span><span class="p">)</span>
<span class="n">asset_table</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;ASSET&quot;</span><span class="p">]</span>
<span class="n">keyword_join</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;KEYWORD_JOIN&quot;</span><span class="p">]</span>
<span class="n">asset_album_table</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;ASSET_ALBUM_TABLE&quot;</span><span class="p">]</span>
<span class="n">album_join</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;ALBUM_JOIN&quot;</span><span class="p">]</span>
<span class="n">album_sort</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;ALBUM_SORT_ORDER&quot;</span><span class="p">]</span>
<span class="n">asset_album_join</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;ASSET_ALBUM_JOIN&quot;</span><span class="p">]</span>
<span class="n">import_fok</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;IMPORT_FOK&quot;</span><span class="p">]</span>
<span class="n">depth_state</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;DEPTH_STATE&quot;</span><span class="p">]</span>
<span class="n">uti_original_column</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;UTI_ORIGINAL&quot;</span><span class="p">]</span>
<span class="n">hdr_type_column</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">&quot;HDR_TYPE&quot;</span><span class="p">]</span>
<span class="c1"># Look for all combinations of persons and pictures</span>
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
@@ -1648,7 +1687,11 @@
<span class="k">for</span> <span class="n">person</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
<span class="n">pk</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">fullname</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="k">if</span> <span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">&quot;&quot;</span> <span class="k">else</span> <span class="n">_UNKNOWN_PERSON</span>
<span class="n">fullname</span> <span class="o">=</span> <span class="p">(</span>
<span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
<span class="k">if</span> <span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">&quot;&quot;</span> <span class="ow">and</span> <span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">)</span>
<span class="k">else</span> <span class="n">_UNKNOWN_PERSON</span>
<span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">&quot;pk&quot;</span><span class="p">:</span> <span class="n">pk</span><span class="p">,</span>
<span class="s2">&quot;uuid&quot;</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
@@ -1734,8 +1777,8 @@
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">album_sort</span><span class="si">}</span><span class="s2"></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 Z_26ASSETS ON </span><span class="si">{</span><span class="n">album_join</span><span class="si">}</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"> JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS</span>
<span class="s2"> JOIN </span><span class="si">{</span><span class="n">asset_album_table</span><span class="si">}</span><span class="s2"> ON </span><span class="si">{</span><span class="n">album_join</span><span class="si">}</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"> JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = </span><span class="si">{</span><span class="n">asset_album_join</span><span class="si">}</span><span class="s2"></span>
<span class="s2"> &quot;&quot;&quot;</span>
<span class="p">)</span>
@@ -1773,7 +1816,9 @@
<span class="s2">&quot;ZTRASHEDSTATE, &quot;</span> <span class="c1"># 9</span>
<span class="s2">&quot;ZCREATIONDATE, &quot;</span> <span class="c1"># 10</span>
<span class="s2">&quot;ZSTARTDATE, &quot;</span> <span class="c1"># 11</span>
<span class="s2">&quot;ZENDDATE &quot;</span> <span class="c1"># 12</span>
<span class="s2">&quot;ZENDDATE, &quot;</span> <span class="c1"># 12</span>
<span class="s2">&quot;ZCUSTOMSORTASCENDING, &quot;</span> <span class="c1"># 13</span>
<span class="s2">&quot;ZCUSTOMSORTKEY &quot;</span> <span class="c1"># 14</span>
<span class="s2">&quot;FROM ZGENERICALBUM &quot;</span>
<span class="p">)</span>
<span class="k">for</span> <span class="n">album</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
@@ -1793,6 +1838,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">10</span><span class="p">],</span>
<span class="s2">&quot;start_date&quot;</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">11</span><span class="p">],</span>
<span class="s2">&quot;end_date&quot;</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">12</span><span class="p">],</span>
<span class="s2">&quot;customsortascending&quot;</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">13</span><span class="p">],</span>
<span class="s2">&quot;customsortkey&quot;</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">14</span><span class="p">],</span>
<span class="p">}</span>
<span class="c1"># add cross-reference by pk to uuid</span>
@@ -1902,7 +1949,7 @@
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZAVALANCHEUUID,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZAVALANCHEPICKTYPE,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZKINDSUBTYPE,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZCUSTOMRENDEREDVALUE,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.</span><span class="si">{</span><span class="n">hdr_type_column</span><span class="si">}</span><span class="s2">,</span>
<span class="s2"> ZADDITIONALASSETATTRIBUTES.ZCAMERACAPTUREDEVICE,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZCLOUDASSETGUID,</span>
<span class="s2"> ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,</span>
@@ -1921,7 +1968,9 @@
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZVISIBILITYSTATE,</span>
<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">.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">.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>
@@ -1970,6 +2019,8 @@
<span class="c1"># 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash</span>
<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>
@@ -2154,6 +2205,9 @@
<span class="k">except</span> <span class="p">(</span><span class="ne">ValueError</span><span class="p">,</span> <span class="ne">TypeError</span><span class="p">):</span>
<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>
<span class="n">info</span><span class="p">[</span><span class="s2">&quot;import_session&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
@@ -2174,6 +2228,13 @@
<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="o">=</span> <span class="n">info</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>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_signatures</span><span class="p">[</span><span class="n">signature</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">uuid</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">_db_signatures</span><span class="p">[</span><span class="n">signature</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="n">uuid</span><span class="p">]</span>
<span class="c1"># # if row[19] is not None and ((row[20] == 2) or (row[20] == 4)):</span>
<span class="c1"># # burst photo</span>
<span class="c1"># if row[19] is not None:</span>
@@ -2259,20 +2320,33 @@
<span class="c1"># Get info on remote/local availability for photos in shared albums</span>
<span class="c1"># Also get UTI of original image (zdatastoresubtype = 1)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;&quot;&quot; SELECT </span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">&gt;=</span> <span class="mi">7</span><span class="p">:</span>
<span class="n">sql_missing</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;&quot;&quot; SELECT </span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID, </span>
<span class="s2"> ZINTERNALRESOURCE.ZLOCALAVAILABILITY, </span>
<span class="s2"> ZINTERNALRESOURCE.ZREMOTEAVAILABILITY,</span>
<span class="s2"> ZINTERNALRESOURCE.ZDATASTORESUBTYPE,</span>
<span class="s2"> ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">uti_original_column</span><span class="si">}</span><span class="s2">,</span>
<span class="s2"> null </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"> JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET </span>
<span class="s2"> WHERE ZDATASTORESUBTYPE = 1 OR ZDATASTORESUBTYPE = 3 &quot;&quot;&quot;</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">sql_missing</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;&quot;&quot; SELECT </span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID, </span>
<span class="s2"> ZINTERNALRESOURCE.ZLOCALAVAILABILITY, </span>
<span class="s2"> ZINTERNALRESOURCE.ZREMOTEAVAILABILITY,</span>
<span class="s2"> ZINTERNALRESOURCE.ZDATASTORESUBTYPE,</span>
<span class="s2"> </span><span class="si">{</span><span class="n">uti_original_column</span><span class="si">}</span><span class="s2">,</span>
<span class="s2"> ZUNIFORMTYPEIDENTIFIER.ZIDENTIFIER</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"> JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET </span>
<span class="s2"> JOIN ZUNIFORMTYPEIDENTIFIER ON ZUNIFORMTYPEIDENTIFIER.Z_PK = ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER </span>
<span class="s2"> WHERE ZDATASTORESUBTYPE = 1 OR ZDATASTORESUBTYPE = 3 &quot;&quot;&quot;</span>
<span class="p">)</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql_missing</span><span class="p">)</span>
<span class="c1"># Order of results:</span>
<span class="c1"># 0 {asset_table}.ZUUID,</span>
@@ -2332,20 +2406,36 @@
<span class="c1"># get information about associted RAW images</span>
<span class="c1"># RAW images have ZDATASTORESUBTYPE = 17</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;&quot;&quot; SELECT</span>
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">&gt;=</span> <span class="mi">7</span><span class="p">:</span>
<span class="n">sql_raw</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;&quot;&quot; SELECT</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID,</span>
<span class="s2"> ZINTERNALRESOURCE.ZDATALENGTH, </span>
<span class="s2"> null,</span>
<span class="s2"> ZINTERNALRESOURCE.ZDATASTORESUBTYPE,</span>
<span class="s2"> ZINTERNALRESOURCE.ZRESOURCETYPE,</span>
<span class="s2"> ZINTERNALRESOURCE.ZFILESYSTEMBOOKMARK</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 ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET</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"> WHERE ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 17</span>
<span class="s2"> &quot;&quot;&quot;</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">sql_raw</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&quot;&quot;&quot; SELECT</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID,</span>
<span class="s2"> ZINTERNALRESOURCE.ZDATALENGTH, </span>
<span class="s2"> ZUNIFORMTYPEIDENTIFIER.ZIDENTIFIER,</span>
<span class="s2"> ZINTERNALRESOURCE.ZDATASTORESUBTYPE,</span>
<span class="s2"> ZINTERNALRESOURCE.ZRESOURCETYPE</span>
<span class="s2"> ZINTERNALRESOURCE.ZRESOURCETYPE,</span>
<span class="s2"> ZINTERNALRESOURCE.ZFILESYSTEMBOOKMARK</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 ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET</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"> JOIN ZUNIFORMTYPEIDENTIFIER ON ZUNIFORMTYPEIDENTIFIER.Z_PK = ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER</span>
<span class="s2"> WHERE ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 17</span>
<span class="s2"> &quot;&quot;&quot;</span>
<span class="p">)</span>
<span class="s2"> &quot;&quot;&quot;</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql_raw</span><span class="p">)</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>
<span class="k">if</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
@@ -2354,6 +2444,33 @@
<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;UTI_raw&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</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;datastore_subtype&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">3</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;resource_type&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">4</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;raw_bookmark&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span>
<span class="c1"># get paths for the relative imports for RAW+JPEG images</span>
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;&quot;&quot; SELECT</span>
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID,</span>
<span class="s2"> ZFILESYSTEMVOLUME.ZNAME,</span>
<span class="s2"> ZFILESYSTEMBOOKMARK.ZPATHRELATIVETOVOLUME</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 ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET</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"> JOIN ZFILESYSTEMBOOKMARK ON ZFILESYSTEMBOOKMARK.ZRESOURCE = ZINTERNALRESOURCE.Z_PK</span>
<span class="s2"> JOIN ZFILESYSTEMVOLUME ON ZFILESYSTEMVOLUME.Z_PK = ZINTERNALRESOURCE.ZFILESYSTEMVOLUME</span>
<span class="s2"> WHERE ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 17</span>
<span class="s2"> &quot;&quot;&quot;</span>
<span class="p">)</span>
<span class="c1"># path to the raw image will be /Volumes/ZFILESYSTEMVOLUME.ZNAME/ZFILESYSTEMBOOKMARK.ZPATHRELATIVETOVOLUME</span>
<span class="c1"># 0: {asset_table}.ZUUID, -- UUID</span>
<span class="c1"># 1: ZFILESYSTEMVOLUME.ZNAME, -- name of the volume</span>
<span class="c1"># 2: ZFILESYSTEMBOOKMARK.ZPATHRELATIVETOVOLUME -- path to the raw image</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>
<span class="k">if</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</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;raw_volume&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</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;raw_relative_path&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
<span class="c1"># add faces and keywords to photo data</span>
<span class="k">for</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
@@ -2413,6 +2530,10 @@
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing comments and likes for shared photos.&quot;</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_process_comments</span><span class="p">()</span>
<span class="c1"># process moments</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Processing moments.&quot;</span><span class="p">)</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_process_moments</span><span class="p">()</span>
<span class="c1"># done processing, dump debug data if requested</span>
<span class="n">verbose</span><span class="p">(</span><span class="s2">&quot;Done processing details from Photos library.&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
@@ -2458,10 +2579,113 @@
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&quot;Burst Photos (dbphotos_burst:&quot;</span><span class="p">)</span>
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">pformat</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos_burst</span><span class="p">))</span>
<span class="k">def</span> <span class="nf">_process_moments</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Process data from ZMOMENT table&quot;&quot;&quot;</span>
<span class="c1"># _db_moment_pk is dict in form {pk: {moment info}} by ZMOMENT.Z_PK</span>
<span class="k">if</span> <span class="bp">self</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">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;Moment info implemented for this database version&quot;</span>
<span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_process_moment_5</span><span class="p">()</span>
<span class="k">def</span> <span class="nf">_process_moment_5</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot;Process moment info for Photos 5 databases&quot;&quot;&quot;</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span> <span class="o">=</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">execute</span><span class="p">(</span>
<span class="sa">f</span><span class="s2">&quot;&quot;&quot;</span>
<span class="s2"> SELECT </span>
<span class="s2"> Z_PK,</span>
<span class="s2"> ZTIMEZONEOFFSET,</span>
<span class="s2"> ZTRASHEDSTATE,</span>
<span class="s2"> ZAPPROXIMATELATITUDE,</span>
<span class="s2"> ZAPPROXIMATELONGITUDE,</span>
<span class="s2"> ZENDDATE,</span>
<span class="s2"> ZMODIFICATIONDATE,</span>
<span class="s2"> ZREPRESENTATIVEDATE,</span>
<span class="s2"> ZSTARTDATE,</span>
<span class="s2"> ZSUBTITLE,</span>
<span class="s2"> ZTITLE,</span>
<span class="s2"> ZUUID</span>
<span class="s2"> FROM ZMOMENT&quot;&quot;&quot;</span>
<span class="p">)</span>
<span class="c1"># results</span>
<span class="c1"># 0 Z_PK,</span>
<span class="c1"># 1 ZTIMEZONEOFFSET,</span>
<span class="c1"># 2 ZTRASHEDSTATE,</span>
<span class="c1"># 3 ZAPPROXIMATELATITUDE,</span>
<span class="c1"># 4 ZAPPROXIMATELONGITUDE,</span>
<span class="c1"># 5 ZENDDATE,</span>
<span class="c1"># 6 ZMODIFICATIONDATE,</span>
<span class="c1"># 7 ZREPRESENTATIVEDATE,</span>
<span class="c1"># 8 ZSTARTDATE,</span>
<span class="c1"># 9 ZSUBTITLE,</span>
<span class="c1"># 10 ZTITLE,</span>
<span class="c1"># 11 ZUUID</span>
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
<span class="n">moment_info</span> <span class="o">=</span> <span class="p">{}</span>
<span class="n">moment_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">0</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;timezoneOffset&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;trashedState&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;approximateLatitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;approximateLongitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;endDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;modificationDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;representativeDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">7</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;startDate&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">8</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;subtitle&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">9</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;uuid&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">11</span><span class="p">]</span>
<span class="c1"># if both lat/lon == -180, then it means location undefined</span>
<span class="k">if</span> <span class="p">(</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;approximateLatitude&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="o">-</span><span class="mf">180.0</span>
<span class="ow">and</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;approximateLongitude&quot;</span><span class="p">]</span> <span class="o">==</span> <span class="o">-</span><span class="mf">180.0</span>
<span class="p">):</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;latitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;longitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;latitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;approximateLatitude&quot;</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;longitude&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;approximateLongitude&quot;</span><span class="p">]</span>
<span class="c1"># process date stamps</span>
<span class="n">offset_seconds</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;timezoneOffset&quot;</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span>
<span class="n">delta</span> <span class="o">=</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">seconds</span><span class="o">=</span><span class="n">offset_seconds</span><span class="p">)</span>
<span class="n">tz</span> <span class="o">=</span> <span class="n">timezone</span><span class="p">(</span><span class="n">delta</span><span class="p">)</span>
<span class="k">for</span> <span class="n">date_name</span> <span class="ow">in</span> <span class="p">[</span>
<span class="s2">&quot;startDate&quot;</span><span class="p">,</span>
<span class="s2">&quot;endDate&quot;</span><span class="p">,</span>
<span class="s2">&quot;modificationDate&quot;</span><span class="p">,</span>
<span class="s2">&quot;representativeDate&quot;</span><span class="p">,</span>
<span class="p">]:</span>
<span class="n">date_stamp</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span><span class="p">]</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">moment_date</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">fromtimestamp</span><span class="p">(</span><span class="n">date_stamp</span> <span class="o">+</span> <span class="n">TIME_DELTA</span><span class="p">)</span>
<span class="c1"># save raw time stamp valu</span>
<span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span> <span class="o">+</span> <span class="s2">&quot;_timestamp&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span><span class="p">]</span>
<span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_date</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">tz</span><span class="p">)</span>
<span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span>
<span class="c1"># sometimes imageDate is invalid so use 1 Jan 1970 in UTC as image date</span>
<span class="n">moment_date</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">tz</span> <span class="o">=</span> <span class="n">timezone</span><span class="p">(</span><span class="n">timedelta</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
<span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span> <span class="o">+</span> <span class="s2">&quot;_timestamp&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">date_stamp</span>
<span class="n">moment_info</span><span class="p">[</span><span class="n">date_name</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_date</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">tz</span><span class="p">)</span>
<span class="c1"># process title/subtitle</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;title&quot;</span><span class="p">]</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span>
<span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;subtitle&quot;</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;subtitle&quot;</span><span class="p">]</span> <span class="ow">or</span> <span class="s2">&quot;&quot;</span>
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span><span class="p">[</span><span class="n">moment_info</span><span class="p">[</span><span class="s2">&quot;pk&quot;</span><span class="p">]]</span> <span class="o">=</span> <span class="n">moment_info</span>
<span class="k">def</span> <span class="nf">_build_album_folder_hierarchy_5</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="n">folders</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; recursively build folder/album hierarchy</span>
<span class="sd"> uuid: uuid of the album/folder being processed</span>
<span class="sd"> folders: dict holding the folder hierarchy &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;recursively build folder/album hierarchy</span>
<span class="sd"> uuid: uuid of the album/folder being processed</span>
<span class="sd"> folders: dict holding the folder hierarchy&quot;&quot;&quot;</span>
<span class="c1"># get parent uuid</span>
<span class="n">parent</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_details</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">&quot;parentfolder&quot;</span><span class="p">]</span>
@@ -2482,17 +2706,17 @@
<span class="k">return</span> <span class="n">folders</span>
<span class="k">def</span> <span class="nf">_album_folder_hierarchy_list</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">album_uuid</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return appropriate album_folder_hierarchy_list for the _db_version &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return appropriate album_folder_hierarchy_list for the _db_version&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="bp">self</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="bp">self</span><span class="o">.</span><span class="n">_album_folder_hierarchy_list_4</span><span class="p">(</span><span class="n">album_uuid</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_folder_hierarchy_list_5</span><span class="p">(</span><span class="n">album_uuid</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_album_folder_hierarchy_list_4</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">album_uuid</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return hierarchical list of folder names album_uuid is contained in</span>
<span class="sd"> the folder list is in form:</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;]</span>
<span class="sd"> returns empty list of album is not in any folders &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return hierarchical list of folder names album_uuid is contained in</span>
<span class="sd"> the folder list is in form:</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;]</span>
<span class="sd"> returns empty list of album is not in any folders&quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">folders</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">[</span><span class="n">album_uuid</span><span class="p">]</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
@@ -2500,7 +2724,7 @@
<span class="k">return</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">_recurse_folder_hierarchy</span><span class="p">(</span><span class="n">folders</span><span class="p">,</span> <span class="n">hierarchy</span><span class="o">=</span><span class="p">[]):</span>
<span class="sd">&quot;&quot;&quot; recursively walk the folders dict to build list of folder hierarchy &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;recursively walk the folders dict to build list of folder hierarchy&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">folders</span><span class="p">:</span>
<span class="c1"># empty folder dict (album has no folder hierarchy)</span>
<span class="k">return</span> <span class="p">[]</span>
@@ -2526,10 +2750,10 @@
<span class="k">return</span> <span class="n">hierarchy</span>
<span class="k">def</span> <span class="nf">_album_folder_hierarchy_list_5</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">album_uuid</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return hierarchical list of folder names album_uuid is contained in</span>
<span class="sd"> the folder list is in form:</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;]</span>
<span class="sd"> returns empty list of album is not in any folders &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return hierarchical list of folder names album_uuid is contained in</span>
<span class="sd"> the folder list is in form:</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;]</span>
<span class="sd"> returns empty list of album is not in any folders&quot;&quot;&quot;</span>
<span class="k">try</span><span class="p">:</span>
<span class="n">folders</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">[</span><span class="n">album_uuid</span><span class="p">]</span>
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
@@ -2537,7 +2761,7 @@
<span class="k">return</span> <span class="p">[]</span>
<span class="k">def</span> <span class="nf">_recurse_folder_hierarchy</span><span class="p">(</span><span class="n">folders</span><span class="p">,</span> <span class="n">hierarchy</span><span class="o">=</span><span class="p">[]):</span>
<span class="sd">&quot;&quot;&quot; recursively walk the folders dict to build list of folder hierarchy &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;recursively walk the folders dict to build list of folder hierarchy&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">folders</span><span class="p">:</span>
<span class="c1"># empty folder dict (album has no folder hierarchy)</span>
@@ -2569,15 +2793,15 @@
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_album_folder_hierarchy_folderinfo_5</span><span class="p">(</span><span class="n">album_uuid</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">_album_folder_hierarchy_folderinfo_4</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">album_uuid</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return hierarchical list of FolderInfo objects album_uuid is contained in</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;]</span>
<span class="sd"> returns empty list of album is not in any folders &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return hierarchical list of FolderInfo objects album_uuid is contained in</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;]</span>
<span class="sd"> returns empty list of album is not in any folders&quot;&quot;&quot;</span>
<span class="c1"># title = photosdb._dbalbum_details[album_uuid][&quot;title&quot;]</span>
<span class="n">folders</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">[</span><span class="n">album_uuid</span><span class="p">]</span>
<span class="c1"># logging.warning(f&quot;uuid = {album_uuid}, folder = {folders}&quot;)</span>
<span class="k">def</span> <span class="nf">_recurse_folder_hierarchy</span><span class="p">(</span><span class="n">folders</span><span class="p">,</span> <span class="n">hierarchy</span><span class="o">=</span><span class="p">[]):</span>
<span class="sd">&quot;&quot;&quot; recursively walk the folders dict to build list of folder hierarchy &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;recursively walk the folders dict to build list of folder hierarchy&quot;&quot;&quot;</span>
<span class="c1"># logging.warning(f&quot;folders={folders},hierarchy = {hierarchy}&quot;)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">folders</span><span class="p">:</span>
<span class="c1"># empty folder dict (album has no folder hierarchy)</span>
@@ -2603,14 +2827,14 @@
<span class="k">return</span> <span class="n">hierarchy</span>
<span class="k">def</span> <span class="nf">_album_folder_hierarchy_folderinfo_5</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">album_uuid</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; return hierarchical list of FolderInfo objects album_uuid is contained in</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;]</span>
<span class="sd"> returns empty list of album is not in any folders &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;return hierarchical list of FolderInfo objects album_uuid is contained in</span>
<span class="sd"> [&quot;Top level folder&quot;, &quot;sub folder 1&quot;, &quot;sub folder 2&quot;]</span>
<span class="sd"> returns empty list of album is not in any folders&quot;&quot;&quot;</span>
<span class="c1"># title = photosdb._dbalbum_details[album_uuid][&quot;title&quot;]</span>
<span class="n">folders</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">[</span><span class="n">album_uuid</span><span class="p">]</span>
<span class="k">def</span> <span class="nf">_recurse_folder_hierarchy</span><span class="p">(</span><span class="n">folders</span><span class="p">,</span> <span class="n">hierarchy</span><span class="o">=</span><span class="p">[]):</span>
<span class="sd">&quot;&quot;&quot; recursively walk the folders dict to build list of folder hierarchy &quot;&quot;&quot;</span>
<span class="sd">&quot;&quot;&quot;recursively walk the folders dict to build list of folder hierarchy&quot;&quot;&quot;</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">folders</span><span class="p">:</span>
<span class="c1"># empty folder dict (album has no folder hierarchy)</span>
@@ -2635,19 +2859,19 @@
<span class="k">return</span> <span class="n">hierarchy</span>
<span class="k">def</span> <span class="nf">_get_album_uuids</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">shared</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">import_session</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Return list of album UUIDs found in photos database</span>
<span class="sd"> </span>
<span class="sd">&quot;&quot;&quot;Return list of album UUIDs found in photos database</span>
<span class="sd"> Filters out albums in the trash and any special album types</span>
<span class="sd"> </span>
<span class="sd"> Args:</span>
<span class="sd"> shared: boolean; if True, returns shared albums, else normal albums</span>
<span class="sd"> import_session: boolean, if True, returns import session albums, else normal or shared albums</span>
<span class="sd"> Note: flags (shared, import_session) are mutually exclusive</span>
<span class="sd"> </span>
<span class="sd"> Raises:</span>
<span class="sd"> ValueError: raised if mutually exclusive flags passed</span>
<span class="sd"> Returns: list of album UUIDs </span>
<span class="sd"> Returns: list of album UUIDs</span>
<span class="sd"> &quot;&quot;&quot;</span>
<span class="k">if</span> <span class="n">shared</span> <span class="ow">and</span> <span class="n">import_session</span><span class="p">:</span>
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span>
@@ -2699,14 +2923,14 @@
<span class="k">return</span> <span class="n">album_list</span>
<span class="k">def</span> <span class="nf">_get_albums</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">shared</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Return list of album titles found in photos database</span>
<span class="sd">&quot;&quot;&quot;Return list of album titles found in photos database</span>
<span class="sd"> Albums may have duplicate titles -- these will be treated as a single album.</span>
<span class="sd"> </span>
<span class="sd"> Filters out albums in the trash and any special album types</span>
<span class="sd"> Args:</span>
<span class="sd"> shared: boolean; if True, returns shared albums, else normal albums</span>
<span class="sd"> </span>
<span class="sd"> Returns: list of album names</span>
<span class="sd"> &quot;&quot;&quot;</span>
@@ -2725,7 +2949,7 @@
<span class="n">to_date</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
<span class="n">intrash</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 a list of PhotoInfo objects</span>
<span class="sd">&quot;&quot;&quot;Return a list of PhotoInfo objects</span>
<span class="sd"> If called with no args, returns the entire database of photos</span>
<span class="sd"> If called with args, returns photos matching the args (e.g. keywords, persons, etc.)</span>
<span class="sd"> If more than one arg, returns photos matching all the criteria (e.g. keywords AND persons)</span>
@@ -2740,10 +2964,10 @@
<span class="sd"> persons: list of persons to search for</span>
<span class="sd"> albums: list of album names to search for</span>
<span class="sd"> images: if True, returns image files, if False, does not return images; default is True</span>
<span class="sd"> movies: if True, returns movie files, if False, does not return movies; default is True </span>
<span class="sd"> movies: if True, returns movie files, if False, does not return movies; default is True</span>
<span class="sd"> from_date: return photos with creation date &gt;= from_date (datetime.datetime object, default None)</span>
<span class="sd"> to_date: return photos with creation date &lt;= to_date (datetime.datetime object, default None)</span>
<span class="sd"> intrash: if True, returns only images in &quot;Recently deleted items&quot; folder, </span>
<span class="sd"> intrash: if True, returns only images in &quot;Recently deleted items&quot; folder,</span>
<span class="sd"> if False returns only photos that aren&#39;t deleted; default is False</span>
<span class="sd"> Returns:</span>
@@ -2850,7 +3074,7 @@
<span class="k">return</span> <span class="n">photoinfo</span></div>
<div class="viewcode-block" id="PhotosDB.get_photo"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.get_photo">[docs]</a> <span class="k">def</span> <span class="nf">get_photo</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; Returns a single photo matching uuid</span>
<span class="sd">&quot;&quot;&quot;Returns a single photo matching uuid</span>
<span class="sd"> Arguments:</span>
<span class="sd"> uuid: the UUID of photo to get</span>
@@ -2865,7 +3089,7 @@
<span class="c1"># TODO: add to docs and test</span>
<div class="viewcode-block" id="PhotosDB.photos_by_uuid"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.photos_by_uuid">[docs]</a> <span class="k">def</span> <span class="nf">photos_by_uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuids</span><span class="p">):</span>
<span class="sd">&quot;&quot;&quot; Returns a list of photos with UUID in uuids.</span>
<span class="sd">&quot;&quot;&quot;Returns a list of photos with UUID in uuids.</span>
<span class="sd"> Does not generate error if invalid or missing UUID passed.</span>
<span class="sd"> This is faster than using PhotosDB.photos if you have list of UUIDs.</span>
<span class="sd"> Returns photos regardless of intrash state.</span>
@@ -3217,11 +3441,12 @@
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">regex</span><span class="p">:</span>
<span class="n">flags</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">IGNORECASE</span> <span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span> <span class="k">else</span> <span class="mi">0</span>
<span class="n">render_options</span> <span class="o">=</span> <span class="n">RenderOptions</span><span class="p">(</span><span class="n">none_str</span><span class="o">=</span><span class="s2">&quot;&quot;</span><span class="p">)</span>
<span class="n">photo_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">regex</span><span class="p">,</span> <span class="n">template</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">regex</span><span class="p">:</span>
<span class="n">regex</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">compile</span><span class="p">(</span><span class="n">regex</span><span class="p">,</span> <span class="n">flags</span><span class="p">)</span>
<span class="n">photo_list</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
<span class="n">rendered</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">template</span><span class="p">,</span> <span class="n">none_str</span><span class="o">=</span><span class="s2">&quot;&quot;</span><span class="p">)</span>
<span class="n">rendered</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">template</span><span class="p">,</span> <span class="n">render_options</span><span class="p">)</span>
<span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">rendered</span><span class="p">:</span>
<span class="k">if</span> <span class="n">regex</span><span class="o">.</span><span class="n">search</span><span class="p">(</span><span class="n">value</span><span class="p">):</span>
<span class="n">photo_list</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
@@ -3236,8 +3461,99 @@
<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="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">&quot;Invalid query_eval CRITERIA: </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="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">duplicate</span><span class="p">:</span>
<span class="n">no_date</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">tz</span> <span class="o">=</span> <span class="n">timezone</span><span class="p">(</span><span class="n">timedelta</span><span class="p">(</span><span class="mi">0</span><span class="p">))</span>
<span class="n">no_date</span> <span class="o">=</span> <span class="n">no_date</span><span class="o">.</span><span class="n">astimezone</span><span class="p">(</span><span class="n">tz</span><span class="o">=</span><span class="n">tz</span><span class="p">)</span>
<span class="n">photos</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span>
<span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">duplicates</span><span class="p">],</span>
<span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="o">.</span><span class="n">date_added</span> <span class="ow">or</span> <span class="n">no_date</span><span class="p">,</span>
<span class="p">)</span>
<span class="c1"># gather all duplicates but ensure each uuid is only represented once</span>
<span class="n">photodict</span> <span class="o">=</span> <span class="n">OrderedDict</span><span class="p">()</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
<span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">uuid</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">photodict</span><span class="p">:</span>
<span class="n">photodict</span><span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="n">p</span>
<span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="nb">sorted</span><span class="p">(</span>
<span class="n">p</span><span class="o">.</span><span class="n">duplicates</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">x</span><span class="p">:</span> <span class="n">x</span><span class="o">.</span><span class="n">date_added</span> <span class="ow">or</span> <span class="n">no_date</span>
<span class="p">):</span>
<span class="k">if</span> <span class="n">d</span><span class="o">.</span><span class="n">uuid</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">photodict</span><span class="p">:</span>
<span class="n">photodict</span><span class="p">[</span><span class="n">d</span><span class="o">.</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="n">d</span>
<span class="n">photos</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">photodict</span><span class="o">.</span><span class="n">values</span><span class="p">())</span>
<span class="c1"># filter for deleted as photo.duplicates will include photos in the trash</span>
<span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="n">options</span><span class="o">.</span><span class="n">deleted</span> <span class="ow">or</span> <span class="n">options</span><span class="o">.</span><span class="n">deleted_only</span><span class="p">):</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">intrash</span><span class="p">]</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">deleted_only</span><span class="p">:</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">intrash</span><span class="p">]</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">location</span><span class="p">:</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">location</span> <span class="o">!=</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">)]</span>
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">no_location</span><span class="p">:</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">location</span> <span class="o">==</span> <span class="p">(</span><span class="kc">None</span><span class="p">,</span> <span class="kc">None</span><span class="p">)]</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">selected</span><span class="p">:</span>
<span class="c1"># photos selected in Photos app</span>
<span class="k">try</span><span class="p">:</span>
<span class="c1"># catch AppleScript errors as the scripting interfce to Photos is flaky</span>
<span class="n">selected</span> <span class="o">=</span> <span class="n">photoscript</span><span class="o">.</span><span class="n">PhotosLibrary</span><span class="p">()</span><span class="o">.</span><span class="n">selection</span>
<span class="n">selected_uuid</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="o">.</span><span class="n">uuid</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">selected</span><span class="p">]</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">uuid</span> <span class="ow">in</span> <span class="n">selected_uuid</span><span class="p">]</span>
<span class="k">except</span> <span class="ne">Exception</span><span class="p">:</span>
<span class="c1"># no photos selected or a selected photo was &quot;open&quot;</span>
<span class="c1"># selection only works if photos selected in main media browser</span>
<span class="n">photos</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">exif</span><span class="p">:</span>
<span class="n">matching_photos</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span><span class="p">:</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">exiftool</span><span class="p">:</span>
<span class="k">continue</span>
<span class="n">exifdata</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">exiftool</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="n">normalized</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
<span class="n">exifdata</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">p</span><span class="o">.</span><span class="n">exiftool</span><span class="o">.</span><span class="n">asdict</span><span class="p">(</span><span class="n">tag_groups</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">normalized</span><span class="o">=</span><span class="kc">True</span><span class="p">))</span>
<span class="k">for</span> <span class="n">exiftag</span><span class="p">,</span> <span class="n">exifvalue</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">exif</span><span class="p">:</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">ignore_case</span><span class="p">:</span>
<span class="n">exifvalue</span> <span class="o">=</span> <span class="n">exifvalue</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="n">exifdata</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">exiftag</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="s2">&quot;&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="n">exifdata_value</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
<span class="k">elif</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">,</span> <span class="n">Iterable</span><span class="p">):</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="p">[</span><span class="n">v</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">exifdata_value</span><span class="p">]</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">)</span>
<span class="k">if</span> <span class="n">exifvalue</span> <span class="ow">in</span> <span class="n">exifdata_value</span><span class="p">:</span>
<span class="n">matching_photos</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="n">exifdata</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">exiftag</span><span class="o">.</span><span class="n">lower</span><span class="p">(),</span> <span class="s2">&quot;&quot;</span><span class="p">)</span>
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">,</span> <span class="p">(</span><span class="nb">str</span><span class="p">,</span> <span class="n">Iterable</span><span class="p">)):</span>
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">)</span>
<span class="k">if</span> <span class="n">exifvalue</span> <span class="ow">in</span> <span class="n">exifdata_value</span><span class="p">:</span>
<span class="n">matching_photos</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">matching_photos</span>
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">function</span><span class="p">:</span>
<span class="k">for</span> <span class="n">function</span> <span class="ow">in</span> <span class="n">options</span><span class="o">.</span><span class="n">function</span><span class="p">:</span>
<span class="n">photos</span> <span class="o">=</span> <span class="n">function</span><span class="p">[</span><span class="mi">0</span><span class="p">](</span><span class="n">photos</span><span class="p">)</span>
<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="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="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>
<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;original_filesize&quot;</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;imageDate&quot;</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;height&quot;</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;width&quot;</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;UTI&quot;</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;hasAdjustments&quot;</span><span class="p">],</span>
<span class="p">)</span>
<span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<span class="k">return</span> <span class="sa">f</span><span class="s2">&quot;osxphotos.</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="o">.</span><span class="vm">__name__</span><span class="si">}</span><span class="s2">(dbfile=&#39;</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">db_path</span><span class="si">}</span><span class="s2">&#39;)&quot;</span>
@@ -3249,10 +3565,14 @@
<span class="k">return</span> <span class="kc">False</span>
<span class="k">def</span> <span class="fm">__len__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
<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;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>
@@ -3280,7 +3600,7 @@
<span class="k">else</span><span class="p">:</span>
<span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
<span class="n">photos_search</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">x</span> <span class="ow">in</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">p</span><span class="p">,</span> <span class="n">attribute</span><span class="p">))</span>
<span class="k">return</span> <span class="n">photos_search</span>
<span class="k">return</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">photos_search</span><span class="p">))</span>
</pre></div>
</div>
@@ -3317,7 +3637,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>
@@ -3339,7 +3659,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.2</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
</div>

111
docs/_static/basic.css vendored
View File

@@ -130,7 +130,7 @@ ul.search li a {
font-weight: bold;
}
ul.search li div.context {
ul.search li p.context {
color: #888;
margin: 2px 0 0 30px;
text-align: left;
@@ -277,25 +277,25 @@ p.rubric {
font-weight: bold;
}
img.align-left, .figure.align-left, object.align-left {
img.align-left, figure.align-left, .figure.align-left, object.align-left {
clear: left;
float: left;
margin-right: 1em;
}
img.align-right, .figure.align-right, object.align-right {
img.align-right, figure.align-right, .figure.align-right, object.align-right {
clear: right;
float: right;
margin-left: 1em;
}
img.align-center, .figure.align-center, object.align-center {
img.align-center, figure.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
img.align-default, .figure.align-default {
img.align-default, figure.align-default, .figure.align-default {
display: block;
margin-left: auto;
margin-right: auto;
@@ -319,7 +319,8 @@ img.align-default, .figure.align-default {
/* -- sidebars -------------------------------------------------------------- */
div.sidebar {
div.sidebar,
aside.sidebar {
margin: 0 0 0.5em 1em;
border: 1px solid #ddb;
padding: 7px;
@@ -377,12 +378,14 @@ div.body p.centered {
/* -- content of sidebars/topics/admonitions -------------------------------- */
div.sidebar > :last-child,
aside.sidebar > :last-child,
div.topic > :last-child,
div.admonition > :last-child {
margin-bottom: 0;
}
div.sidebar::after,
aside.sidebar::after,
div.topic::after,
div.admonition::after,
blockquote::after {
@@ -455,20 +458,22 @@ td > :last-child {
/* -- figures --------------------------------------------------------------- */
div.figure {
div.figure, figure {
margin: 0.5em;
padding: 0.5em;
}
div.figure p.caption {
div.figure p.caption, figcaption {
padding: 0.3em;
}
div.figure p.caption span.caption-number {
div.figure p.caption span.caption-number,
figcaption span.caption-number {
font-style: italic;
}
div.figure p.caption span.caption-text {
div.figure p.caption span.caption-text,
figcaption span.caption-text {
}
/* -- field list styles ----------------------------------------------------- */
@@ -503,6 +508,63 @@ table.hlist td {
vertical-align: top;
}
/* -- object description styles --------------------------------------------- */
.sig {
font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
}
.sig-name, code.descname {
background-color: transparent;
font-weight: bold;
}
.sig-name {
font-size: 1.1em;
}
code.descname {
font-size: 1.2em;
}
.sig-prename, code.descclassname {
background-color: transparent;
}
.optional {
font-size: 1.3em;
}
.sig-paren {
font-size: larger;
}
.sig-param.n {
font-style: italic;
}
/* C++ specific styling */
.sig-inline.c-texpr,
.sig-inline.cpp-texpr {
font-family: unset;
}
.sig.c .k, .sig.c .kt,
.sig.cpp .k, .sig.cpp .kt {
color: #0033B3;
}
.sig.c .m,
.sig.cpp .m {
color: #1750EB;
}
.sig.c .s, .sig.c .sc,
.sig.cpp .s, .sig.cpp .sc {
color: #067D17;
}
/* -- other body styles ----------------------------------------------------- */
@@ -629,14 +691,6 @@ dl.glossary dt {
font-size: 1.1em;
}
.optional {
font-size: 1.3em;
}
.sig-paren {
font-size: larger;
}
.versionmodified {
font-style: italic;
}
@@ -677,8 +731,9 @@ dl.glossary dt {
.classifier:before {
font-style: normal;
margin: 0.5em;
margin: 0 0.5em;
content: ":";
display: inline-block;
}
abbr, acronym {
@@ -765,8 +820,12 @@ div.code-block-caption code {
table.highlighttable td.linenos,
span.linenos,
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
-webkit-user-select: text; /* Safari fallback only */
-webkit-user-select: none; /* Chrome/Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* IE10+ */
}
div.code-block-caption span.caption-number {
@@ -781,16 +840,6 @@ div.literal-block-wrapper {
margin: 1em 0;
}
code.descname {
background-color: transparent;
font-weight: bold;
font-size: 1.2em;
}
code.descclassname {
background-color: transparent;
}
code.xref, a code {
background-color: transparent;
font-weight: bold;

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.20',
VERSION: '0.44.1',
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() {
@@ -325,7 +328,9 @@ var Search = {
var results = [];
for (var prefix in objects) {
for (var name in objects[prefix]) {
for (var iMatch = 0; iMatch != objects[prefix].length; ++iMatch) {
var match = objects[prefix][iMatch];
var name = match[4];
var fullname = (prefix ? prefix + '.' : '') + name;
var fullnameLower = fullname.toLowerCase()
if (fullnameLower.indexOf(object) > -1) {
@@ -339,7 +344,6 @@ var Search = {
} else if (parts[parts.length - 1].indexOf(object) > -1) {
score += Scorer.objPartialMatch;
}
var match = objects[prefix][name];
var objname = objnames[match[1]][2];
var title = titles[match[0]];
// If more than one term searched for, we require other words to be
@@ -498,6 +502,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() {
@@ -509,7 +516,7 @@ var Search = {
var excerpt = ((start > 0) ? '...' : '') +
$.trim(text.substr(start, 240)) +
((start + 240 - text.length) ? '...' : '');
var rv = $('<div class="context"></div>').text(excerpt);
var rv = $('<p class="context"></p>').text(excerpt);
$.each(hlwords, function() {
rv = rv.highlightText(this, 'highlighted');
});

2042
docs/_static/underscore-1.13.1.js vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,12 @@
<html>
<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.20 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>Welcome to osxphotoss documentation! &#8212; osxphotos 0.44.1 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>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
@@ -31,30 +32,30 @@
<div class="body" role="main">
<div class="section" id="welcome-to-osxphotos-s-documentation">
<section id="welcome-to-osxphotos-s-documentation">
<h1>Welcome to osxphotoss documentation!<a class="headerlink" href="#welcome-to-osxphotos-s-documentation" title="Permalink to this headline"></a></h1>
</div>
<div class="section" id="osxphotos">
</section>
<section id="osxphotos">
<h1>OSXPhotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline"></a></h1>
<div class="section" id="what-is-osxphotos">
<section id="what-is-osxphotos">
<h2>What is osxphotos?<a class="headerlink" href="#what-is-osxphotos" title="Permalink to this headline"></a></h2>
<p>OSXPhotos provides both the ability to interact with and query Apples Photos.app library on macOS directly from your python code
as well as a very flexible command line interface (CLI) app for exporting photos.
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.</p>
</div>
<div class="section" id="supported-operating-systems">
</section>
<section id="supported-operating-systems">
<h2>Supported operating systems<a class="headerlink" href="#supported-operating-systems" title="Permalink to this headline"></a></h2>
<p>Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Catalina (10.15.7).
Beta support for macOS Big Sur (10.16.01/11.01).</p>
<p>Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Big Sur (11.3).</p>
<p>If you have access to macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please contact me via GitHub.</p>
<p>This package will read Photos databases for any supported version on any supported macOS version.
E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa.</p>
<p>Requires python &gt;= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
</div>
<div class="section" id="installation">
</section>
<section id="installation">
<h2>Installation<a class="headerlink" href="#installation" title="Permalink to this headline"></a></h2>
<p>If you are new to python and just want to use the command line application, I recommend you to install using pipx. See other advanced options below.</p>
<div class="section" id="installation-using-pipx">
<section id="installation-using-pipx">
<h3>Installation using pipx<a class="headerlink" href="#installation-using-pipx" title="Permalink to this headline"></a></h3>
<p>If you arent familiar with installing python applications, I recommend you install <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> with <a class="reference external" href="https://github.com/pipxproject/pipx">pipx</a>. If you use <code class="docutils literal notranslate"><span class="pre">pipx</span></code>, you will not need to create a virtual environment as <code class="docutils literal notranslate"><span class="pre">pipx</span></code> takes care of this. The easiest way to do this on a Mac is to use <a class="reference external" href="https://brew.sh/">homebrew</a>:</p>
<ul class="simple">
@@ -64,15 +65,15 @@ E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine
<li><p>Then type this: <code class="docutils literal notranslate"><span class="pre">pipx</span> <span class="pre">install</span> <span class="pre">osxphotos</span></code></p></li>
<li><p>Now you should be able to run <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> by typing: <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code></p></li>
</ul>
</div>
<div class="section" id="installation-using-pip">
</section>
<section id="installation-using-pip">
<h3>Installation using pip<a class="headerlink" href="#installation-using-pip" title="Permalink to this headline"></a></h3>
<p>You can also install directly from <a class="reference external" href="https://pypi.org/project/osxphotos/">pypi</a>:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">pip</span> <span class="n">install</span> <span class="n">osxphotos</span>
</pre></div>
</div>
</div>
<div class="section" id="installation-from-git-repository">
</section>
<section id="installation-from-git-repository">
<h3>Installation from git repository<a class="headerlink" href="#installation-from-git-repository" title="Permalink to this headline"></a></h3>
<p>OSXPhotos uses setuptools, thus simply run:</p>
<div class="highlight-default notranslate"><div class="highlight"><pre><span></span><span class="n">git</span> <span class="n">clone</span> <span class="n">https</span><span class="p">:</span><span class="o">//</span><span class="n">github</span><span class="o">.</span><span class="n">com</span><span class="o">/</span><span class="n">RhetTbull</span><span class="o">/</span><span class="n">osxphotos</span><span class="o">.</span><span class="n">git</span>
@@ -87,9 +88,9 @@ I recommend you install the latest version from <a class="reference external" hr
libraries. If you just want to use the command line utility, you can download a pre-built executable of the latest
<a class="reference external" href="https://github.com/RhetTbull/osxphotos/releases">release</a> or you can install via <code class="docutils literal notranslate"><span class="pre">pip</span></code> which also installs the command line app.
If you arent comfortable with running python on your Mac, start with the pre-built executable or <code class="docutils literal notranslate"><span class="pre">pipx</span></code> as described above.</p>
</div>
</div>
<div class="section" id="command-line-usage">
</section>
</section>
<section id="command-line-usage">
<h2>Command Line Usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline"></a></h2>
<p>This package will install a command line utility called <code class="docutils literal notranslate"><span class="pre">osxphotos</span></code> that allows you to query the Photos database and export photos.
Alternatively, you can also run the command line utility like this: <code class="docutils literal notranslate"><span class="pre">python3</span> <span class="pre">-m</span> <span class="pre">osxphotos</span></code></p>
@@ -122,41 +123,43 @@ Alternatively, you can also run the command line utility like this: <code class=
<span class="n">persons</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">persons</span> <span class="p">(</span><span class="n">faces</span><span class="p">)</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">places</span> <span class="n">Print</span> <span class="n">out</span> <span class="n">places</span> <span class="n">found</span> <span class="ow">in</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">library</span><span class="o">.</span>
<span class="n">query</span> <span class="n">Query</span> <span class="n">the</span> <span class="n">Photos</span> <span class="n">database</span> <span class="n">using</span> <span class="mi">1</span> <span class="ow">or</span> <span class="n">more</span> <span class="n">search</span> <span class="n">options</span><span class="p">;</span> <span class="k">if</span><span class="o">...</span>
<span class="n">repl</span> <span class="n">Run</span> <span class="n">interactive</span> <span class="n">osxphotos</span> <span class="n">shell</span>
<span class="n">tutorial</span> <span class="n">Display</span> <span class="n">osxphotos</span> <span class="n">tutorial</span><span class="o">.</span>
</pre></div>
</div>
<p>To get help on a specific command, use <code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">help</span> <span class="pre">&lt;command_name&gt;</span></code></p>
<div class="section" id="command-line-examples">
<section id="command-line-examples">
<h3>Command line examples<a class="headerlink" href="#command-line-examples" title="Permalink to this headline"></a></h3>
<div class="section" id="export-all-photos-to-desktop-export-group-in-folders-by-date-created">
<section id="export-all-photos-to-desktop-export-group-in-folders-by-date-created">
<h4>export all photos to ~/Desktop/export group in folders by date created<a class="headerlink" href="#export-all-photos-to-desktop-export-group-in-folders-by-date-created" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
<p><strong>Note</strong>: Photos library/database path can also be specified using <code class="docutils literal notranslate"><span class="pre">--db</span></code> option:</p>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">--export-by-date</span> <span class="pre">--db</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">~/Desktop/export</span></code></p>
</div>
<div class="section" id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
</section>
<section id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
<h4>find all photos with keyword “Kids” and output results to json file named results.json:<a class="headerlink" href="#find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--keyword</span> <span class="pre">Kids</span> <span class="pre">--json</span> <span class="pre">~/Pictures/Photos\</span> <span class="pre">Library.photoslibrary</span> <span class="pre">&gt;results.json</span></code></p>
</div>
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
</section>
<section id="export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date">
<h4>export photos to file structure based on 4-digit year and full name of month of photos creation date:<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-and-full-name-of-month-of-photo-s-creation-date" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{created.year}/{created.month}&quot;</span></code></p>
<p>(by default, it will attempt to use the system library)</p>
</div>
<div class="section" id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
</section>
<section id="export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher">
<h4>export photos to file structure based on 4-digit year of photos creation date and add keywords for media type and labels (labels are only awailable on Photos 5 and higher):<a class="headerlink" href="#export-photos-to-file-structure-based-on-4-digit-year-of-photo-s-creation-date-and-add-keywords-for-media-type-and-labels-labels-are-only-awailable-on-photos-5-and-higher" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{created.year}&quot;</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{label}&quot;</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{media_type}&quot;</span></code></p>
</div>
<div class="section" id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
</section>
<section id="export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput">
<h4>export default library using country name/year as output directory (but use “NoCountry/year” if country not specified), add persons, album names, and year as keywords, write exif metadata to files when exporting, update only changed files, print verbose ouput<a class="headerlink" href="#export-default-library-using-country-name-year-as-output-directory-but-use-nocountry-year-if-country-not-specified-add-persons-album-names-and-year-as-keywords-write-exif-metadata-to-files-when-exporting-update-only-changed-files-print-verbose-ouput" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">export</span> <span class="pre">~/Desktop/export</span> <span class="pre">--directory</span> <span class="pre">&quot;{place.name.country,NoCountry}/{created.year}&quot;</span>&#160; <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">&quot;{created.year}&quot;</span> <span class="pre">--exiftool</span> <span class="pre">--update</span> <span class="pre">--verbose</span></code></p>
</div>
<div class="section" id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
</section>
<section id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
<h4>find all videos larger than 200MB and add them to Photos album “Big Videos” creating the album if necessary<a class="headerlink" href="#find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary" title="Permalink to this headline"></a></h4>
<p><code class="docutils literal notranslate"><span class="pre">osxphotos</span> <span class="pre">query</span> <span class="pre">--only-movies</span> <span class="pre">--min-size</span> <span class="pre">200MB</span> <span class="pre">--add-to-album</span> <span class="pre">&quot;Big</span> <span class="pre">Videos&quot;</span></code></p>
</div>
</div>
</div>
<div class="section" id="example-uses-of-the-package">
</section>
</section>
</section>
<section id="example-uses-of-the-package">
<h2>Example uses of the package<a class="headerlink" href="#example-uses-of-the-package" title="Permalink to this headline"></a></h2>
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">&quot;&quot;&quot; Simple usage of the package &quot;&quot;&quot;</span>
<span class="kn">import</span> <span class="nn">osxphotos</span>
@@ -272,46 +275,29 @@ Alternatively, you can also run the command line utility like this: <code class=
<span class="n">export</span><span class="p">()</span> <span class="c1"># pylint: disable=no-value-for-parameter</span>
</pre></div>
</div>
</div>
<div class="section" id="package-interface">
</section>
<section id="package-interface">
<h2>Package Interface<a class="headerlink" href="#package-interface" title="Permalink to this headline"></a></h2>
<p>Reference full documentation on <a class="reference external" href="https://github.com/RhetTbull/osxphotos/blob/master/README.md">GitHub</a></p>
<div class="toctree-wrapper compound">
<ul>
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a><ul>
<li class="toctree-l2"><a class="reference internal" href="cli.html#osxphotos">osxphotos</a><ul>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-about">about</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-albums">albums</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-dump">dump</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-export">export</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-help">help</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-info">info</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-keywords">keywords</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-labels">labels</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-list">list</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-persons">persons</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-places">places</a></li>
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-query">query</a></li>
</ul>
</li>
</ul>
</li>
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a></li>
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a><ul>
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos-module">osxphotos module</a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<div class="section" id="indices-and-tables">
</section>
</section>
<section id="indices-and-tables">
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this headline"></a></h1>
<ul class="simple">
<li><p><a class="reference internal" href="genindex.html"><span class="std std-ref">Index</span></a></p></li>
<li><p><a class="reference internal" href="py-modindex.html"><span class="std std-ref">Module Index</span></a></p></li>
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
</ul>
</div>
</section>
</div>
@@ -347,7 +333,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>
@@ -369,7 +355,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 3.5.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|

View File

@@ -4,11 +4,12 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>osxphotos &#8212; osxphotos 0.42.20 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta name="generator" content="Docutils 0.17.1: http://docutils.sourceforge.net/" />
<title>osxphotos &#8212; osxphotos 0.44.1 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>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
@@ -30,11 +31,11 @@
<div class="body" role="main">
<div class="section" id="osxphotos">
<section id="osxphotos">
<h1>osxphotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline"></a></h1>
<div class="toctree-wrapper compound">
</div>
</div>
</section>
</div>
@@ -69,7 +70,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 +92,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
&amp; <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -5,11 +5,11 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Search &#8212; osxphotos 0.42.20 documentation</title>
<link rel="stylesheet" href="_static/pygments.css" type="text/css" />
<link rel="stylesheet" href="_static/alabaster.css" type="text/css" />
<title>Search &#8212; osxphotos 0.44.1 documentation</title>
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
<script id="documentation_options" data-url_root="./" src="_static/documentation_options.js"></script>
<script data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
<script src="_static/jquery.js"></script>
<script src="_static/underscore.js"></script>
<script src="_static/doctools.js"></script>
@@ -37,26 +37,35 @@
<div class="body" role="main">
<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>
Searching for multiple words only shows matches that contain
all words.
</p>
<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>
<div id="search-results">
</div>
</div>
@@ -102,7 +111,7 @@
&copy;2021, Rhet Turnbull.
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 3.5.2</a>
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</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

@@ -10,24 +10,6 @@ from osxphotos.path_utils import sanitize_dirname
from osxphotos.phototemplate import RenderOptions
def _get_album_sort_order(album: AlbumInfo, photo: PhotoInfo) -> Optional[int]:
"""Get the sort order of photo in album
Returns: sort order as int or None if photo not found in album
"""
# get the album sort order from the album_info
sort_order = 0 # change this to 1 if you want counting to start at 1
for album_photo in album.photos:
if album_photo.uuid == photo.uuid:
# found the photo we're processing
break
sort_order += 1
else:
# didn't find the photo, so skip this file
return None
return sort_order
def album_sequence(photo: PhotoInfo, options: RenderOptions, **kwargs) -> str:
"""Call this with {function} template to get album sequence (sort order) when exporting with {folder_album} template
@@ -59,7 +41,7 @@ def album_sequence(photo: PhotoInfo, options: RenderOptions, **kwargs) -> str:
def album_sort_order(
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
):
"""Call this with osxphotos export /path/to/export --post-function post_function.py::post_function
"""Call this with osxphotos export /path/to/export --post-function album_sort_order.py::album_sort_order
This will get called immediately after the photo has been exported
Args:
@@ -119,9 +101,10 @@ def album_sort_order(
# didn't find the album, so skip this file
return
sort_order = _get_album_sort_order(album_info, photo)
if sort_order is None:
# didn't find the photo, so skip this file
try:
sort_order = album_info.photo_index(photo)
except ValueError:
# photo not in album, so skip this file
return
verbose(f"Sort order for {filepath} in album {album_dir} is {sort_order}")

View File

@@ -30,6 +30,8 @@ _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
# Photos 6 (10.16.0 Beta) == 14104
_TEST_MODEL_VERSIONS = ["13537", "13703", "14104"]
_PHOTOS_2_VERSION = "2622"
# only version 3 - 4 have RKVersion.selfPortrait
_PHOTOS_3_VERSION = "3301"
@@ -94,6 +96,8 @@ _TESTED_OS_VERSIONS = [
("11", "2"),
("11", "3"),
("11", "4"),
("11", "5"),
("11", "6"),
]
# Photos 5 has persons who are empty string if unidentified face
@@ -119,12 +123,20 @@ _XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
# Constants used for processing folders and albums
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
_PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared album
_PHOTOS_5_PROJECT_ALBUM_KIND = 1508 # My Projects (e.g. Calendar, Card, Slideshow)
_PHOTOS_5_FOLDER_KIND = 4000 # user folder
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session
_PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass
_PHOTOS_4_TOP_LEVEL_ALBUM = "TopLevelAlbums"
_PHOTOS_4_ALBUM_TYPE_ALBUM = 1 # RKAlbum.albumType
_PHOTOS_4_ALBUM_TYPE_PROJECT = 9 # RKAlbum.albumType
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW = 8 # RKAlbum.albumType
_PHOTOS_4_TOP_LEVEL_ALBUMS = [
"TopLevelAlbums",
"TopLevelKeepsakes",
"TopLevelSlideshows",
]
_PHOTOS_4_ROOT_FOLDER = "LibraryFolder"
# EXIF related constants
@@ -275,10 +287,15 @@ POST_COMMAND_CATEGORIES = {
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
}
class AlbumSortOrder(Enum):
"""Album Sort Order"""
UNKNOWN = 0
MANUAL = 1
NEWEST_FIRST = 2
OLDEST_FIRST = 3
TITLE = 5
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.65"
__version__ = "0.44.2"

View File

@@ -14,7 +14,7 @@ from datetime import datetime, timedelta, timezone
from ._constants import (
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_TOP_LEVEL_ALBUM,
_PHOTOS_4_TOP_LEVEL_ALBUMS,
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
@@ -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)
@@ -138,7 +161,6 @@ class AlbumInfoBaseClass:
class AlbumInfo(AlbumInfoBaseClass):
"""
Base class for AlbumInfo, ImportInfo
Info about a specific Album, contains all the details about the album
including folders, photos, etc.
"""
@@ -208,7 +230,7 @@ class AlbumInfo(AlbumInfoBaseClass):
parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
self._parent = (
FolderInfo(db=self._db, uuid=parent_uuid)
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM
if parent_uuid not in _PHOTOS_4_TOP_LEVEL_ALBUMS
else None
)
else:
@@ -243,18 +265,17 @@ class AlbumInfo(AlbumInfoBaseClass):
def photo_index(self, photo):
"""return index of photo in album (based on album sort order)"""
index = 0
for p in self.photos:
for index, p in enumerate(self.photos):
if p.uuid == photo.uuid:
return index
index += 1
else:
raise ValueError(
f"Photo with uuid {photo.uuid} does not appear to be in this album"
)
raise ValueError(
f"Photo with uuid {photo.uuid} does not appear to be in this album"
)
class ImportInfo(AlbumInfoBaseClass):
"""Information about import sessions"""
@property
def photos(self):
"""return list of photos contained in import session"""
@@ -273,6 +294,15 @@ class ImportInfo(AlbumInfoBaseClass):
return self._photos
class ProjectInfo(AlbumInfo):
"""
ProjectInfo with info about projects
Projects are cards, calendars, slideshows, etc.
"""
...
class FolderInfo:
"""
Info about a specific folder, contains all the details about the folder
@@ -334,7 +364,7 @@ class FolderInfo:
parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"]
self._parent = (
FolderInfo(db=self._db, uuid=parent_uuid)
if parent_uuid != _PHOTOS_4_TOP_LEVEL_ALBUM
if parent_uuid not in _PHOTOS_4_TOP_LEVEL_ALBUMS
else None
)
else:

View File

@@ -20,6 +20,7 @@ import photoscript
import rich.traceback
import yaml
from rich import pretty
from runpy import run_module
import osxphotos
@@ -55,13 +56,14 @@ from .exiftool import get_exiftool_path
from .export_db import ExportDB, ExportDBInMemory
from .fileutil import FileUtil, FileUtilNoOp
from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
from .photoinfo import ExportResults
from .photoinfo import ExportResults, PhotoInfo
from .photokit import check_photokit_authorization, request_photokit_authorization
from .photosalbum import PhotosAlbum
from .phototemplate import PhotoTemplate, RenderOptions
from .pyrepl import embed_repl
from .queryoptions import QueryOptions
from .uti import get_preferred_uti_extension
from .utils import expand_and_validate_filepath, load_function
from .utils import expand_and_validate_filepath, load_function, normalize_fs_path
# global variable to control verbose output
# set via --verbose/-V
@@ -296,7 +298,8 @@ def QUERY_OPTIONS(f):
metavar="UUID",
default=None,
multiple=True,
help="Search for photos with UUID(s).",
help="Search for photos with UUID(s). "
"May be repeated to include multiple UUIDs.",
),
o(
"--uuid-from-file",
@@ -541,6 +544,17 @@ def QUERY_OPTIONS(f):
is_flag=True,
help="Filter for photos that are currently selected in Photos.",
),
o(
"--exif",
metavar="EXIF_TAG VALUE",
nargs=2,
multiple=True,
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
"To use --exif, exiftool must be installed and in the path.",
),
o(
"--query-eval",
metavar="CRITERIA",
@@ -686,6 +700,23 @@ def cli(ctx, db, json_, debug):
"Note: this does not skip RAW photos if the RAW photo does not have an associated JPEG image "
"(e.g. the RAW file was imported to Photos without a JPEG preview).",
)
@click.option(
"--skip-uuid",
metavar="UUID",
default=None,
multiple=True,
help="Skip photos with UUID(s) during export. "
"May be repeated to include multiple UUIDs.",
)
@click.option(
"--skip-uuid-from-file",
metavar="FILE",
default=None,
multiple=False,
help="Skip photos with UUID(s) loaded from FILE. "
"Format is a single UUID per line. Lines preceded with # are ignored.",
type=click.Path(exists=True),
)
@click.option(
"--current-name",
is_flag=True,
@@ -1111,6 +1142,8 @@ def export(
skip_bursts,
skip_live,
skip_raw,
skip_uuid,
skip_uuid_from_file,
person_keyword,
album_keyword,
keyword_template,
@@ -1188,6 +1221,7 @@ def export(
max_size,
regex,
selected,
exif,
query_eval,
query_function,
duplicate,
@@ -1278,6 +1312,8 @@ def export(
skip_bursts = cfg.skip_bursts
skip_live = cfg.skip_live
skip_raw = cfg.skip_raw
skip_uuid = cfg.skip_uuid
skip_uuid_from_file = cfg.skip_uuid_from_file
person_keyword = cfg.person_keyword
album_keyword = cfg.album_keyword
keyword_template = cfg.keyword_template
@@ -1352,6 +1388,7 @@ def export(
max_size = cfg.max_size
regex = cfg.regex
selected = cfg.selected
exif = cfg.exif
query_eval = cfg.query_eval
query_function = cfg.query_function
duplicate = cfg.duplicate
@@ -1671,6 +1708,7 @@ def export(
max_size=max_size,
regex=regex,
selected=selected,
exif=exif,
query_eval=query_eval,
function=query_function,
duplicate=duplicate,
@@ -1687,6 +1725,13 @@ def export(
else:
raise ValueError(e)
if skip_uuid:
photos = [p for p in photos if p.uuid not in skip_uuid]
if skip_uuid_from_file:
skip_uuid_list = load_uuid_from_file(skip_uuid_from_file)
photos = [p for p in photos if p.uuid not in skip_uuid_list]
if photos and only_new:
# ignore previously exported files
previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()}
@@ -1796,6 +1841,7 @@ def export(
export_dir=dest,
dry_run=dry_run,
exiftool_path=exiftool_path,
export_db=export_db,
)
if album_export and export_results.exported:
@@ -1865,6 +1911,7 @@ def export(
finder_tag_template=finder_tag_template,
strip=strip,
export_dir=dest,
export_db=export_db,
)
results.xattr_written.extend(tags_written)
results.xattr_skipped.extend(tags_skipped)
@@ -1876,6 +1923,7 @@ def export(
xattr_template,
strip=strip,
export_dir=dest,
export_db=export_db,
)
results.xattr_written.extend(xattr_written)
results.xattr_skipped.extend(xattr_skipped)
@@ -2080,6 +2128,7 @@ def query(
max_size,
regex,
selected,
exif,
query_eval,
query_function,
add_to_album,
@@ -2115,6 +2164,7 @@ def query(
max_size,
regex,
selected,
exif,
duplicate,
]
exclusive = [
@@ -2246,6 +2296,7 @@ def query(
function=query_function,
regex=regex,
selected=selected,
exif=exif,
duplicate=duplicate,
)
@@ -2538,10 +2589,22 @@ def export_photo(
sidecar_flags |= SIDECAR_EXIFTOOL
rendered_suffix = _render_suffix_template(
original_suffix, "original_suffix", "--original-suffix", strip, dest, photo
original_suffix,
"original_suffix",
"--original-suffix",
strip,
dest,
photo,
export_db,
)
rendered_preview_suffix = _render_suffix_template(
preview_suffix, "preview_suffix", "--preview-suffix", strip, dest, photo
preview_suffix,
"preview_suffix",
"--preview-suffix",
strip,
dest,
photo,
export_db,
)
# if download_missing and the photo is missing or path doesn't exist,
@@ -2557,11 +2620,24 @@ def export_photo(
results = ExportResults()
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=False
photo,
directory,
export_by_date,
dest,
dry_run,
strip=strip,
edited=False,
export_db=export_db,
)
for dest_path in dest_paths:
filenames = get_filenames_from_template(
photo, filename_template, dest, dest_path, original_name, strip=strip
photo,
filename_template,
dest,
dest_path,
original_name,
strip=strip,
export_db=export_db,
)
for filename in filenames:
@@ -2632,12 +2708,26 @@ def export_photo(
if export_edited and photo.hasadjustments:
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=True
photo,
directory,
export_by_date,
dest,
dry_run,
strip=strip,
edited=True,
export_db=export_db,
)
for dest_path in dest_paths:
# if export-edited, also export the edited version
edited_filenames = get_filenames_from_template(
photo, filename_template, dest, dest_path, original_name, strip=strip, edited=True
photo,
filename_template,
dest,
dest_path,
original_name,
strip=strip,
edited=True,
export_db=export_db,
)
for edited_filename in edited_filenames:
edited_filename = pathlib.Path(edited_filename)
@@ -2674,6 +2764,7 @@ def export_photo(
strip,
dest,
photo,
export_db,
)
edited_filename = (
f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}"
@@ -2729,7 +2820,9 @@ def export_photo(
return results
def _render_suffix_template(suffix_template, var_name, option_name, strip, dest, photo):
def _render_suffix_template(
suffix_template, var_name, option_name, strip, dest, photo, export_db
):
"""render suffix template
Returns:
@@ -2739,7 +2832,7 @@ def _render_suffix_template(suffix_template, var_name, option_name, strip, dest,
return ""
try:
options = RenderOptions(filename=True, strip=strip, export_dir=dest)
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(
@@ -2756,6 +2849,10 @@ def _render_suffix_template(suffix_template, var_name, option_name, strip, dest,
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]
@@ -2848,7 +2945,9 @@ def export_photo_to_directory(
results.missing.append(str(pathlib.Path(dest_path) / filename))
return results
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
render_options = RenderOptions(
export_dir=export_dir, dest_path=dest_path, exportdb=export_db
)
tries = 0
while tries <= retry:
@@ -2917,7 +3016,7 @@ def export_photo_to_directory(
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo(
@@ -2960,6 +3059,7 @@ def get_filenames_from_template(
original_name,
strip=False,
edited=False,
export_db=None,
):
"""get list of export filenames for a photo
@@ -2983,10 +3083,10 @@ def get_filenames_from_template(
options = RenderOptions(
path_sep="_",
filename=True,
strip=strip,
edited_version=edited,
export_dir=export_dir,
dest_path=dest_path,
exportdb=export_db,
)
filenames, unmatched = photo.render_template(filename_template, options)
except ValueError as e:
@@ -3006,12 +3106,22 @@ 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
def get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=False, edited=False
photo,
directory,
export_by_date,
dest,
dry_run,
strip=False,
edited=False,
export_db=None,
):
"""get list of directories to export a photo into, creates directories if they don't exist
@@ -3042,7 +3152,9 @@ def get_dirnames_from_template(
elif directory:
# got a directory template, render it and check results are valid
try:
options = RenderOptions(dirname=True, strip=strip, edited_version=edited)
options = RenderOptions(
dirname=True, edited_version=edited, exportdb=export_db
)
dirnames, unmatched = photo.render_template(directory, options)
except ValueError as e:
raise click.BadOptionUsage(
@@ -3056,6 +3168,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):
@@ -3291,11 +3405,13 @@ def cleanup_files(dest_path, files_to_keep, fileutil):
Returns:
tuple of (list of files deleted, list of directories deleted)
"""
keepers = {str(filename).lower(): 1 for filename in files_to_keep}
keepers = {
normalize_fs_path(str(filename).lower()): 1 for filename in files_to_keep
}
deleted_files = []
for p in pathlib.Path(dest_path).rglob("*"):
path = str(p).lower()
path = normalize_fs_path(str(p).lower())
if p.is_file() and path not in keepers:
verbose_(f"Deleting {p}")
fileutil.unlink(p)
@@ -3325,6 +3441,7 @@ def write_finder_tags(
finder_tag_template=None,
strip=False,
export_dir=None,
export_db=None,
):
"""Write Finder tags (extended attributes) to files; only writes attributes if attributes on file differ from what would be written
@@ -3338,6 +3455,7 @@ def write_finder_tags(
exiftool_merge_keywords: if True, include any keywords in the exif data of the source image as keywords
finder_tag_template: list of templates to evaluate for determining Finder tags
export_dir: value to use for {export_dir} template
export_db: an ExportDB object
Returns:
(list of file paths that were updated with new Finder tags, list of file paths skipped because Finder tags didn't need updating)
@@ -3367,8 +3485,8 @@ def write_finder_tags(
options = RenderOptions(
none_str=_OSXPHOTOS_NONE_SENTINEL,
path_sep="/",
strip=strip,
export_dir=export_dir,
exportdb=export_db,
)
rendered, unmatched = photo.render_template(template_str, options)
except ValueError as e:
@@ -3388,6 +3506,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
]
@@ -3408,7 +3529,12 @@ def write_finder_tags(
def write_extended_attributes(
photo, files, xattr_template, strip=False, export_dir=None
photo,
files,
xattr_template,
strip=False,
export_dir=None,
export_db=None,
):
"""Writes extended attributes to exported files
@@ -3416,6 +3542,7 @@ def write_extended_attributes(
photo: a PhotoInfo object
strip: xattr_template: list of tuples: (attribute name, attribute template)
export_dir: value to use for {export_dir} template
exportdb: an ExportDB object
Returns:
tuple(list of file paths that were updated with new attributes, list of file paths skipped because attributes didn't need updating)
@@ -3427,8 +3554,8 @@ def write_extended_attributes(
options = RenderOptions(
none_str=_OSXPHOTOS_NONE_SENTINEL,
path_sep="/",
strip=strip,
export_dir=export_dir,
exportdb=export_db,
)
rendered, unmatched = photo.render_template(template_str, options)
except ValueError as e:
@@ -3446,6 +3573,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:
@@ -3480,7 +3610,7 @@ def write_extended_attributes(
def run_post_command(
photo, post_command, export_results, export_dir, dry_run, exiftool_path
photo, post_command, export_results, export_dir, dry_run, exiftool_path, export_db
):
# todo: pass in RenderOptions from export? (e.g. so it contains strip, etc?)
# todo: need a shell_quote template type:
@@ -3492,7 +3622,9 @@ def run_post_command(
# some categories, like error, return a tuple of (file, error str)
if isinstance(f, tuple):
f = f[0]
render_options = RenderOptions(export_dir=export_dir, filepath=f)
render_options = RenderOptions(
export_dir=export_dir, filepath=f, exportdb=export_db
)
template = PhotoTemplate(photo, exiftool_path=exiftool_path)
command, _ = template.render(command_template, options=render_options)
command = command[0] if command else None
@@ -3519,6 +3651,30 @@ def run_post_command(
)
@cli.command()
@click.argument("packages", nargs=-1, required=True)
@click.option(
"-U", "--upgrade", is_flag=True, help="Upgrade packages to latest version"
)
def install(packages, upgrade):
"""Install Python packages into the same environment as osxphotos"""
args = ["pip", "install"]
if upgrade:
args += ["--upgrade"]
args += list(packages)
sys.argv = args
run_module("pip", run_name="__main__")
@cli.command()
@click.argument("packages", nargs=-1, required=True)
@click.option("-y", "--yes", is_flag=True, help="Don't ask for confirmation")
def uninstall(packages, yes):
"""Uninstall Python packages from the osxphotos environment"""
sys.argv = ["pip", "uninstall"] + list(packages) + (["-y"] if yes else [])
run_module("pip", run_name="__main__")
@cli.command(hidden=True)
@DB_OPTION
@DB_ARGUMENT
@@ -3532,7 +3688,8 @@ def run_post_command(
@click.option(
"--uuid",
metavar="UUID",
help="Use with '--dump photos' to dump only certain UUIDs",
help="Use with '--dump photos' to dump only certain UUIDs. "
"May be repeated to include multiple UUIDs.",
multiple=True,
)
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
@@ -3929,6 +4086,32 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
osxphotos uses the following 3rd party software licensed under the BSD-3-Clause License:
Click (Copyright 2014 Pallets), ptpython (Copyright (c) 2015, Jonathan Slenders)
Redistribution and use in source and binary forms, with or without modification, are
permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list
of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list
of conditions and the following disclaimer in the documentation and/or other materials
provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be
used to endorse or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
click.echo(f"osxphotos, version {__version__}")
click.echo("")
@@ -3950,7 +4133,7 @@ def tutorial(ctx, cli_obj, width):
click.echo_via_pager(tutorial_help(width=width))
def _show_photo(photo):
def _show_photo(photo: PhotoInfo):
"""open image with default image viewer
Note: This is for debugging only -- it will actually open any filetype which could
@@ -3996,12 +4179,38 @@ def _get_selected(photosdb):
return get_selected
def _spotlight_photo(photo: PhotoInfo):
photo_ = photoscript.Photo(photo.uuid)
photo_.spotlight()
@cli.command()
@DB_OPTION
@click.pass_obj
@click.pass_context
def repl(ctx, cli_obj, db):
"""Run interactive osxphotos shell"""
@click.option(
"--emacs",
required=False,
is_flag=True,
default=False,
help="Launch REPL with Emacs keybindings (default is vi bindings)",
)
def repl(ctx, cli_obj, db, emacs):
"""Run interactive osxphotos REPL shell (useful for debugging, prototyping, and inspecting your Photos library)"""
import logging
from objexplore import explore
from photoscript import Album, Photo, PhotosLibrary
from rich import inspect as _inspect
from osxphotos import ExifTool, PhotoInfo, PhotosDB
from osxphotos.albuminfo import AlbumInfo
from osxphotos.placeinfo import PlaceInfo
from osxphotos.queryoptions import QueryOptions
logger = logging.getLogger()
logger.disabled = True
pretty.install()
print(f"python version: {sys.version}")
print(f"osxphotos version: {osxphotos._version.__version__}")
@@ -4016,22 +4225,58 @@ def repl(ctx, cli_obj, db):
# shortcut for helper functions
get_photo = photosdb.get_photo
show = _show_photo
spotlight = _spotlight_photo
get_selected = _get_selected(photosdb)
try:
selected = get_selected()
except Exception:
# get_selected sometimes fails
selected = []
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds")
def inspect(obj):
"""inspect object"""
return _inspect(obj, methods=True)
print(f"Found {len(photos)} photos in {tictoc:0.2f} seconds\n")
print("The following classes have been imported from osxphotos:")
print("- AlbumInfo, ExifTool, PhotoInfo, PhotosDB, PlaceInfo, QueryOptions\n")
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")
print(
f"- get_selected(): return list of PhotoInfo objects for photos selected in Photos"
f"- get_photo(uuid): return a PhotoInfo object for photo with uuid; e.g. get_photo('B13F4485-94E0-41CD-AF71-913095D62E31')"
)
print(f"- show(photo): open a photo object in the default viewer")
print(
f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
f"- get_selected(); return list of PhotoInfo objects for photos selected in Photos"
)
print(
f"- show(photo): open a photo object in the default viewer; e.g. show(selected[0])"
)
print(
f"- show(path): open a file at path in the default viewer; e.g. show('/path/to/photo.jpg')"
)
print(f"- spotlight(photo): open a photo and spotlight it in Photos")
# print(
# f"- help(object): print help text including list of methods for object; for example, help(PhotosDB)"
# )
print(
f"- inspect(object): print information about an object; e.g. inspect(PhotoInfo)"
)
print(
f"- explore(object): interactively explore an object with objexplore; e.g. explore(PhotoInfo)"
)
print(f"- q, quit, quit(), exit, exit(): exit this interactive shell\n")
embed_repl(
globals=globals(),
locals=locals(),
history_filename=str(pathlib.Path.home() / ".osxphotos_repl_history"),
quit_words=["q", "quit", "exit"],
vi_mode=not emacs,
)
print(f"- quit(): exit this interactive shell\n")
code.interact(banner="", local=locals())

View File

@@ -207,7 +207,7 @@ The following attributes may be used with '--xattr-template':
+ "The following categories are available: "
)
formatter.write("\n")
templ_tuples = [("Catgory", "Description")]
templ_tuples = [("Category", "Description")]
templ_tuples.extend((k, v) for k, v in POST_COMMAND_CATEGORIES.items())
formatter.write_dl(templ_tuples)
formatter.write("\n")
@@ -224,13 +224,13 @@ The following attributes may be used with '--xattr-template':
)
formatter.write("\n")
formatter.write(
'--post-command new "echo {filepath.name|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"'
'--post-command new "echo {filepath|shell_quote} >> {shell_quote,{export_dir}/exported.txt}"'
)
formatter.write("\n\n")
formatter.write_text(
"In the above command, the 'shell_quote' filter is used to ensure '{filepath.name}' is properly quoted "
"In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is properly quoted "
+ "and the '{shell_quote}' template ensures the constructed path of '{exported_dir}/exported.txt' is properly quoted. "
"If '{filepath.name}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command "
"If '{filepath}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command "
"thus renders to: "
)
formatter.write("\n")

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

@@ -1,6 +1,4 @@
""" Helper class for managing a database used by
PhotoInfo.export for tracking state of exports and updates
"""
""" Helper class for managing a database used by PhotoInfo.export for tracking state of exports and updates """
import datetime
import logging
@@ -12,13 +10,15 @@ from abc import ABC, abstractmethod
from io import StringIO
from sqlite3 import Error
from ._constants import OSXPHOTOS_EXPORT_DB
from ._version import __version__
OSXPHOTOS_EXPORTDB_VERSION = "3.2"
OSXPHOTOS_EXPORTDB_VERSION = "4.0"
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {str(datetime.datetime.now())}"
class ExportDB_ABC(ABC):
""" abstract base class for ExportDB """
"""abstract base class for ExportDB"""
@abstractmethod
def get_uuid_for_file(self, filename):
@@ -88,6 +88,14 @@ class ExportDB_ABC(ABC):
def get_previous_uuids(self):
pass
@abstractmethod
def get_detected_text_for_uuid(self, uuid):
pass
@abstractmethod
def set_detected_text_for_uuid(self, uuid, json_text):
pass
@abstractmethod
def set_data(
self,
@@ -104,7 +112,7 @@ class ExportDB_ABC(ABC):
class ExportDBNoOp(ExportDB_ABC):
""" An ExportDB with NoOp methods """
"""An ExportDB with NoOp methods"""
def __init__(self):
self.was_created = True
@@ -162,6 +170,12 @@ class ExportDBNoOp(ExportDB_ABC):
def get_previous_uuids(self):
return []
def get_detected_text_for_uuid(self, uuid):
return None
def set_detected_text_for_uuid(self, uuid, json_text):
pass
def set_data(
self,
filename,
@@ -177,23 +191,23 @@ class ExportDBNoOp(ExportDB_ABC):
class ExportDB(ExportDB_ABC):
""" Interface to sqlite3 database used to store state information for osxphotos export command """
"""Interface to sqlite3 database used to store state information for osxphotos export command"""
def __init__(self, dbfile):
""" dbfile: path to osxphotos export database file """
"""dbfile: path to osxphotos export database file"""
self._dbfile = dbfile
# _path is parent of the database
# all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this parent _path
# this allows the entire export tree to be moved to a new disk/location
# whilst preserving the UUID to filename mappping
# whilst preserving the UUID to filename mapping
self._path = pathlib.Path(dbfile).parent
self._conn = self._open_export_db(dbfile)
self._insert_run_info()
def get_uuid_for_file(self, filename):
""" query database for filename and return UUID
returns None if filename not found in database
"""query database for filename and return UUID
returns None if filename not found in database
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
@@ -211,7 +225,7 @@ class ExportDB(ExportDB_ABC):
return uuid
def set_uuid_for_file(self, filename, uuid):
""" set UUID of filename to uuid in the database """
"""set UUID of filename to uuid in the database"""
filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower()
conn = self._conn
@@ -226,9 +240,9 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def set_stat_orig_for_file(self, filename, stats):
""" set stat info for filename
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for filename
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
@@ -247,8 +261,8 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_stat_orig_for_file(self, filename):
""" get stat info for filename
returns: tuple of (mode, size, mtime)
"""get stat info for filename
returns: tuple of (mode, size, mtime)
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
@@ -272,21 +286,21 @@ class ExportDB(ExportDB_ABC):
return stats
def set_stat_edited_for_file(self, filename, stats):
""" set stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
return self._set_stat_for_file("edited", filename, stats)
def get_stat_edited_for_file(self, filename):
""" get stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""get stat info for edited version of image (in Photos' library)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
return self._get_stat_for_file("edited", filename)
def set_stat_exif_for_file(self, filename, stats):
""" set stat info for filename (after exiftool has updated it)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for filename (after exiftool has updated it)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
if len(stats) != 3:
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
@@ -305,8 +319,8 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_stat_exif_for_file(self, filename):
""" get stat info for filename (after exiftool has updated it)
returns: tuple of (mode, size, mtime)
"""get stat info for filename (after exiftool has updated it)
returns: tuple of (mode, size, mtime)
"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
@@ -330,19 +344,19 @@ class ExportDB(ExportDB_ABC):
return stats
def set_stat_converted_for_file(self, filename, stats):
""" set stat info for filename (after image converted to jpeg)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime """
"""set stat info for filename (after image converted to jpeg)
filename: filename to set the stat info for
stat: a tuple of length 3: mode, size, mtime"""
return self._set_stat_for_file("converted", filename, stats)
def get_stat_converted_for_file(self, filename):
""" get stat info for filename (after jpeg conversion)
returns: tuple of (mode, size, mtime)
"""get stat info for filename (after jpeg conversion)
returns: tuple of (mode, size, mtime)
"""
return self._get_stat_for_file("converted", filename)
def get_info_for_uuid(self, uuid):
""" returns the info JSON struct for a UUID """
"""returns the info JSON struct for a UUID"""
conn = self._conn
try:
c = conn.cursor()
@@ -356,7 +370,7 @@ class ExportDB(ExportDB_ABC):
return info
def set_info_for_uuid(self, uuid, info):
""" sets the info JSON struct for a UUID """
"""sets the info JSON struct for a UUID"""
conn = self._conn
try:
c = conn.cursor()
@@ -369,7 +383,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_exifdata_for_file(self, filename):
""" returns the exifdata JSON struct for a file """
"""returns the exifdata JSON struct for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -387,7 +401,7 @@ class ExportDB(ExportDB_ABC):
return exifdata
def set_exifdata_for_file(self, filename, exifdata):
""" sets the exifdata JSON struct for a file """
"""sets the exifdata JSON struct for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -401,7 +415,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_sidecar_for_file(self, filename):
""" returns the sidecar data and signature for a file """
"""returns the sidecar data and signature for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -429,7 +443,7 @@ class ExportDB(ExportDB_ABC):
return sidecar_data, sidecar_sig
def set_sidecar_for_file(self, filename, sidecar_data, sidecar_sig):
""" sets the sidecar data and signature for a file """
"""sets the sidecar data and signature for a file"""
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
conn = self._conn
try:
@@ -443,7 +457,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def get_previous_uuids(self):
"""returns list of UUIDs of previously exported photos found in export database """
"""returns list of UUIDs of previously exported photos found in export database"""
conn = self._conn
previous_uuids = []
try:
@@ -455,6 +469,36 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
return previous_uuids
def get_detected_text_for_uuid(self, uuid):
"""Get the detected_text for a uuid"""
conn = self._conn
try:
c = conn.cursor()
c.execute(
"SELECT text_data FROM detected_text WHERE uuid = ?",
(uuid,),
)
results = c.fetchone()
detected_text = results[0] if results else None
except Error as e:
logging.warning(e)
detected_text = None
return detected_text
def set_detected_text_for_uuid(self, uuid, text_json):
"""Set the detected text for uuid"""
conn = self._conn
try:
c = conn.cursor()
c.execute(
"INSERT OR REPLACE INTO detected_text(uuid, text_data) VALUES (?, ?);",
(uuid, text_json,),
)
conn.commit()
except Error as e:
logging.warning(e)
def set_data(
self,
filename,
@@ -466,8 +510,7 @@ class ExportDB(ExportDB_ABC):
info_json,
exif_json,
):
""" sets all the data for file and uuid at once
"""
"""sets all the data for file and uuid at once"""
filename = str(pathlib.Path(filename).relative_to(self._path))
filename_normalized = filename.lower()
conn = self._conn
@@ -510,7 +553,7 @@ class ExportDB(ExportDB_ABC):
logging.warning(e)
def close(self):
""" close the database connection """
"""close the database connection"""
try:
self._conn.close()
except Error as e:
@@ -548,9 +591,9 @@ class ExportDB(ExportDB_ABC):
return stats
def _open_export_db(self, dbfile):
""" open export database and return a db connection
if dbfile does not exist, will create and initialize the database
returns: connection to the database
"""open export database and return a db connection
if dbfile does not exist, will create and initialize the database
returns: connection to the database
"""
if not os.path.isfile(dbfile):
@@ -573,7 +616,7 @@ class ExportDB(ExportDB_ABC):
return conn
def _get_db_connection(self, dbfile):
""" return db connection to dbname """
"""return db connection to dbname"""
try:
conn = sqlite3.connect(dbfile)
except Error as e:
@@ -583,15 +626,15 @@ class ExportDB(ExportDB_ABC):
return conn
def _get_database_version(self, conn):
""" return tuple of (osxphotos, exportdb) versions for database connection conn """
"""return tuple of (osxphotos, exportdb) versions for database connection conn"""
version_info = conn.execute(
"SELECT osxphotos, exportdb, max(id) FROM version"
).fetchone()
return (version_info[0], version_info[1])
def _create_db_tables(self, conn):
""" create (if not already created) the necessary db tables for the export database
conn: sqlite3 db connection
"""create (if not already created) the necessary db tables for the export database
conn: sqlite3 db connection
"""
sql_commands = {
"sql_version_table": """ CREATE TABLE IF NOT EXISTS version (
@@ -599,6 +642,10 @@ class ExportDB(ExportDB_ABC):
osxphotos TEXT,
exportdb TEXT
); """,
"sql_about_table": """ CREATE TABLE IF NOT EXISTS about (
id INTEGER PRIMARY KEY,
about TEXT
);""",
"sql_files_table": """ CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY,
filepath TEXT NOT NULL,
@@ -651,12 +698,18 @@ class ExportDB(ExportDB_ABC):
size INTEGER,
mtime REAL
); """,
"sql_detected_text_table": """ CREATE TABLE IF NOT EXISTS detected_text (
id INTEGER PRIMARY KEY,
uuid TEXT NOT NULL,
text_data JSON
); """,
"sql_files_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_files_filepath_normalized on files (filepath_normalized); """,
"sql_info_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_info_uuid on info (uuid); """,
"sql_exifdata_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_exifdata_filename on exifdata (filepath_normalized); """,
"sql_edited_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_edited_filename on edited (filepath_normalized);""",
"sql_converted_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_converted_filename on converted (filepath_normalized);""",
"sql_sidecar_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_sidecar_filename on sidecar (filepath_normalized);""",
"sql_detected_text_idx": """ CREATE UNIQUE INDEX IF NOT EXISTS idx_detected_text on detected_text (uuid);""",
}
try:
c = conn.cursor()
@@ -666,12 +719,13 @@ class ExportDB(ExportDB_ABC):
"INSERT INTO version(osxphotos, exportdb) VALUES (?, ?);",
(__version__, OSXPHOTOS_EXPORTDB_VERSION),
)
c.execute("INSERT INTO about(about) VALUES (?);", (OSXPHOTOS_ABOUT_STRING,))
conn.commit()
except Error as e:
logging.warning(e)
def __del__(self):
""" ensure the database connection is closed """
"""ensure the database connection is closed"""
try:
self._conn.close()
except:
@@ -696,35 +750,33 @@ class ExportDB(ExportDB_ABC):
class ExportDBInMemory(ExportDB):
""" In memory version of ExportDB
Copies the on-disk database into memory so it may be operated on without
modifying the on-disk verison
"""In memory version of ExportDB
Copies the on-disk database into memory so it may be operated on without
modifying the on-disk version
"""
def init(self, dbfile):
self._dbfile = dbfile
def __init__(self, dbfile):
self._dbfile = dbfile or f"./{OSXPHOTOS_EXPORT_DB}"
# _path is parent of the database
# all files referenced by get_/set_uuid_for_file will be converted to
# relative paths to this parent _path
# this allows the entire export tree to be moved to a new disk/location
# whilst preserving the UUID to filename mappping
self._path = pathlib.Path(dbfile).parent
self._conn = self._open_export_db(dbfile)
# whilst preserving the UUID to filename mapping
self._path = pathlib.Path(self._dbfile).parent
self._conn = self._open_export_db(self._dbfile)
self._insert_run_info()
def _open_export_db(self, dbfile):
""" open export database and return a db connection
returns: connection to the database
"""open export database and return a db connection
returns: connection to the database
"""
if not os.path.isfile(dbfile):
conn = self._get_db_connection()
if conn:
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
else:
if not conn:
raise Exception("Error getting connection to in-memory database")
self._create_db_tables(conn)
self.was_created = True
self.was_upgraded = ()
else:
try:
conn = sqlite3.connect(dbfile)
@@ -749,12 +801,11 @@ class ExportDBInMemory(ExportDB):
self.was_upgraded = (exportdb_ver, OSXPHOTOS_EXPORTDB_VERSION)
else:
self.was_upgraded = ()
self.version = OSXPHOTOS_EXPORTDB_VERSION
self.version = OSXPHOTOS_EXPORTDB_VERSION
return conn
def _get_db_connection(self):
""" return db connection to in memory database """
"""return db connection to in memory database"""
try:
conn = sqlite3.connect(":memory:")
except Error as e:

View File

@@ -7,7 +7,7 @@ import subprocess
import sys
from abc import ABC, abstractmethod
import CoreFoundation
import Foundation
from .imageconverter import ImageConverter
@@ -114,7 +114,7 @@ class FileUtilMacOS(FileUtilABC):
if dest.is_dir():
dest /= src.name
filemgr = CoreFoundation.NSFileManager.defaultManager()
filemgr = Foundation.NSFileManager.defaultManager()
error = filemgr.copyItemAtPath_toPath_error_(str(src), str(dest), None)
# error is a tuple of (bool, error_string)
# error[0] is True if copy succeeded

69
osxphotos/momentinfo.py Normal file
View File

@@ -0,0 +1,69 @@
"""MomentInfo class with details about photo moments."""
class MomentInfo:
"""Info about a photo moment"""
def __init__(self, db, moment_pk):
"""Initialize with a moment PK; returns None if PK not found."""
self._db = db
self._pk = moment_pk
self._moment = self._db._db_moment_pk.get(moment_pk)
if not self._moment:
raise ValueError(f"No moment with PK {moment_pk}")
@property
def pk(self):
"""Primary key of the moment."""
return self._pk
@property
def location(self):
"""Location of the moment."""
return (self._moment.get("latitude"), self._moment.get("longitude"))
@property
def title(self):
"""Title of the moment."""
return self._moment.get("title")
@property
def subtitle(self):
"""Subtitle of the moment."""
return self._moment.get("subtitle")
@property
def start_date(self):
"""Start date of the moment."""
return self._moment.get("startDate")
@property
def end_date(self):
"""Stop date of the moment."""
return self._moment.get("endDate")
@property
def date(self):
"""Date of the moment."""
return self._moment.get("representativeDate")
@property
def modification_date(self):
"""Modification date of the moment."""
return self._moment.get("modificationDate")
@property
def photos(self):
"""All photos in this moment"""
try:
return self._photos
except AttributeError:
photo_uuids = [
uuid
for uuid, photo in self._db._dbphotos.items()
if photo["momentID"] == self._pk
]
self._photos = self._db.photos_by_uuid(photo_uuids)
return self._photos

View File

@@ -141,7 +141,7 @@ class FaceInfo:
self.manual = face["manual"]
self.face_type = face["facetype"]
self.age_type = face["agetype"]
self.bald_type = face["baldtype"]
# self.bald_type = face["baldtype"]
self.eye_makeup_type = face["eyemakeuptype"]
self.eye_state = face["eyestate"]
self.facial_hair_type = face["facialhairtype"]
@@ -438,7 +438,7 @@ class FaceInfo:
"manual": self.manual,
"face_type": self.face_type,
"age_type": self.age_type,
"bald_type": self.bald_type,
# "bald_type": self.bald_type,
"eye_makeup_type": self.eye_makeup_type,
"eye_state": self.eye_state,
"facial_hair_type": self.facial_hair_type,

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
@@ -280,7 +280,7 @@ def _export_photo_uuid_applescript(
if not exported_files or not filename:
# nothing got exported
raise ExportError(f"Could not export photo {uuid}")
raise ExportError(f"Could not export photo {uuid} ({lineno(__file__)})")
# need to find actual filename as sometimes Photos renames JPG to jpeg on export
# may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)
@@ -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,14 @@ 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}"
# if original is missing, the filename won't have been incremented so
# need to check here to make sure there aren't duplicate preview files in
# the export directory
preview_name = (
preview_name
if overwrite or update
else pathlib.Path(increment_filename(preview_name))
)
if preview_path is not None:
results = self._export_photo(
preview_path,
@@ -969,6 +962,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
sidecars.append(
(
@@ -995,6 +989,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
sidecars.append(
(
@@ -1017,6 +1012,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
sidecars.append(
(
@@ -1087,6 +1083,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
)[0]
if old_data != current_data:
@@ -1110,6 +1107,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 +1128,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
),
)
export_db.set_stat_exif_for_file(
@@ -1155,6 +1154,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 +1175,7 @@ def export2(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
),
)
export_db.set_stat_exif_for_file(
@@ -1265,6 +1266,7 @@ def _export_photo_with_photos_export(
dest.name,
version=PHOTOS_VERSION_CURRENT,
overwrite=overwrite,
video=live_photo,
)
all_results.exported.extend(exported)
except Exception as e:
@@ -1312,6 +1314,7 @@ def _export_photo_with_photos_export(
dest.name,
version=PHOTOS_VERSION_ORIGINAL,
overwrite=overwrite,
video=live_photo,
)
all_results.exported.extend(exported)
except Exception as e:
@@ -1521,16 +1524,31 @@ def _export_photo(
edited_stat = fileutil.file_sig(src) if edited else (None, None, None)
if dest_exists and (update or overwrite):
# need to remove the destination first
fileutil.unlink(dest)
try:
fileutil.unlink(dest)
except Exception as e:
raise ExportError(
f"Error removing file {dest}: {e} (({lineno(__file__)})"
) from e
if export_as_hardlink:
fileutil.hardlink(src, dest)
try:
fileutil.hardlink(src, dest)
except Exception as e:
raise ExportError(
f"Error hardlinking {src} to {dest}: {e} ({lineno(__file__)})"
) from e
elif convert_to_jpeg:
# use convert_to_jpeg to export the file
fileutil.convert_to_jpeg(src, dest_str, compression_quality=jpeg_quality)
converted_stat = fileutil.file_sig(dest_str)
converted_to_jpeg_files.append(dest_str)
else:
fileutil.copy(src, dest_str)
try:
fileutil.copy(src, dest_str)
except Exception as e:
raise ExportError(
f"Error copying file {src} to {dest_str}: {e} ({lineno(__file__)})"
) from e
export_db.set_data(
filename=dest_str,
@@ -1580,6 +1598,7 @@ def _write_exif_data(
persons=True,
location=True,
replace_keywords=False,
strip=False,
):
"""write exif data to image file at filepath
@@ -1593,6 +1612,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 +1630,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 +1656,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 +1673,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 +1721,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 +1770,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 +1879,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 +1931,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 +1949,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 +1989,7 @@ def _exiftool_json_sidecar(
persons=persons,
location=location,
replace_keywords=replace_keywords,
strip=strip,
)
if not tag_groups:
@@ -1990,6 +2015,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 +2028,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 +2046,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 +2089,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,15 +14,20 @@ from datetime import timedelta, timezone
from typing import Optional
import yaml
from osxmetadata import OSXMetaData
from .._constants import (
_MOVIE_TYPE,
_PHOTO_TYPE,
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_ALBUM_TYPE_ALBUM,
_PHOTOS_4_ALBUM_TYPE_PROJECT,
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
_PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
_PHOTOS_5_PROJECT_ALBUM_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND,
_PHOTOS_5_SHARED_PHOTO_PATH,
_PHOTOS_5_VERSION,
@@ -30,12 +35,16 @@ from .._constants import (
BURST_KEY,
BURST_NOT_SELECTED,
BURST_SELECTED,
TEXT_DETECTION_CONFIDENCE_THRESHOLD,
)
from ..adjustmentsinfo import AdjustmentsInfo
from ..albuminfo import AlbumInfo, ImportInfo
from ..albuminfo import AlbumInfo, ImportInfo, ProjectInfo
from ..momentinfo import MomentInfo
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
@@ -490,6 +499,18 @@ class PhotoInfo:
self._faceinfo = []
return self._faceinfo
@property
def moment(self):
"""Moment photo belongs to"""
try:
return self._moment
except AttributeError:
try:
self._moment = MomentInfo(db=self._db, moment_pk=self._info["momentID"])
except ValueError:
self._moment = None
return self._moment
@property
def albums(self):
"""list of albums picture is contained in"""
@@ -553,6 +574,18 @@ class PhotoInfo:
)
return self._import_info
@property
def project_info(self):
"""list of AlbumInfo objects representing projects for the photo or None if no projects"""
try:
return self._project_info
except AttributeError:
project_uuids = self._get_album_uuids(project=True)
self._project_info = [
ProjectInfo(db=self._db, uuid=album) for album in project_uuids
]
return self._project_info
@property
def keywords(self):
"""list of keywords for picture"""
@@ -561,7 +594,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):
@@ -834,7 +872,7 @@ class PhotoInfo:
if self._db._db_version <= _PHOTOS_4_VERSION:
if self.live_photo and not self.ismissing:
live_model_id = self._info["live_model_id"]
if live_model_id == None:
if live_model_id is None:
logging.debug(f"missing live_model_id: {self._uuid}")
photopath = None
else:
@@ -855,28 +893,20 @@ class PhotoInfo:
# photos 4 has "isOnDisk" column we could check
# or could do the actual check with "isfile"
# TODO: should this be a warning or debug?
logging.debug(
f"MISSING PATH: live photo path for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
photopath = None
else:
# Photos 5
if self.live_photo and not self.ismissing:
filename = pathlib.Path(self.path)
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
photopath = str(photopath)
if not os.path.isfile(photopath):
# In testing, I've seen occasional missing movie for live photo
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
# TODO: should this be a warning or debug?
logging.debug(
f"MISSING PATH: live photo path for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
else:
elif self.live_photo and self.path and not self.ismissing:
filename = pathlib.Path(self.path)
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
photopath = str(photopath)
if not os.path.isfile(photopath):
# In testing, I've seen occasional missing movie for live photo
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
# TODO: should this be a warning or debug?
photopath = None
else:
photopath = None
return photopath
@@ -1046,15 +1076,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"""
@@ -1090,6 +1120,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
):
@@ -1106,6 +1156,53 @@ class PhotoInfo:
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
return template.render(template_str, options)
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Detects text in photo and returns lists of results as (detected text, confidence)
confidence_threshold: float between 0.0 and 1.0. If text detection confidence is below this threshold,
text will not be returned. Default is TEXT_DETECTION_CONFIDENCE_THRESHOLD
If photo is edited, uses the edited photo, otherwise the original; falls back to the preview image if neither edited or original is available
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
)
path = path or self.path_derivatives[0] if self.path_derivatives else None
if not path:
return []
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):
"""Returns longitude, in degrees"""
@@ -1116,34 +1213,48 @@ class PhotoInfo:
"""Returns latitude, in degrees"""
return self._info["latitude"]
def _get_album_uuids(self):
def _get_album_uuids(self, project=False):
"""Return list of album UUIDs this photo is found in
Filters out albums in the trash and any special album types
if project is True, returns special "My Project" albums (e.g. cards, calendars, slideshows)
Returns: list of album UUIDs
"""
if self._db._db_version <= _PHOTOS_4_VERSION:
version4 = True
album_kind = [_PHOTOS_4_ALBUM_KIND]
else:
version4 = False
album_kind = [_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND]
album_type = (
[_PHOTOS_4_ALBUM_TYPE_PROJECT, _PHOTOS_4_ALBUM_TYPE_SLIDESHOW]
if project
else [_PHOTOS_4_ALBUM_TYPE_ALBUM]
)
album_list = []
for album in self._info["albums"]:
detail = self._db._dbalbum_details[album]
if (
detail["kind"] in album_kind
and detail["albumType"] in album_type
and not detail["intrash"]
and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
# but should not be listed here; they can be distinguished by looking
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
):
album_list.append(album)
return album_list
# Photos 5+
album_kind = (
[_PHOTOS_5_PROJECT_ALBUM_KIND]
if project
else [_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND]
)
album_list = []
for album in self._info["albums"]:
detail = self._db._dbalbum_details[album]
if (
detail["kind"] in album_kind
and not detail["intrash"]
and (
not version4
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
# but should not be listed here; they can be distinguished by looking
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
or (version4 and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER)
)
):
if detail["kind"] in album_kind and not detail["intrash"]:
album_list.append(album)
return album_list

View File

@@ -6,8 +6,6 @@
"""
# NOTES:
# - This likely leaks memory like a sieve as I need to ensure all the
# Objective C objects are cleaned up.
# - There are several techniques used for handling PhotoKit's various
# asynchronous calls used in this code: event loop+notification, threading
# event, while loop. I've experimented with each to find the one that works.
@@ -200,16 +198,6 @@ class PHAssetResourceData:
self.data = b""
# class LivePhotoData:
# """ Simple class to hold the data passed to the handler for
# requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler:
# """
# def __init__(self):
# self.live_photo = None
# self.info = None
class PhotoKitNotificationDelegate(NSObject):
"""Handles notifications from NotificationCenter;
used with asynchronous PhotoKit requests to stop event loop when complete
@@ -487,6 +475,7 @@ class PhotoAsset:
version=PHOTOS_VERSION_CURRENT,
overwrite=False,
raw=False,
**kwargs,
):
"""Export image to path
@@ -496,6 +485,7 @@ class PhotoAsset:
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
overwrite: bool, if True, overwrites destination file if it already exists; default is False
raw: bool, if True, export RAW component of RAW+JPEG pair, default is False
**kwargs: used only to avoid issues with each asset type having slightly different export arguments
Returns:
List of path to exported image(s)
@@ -504,9 +494,6 @@ class PhotoAsset:
ValueError if dest is not a valid directory
"""
# if self.live:
# raise NotImplementedError("Live photos not implemented yet")
with objc.autorelease_pool():
filename = (
pathlib.Path(filename)
@@ -615,9 +602,7 @@ class PhotoAsset:
nonlocal requestdata
options = {}
# pylint: disable=no-member
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
options = {Quartz.kCGImageSourceShouldCache: Foundation.kCFBooleanFalse}
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
imgSrc, 0, options
@@ -701,9 +686,7 @@ class PhotoAsset:
nonlocal data
options = {}
# pylint: disable=no-member
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
options = {Quartz.kCGImageSourceShouldCache: Foundation.kCFBooleanFalse}
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
data.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
imgSrc, 0, options
@@ -789,7 +772,6 @@ class SlowMoVideoExporter(NSObject):
self.url = None
self.done = None
self.nc = None
# super(NSObject, self).dealloc()
class VideoAsset(PhotoAsset):
@@ -801,7 +783,12 @@ class VideoAsset(PhotoAsset):
# https://developer.apple.com/documentation/photokit/phimagemanager/1616981-requestexportsessionforvideo?language=objc
# above 10.15 only
def export(
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
self,
dest,
filename=None,
version=PHOTOS_VERSION_CURRENT,
overwrite=False,
**kwargs,
):
"""Export video to path
@@ -810,6 +797,7 @@ class VideoAsset(PhotoAsset):
filename: str, optional name of exported file; if not provided, defaults to asset's original filename
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
overwrite: bool, if True, overwrites destination file if it already exists; default is False
**kwargs: used only to avoid issues with each asset type having slightly different export arguments
Returns:
List of path to exported image(s)
@@ -1043,6 +1031,7 @@ class LivePhotoAsset(PhotoAsset):
overwrite=False,
photo=True,
video=True,
**kwargs,
):
"""Export image to path
@@ -1053,6 +1042,7 @@ class LivePhotoAsset(PhotoAsset):
overwrite: bool, if True, overwrites destination file if it already exists; default is False
photo: bool, if True, export photo component of live photo
video: bool, if True, export live video component of live photo
**kwargs: used only to avoid issues with each asset type having slightly different export arguments
Returns:
list of [path to exported image and/or video]
@@ -1104,39 +1094,6 @@ class LivePhotoAsset(PhotoAsset):
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
video_output_file = pathlib.Path(increment_filename(video_output_file))
# def handler(error):
# if error:
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
# resource_manager = Photos.PHAssetResourceManager.defaultManager()
# options = Photos.PHAssetResourceRequestOptions.alloc().init()
# options.setNetworkAccessAllowed_(True)
# exported = []
# Note: Tried writeDataForAssetResource_toFile_options_completionHandler_ which works
# but sets quarantine flag and for reasons I can't determine (maybe quarantine flag)
# causes pathlib.Path().is_file() to fail in tests
# if photo:
# photo_output_url = path_to_NSURL(photo_output_file)
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
# photo_resource, photo_output_url, options, handler
# )
# exported.append(str(photo_output_file))
# if video:
# video_output_url = path_to_NSURL(video_output_file)
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
# video_resource, video_output_url, options, handler
# )
# exported.append(str(video_output_file))
# def completion_handler(error):
# if error:
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
# would be nice to be able to usewriteDataForAssetResource_toFile_options_completionHandler_
# but it sets quarantine flags that cause issues so instead, request the data and write the files directly
exported = []
if photo:
data = self._request_resource_data(photo_resource)
@@ -1155,41 +1112,6 @@ class LivePhotoAsset(PhotoAsset):
request.dealloc()
return exported
# def request_image_data(self, version=PHOTOS_VERSION_CURRENT):
# # Returns an NSImage which isn't overly useful
# # https://developer.apple.com/documentation/photokit/phimagemanager/1616964-requestimageforasset?language=objc
# # requestImageForAsset:targetSize:contentMode:options:resultHandler:
# options = Photos.PHImageRequestOptions.alloc().init()
# options.setVersion_(version)
# options.setNetworkAccessAllowed_(True)
# options.setSynchronous_(True)
# options.setDeliveryMode_(
# Photos.PHImageRequestOptionsDeliveryModeHighQualityFormat
# )
# event = threading.Event()
# image_data = ImageData()
# def handler(result, info):
# nonlocal image_data
# if not info["PHImageResultIsDegradedKey"]:
# image_data.image_data = result
# image_data.info = info
# event.set()
# self._manager.requestImageForAsset_targetSize_contentMode_options_resultHandler_(
# self._phasset,
# Photos.PHImageManagerMaximumSize,
# Photos.PHImageContentModeDefault,
# options,
# handler,
# )
# event.wait()
# options.dealloc()
# return image_data
class PhotoLibrary:
"""Interface to PhotoKit PHImageManager and PHPhotoLibrary"""

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

@@ -146,7 +146,6 @@ def _process_faceinfo_4(photosdb):
# Photos 5 only
face["agetype"] = None
face["baldtype"] = None
face["eyemakeuptype"] = None
face["eyestate"] = None
face["facialhairtype"] = None
@@ -194,7 +193,7 @@ def _process_faceinfo_5(photosdb):
ZDETECTEDFACE.ZPERSON,
ZPERSON.ZFULLNAME,
ZDETECTEDFACE.ZAGETYPE,
ZDETECTEDFACE.ZBALDTYPE,
NULL, -- ZDETECTEDFACE.ZBALDTYPE (Removed in Monterey)
ZDETECTEDFACE.ZEYEMAKEUPTYPE,
ZDETECTEDFACE.ZEYESSTATE,
ZDETECTEDFACE.ZFACIALHAIRTYPE,
@@ -239,7 +238,7 @@ def _process_faceinfo_5(photosdb):
# 3 ZDETECTEDFACE.ZPERSON,
# 4 ZPERSON.ZFULLNAME,
# 5 ZDETECTEDFACE.ZAGETYPE,
# 6 ZDETECTEDFACE.ZBALDTYPE,
# 6 ZDETECTEDFACE.ZBALDTYPE, (Not available on Monterey)
# 7 ZDETECTEDFACE.ZEYEMAKEUPTYPE,
# 8 ZDETECTEDFACE.ZEYESSTATE,
# 9 ZDETECTEDFACE.ZFACIALHAIRTYPE,
@@ -284,7 +283,6 @@ def _process_faceinfo_5(photosdb):
face["person"] = person_pk
face["fullname"] = normalize_unicode(row[4])
face["agetype"] = row[5]
face["baldtype"] = row[6]
face["eyemakeuptype"] = row[7]
face["eyestate"] = row[8]
face["facialhairtype"] = row[9]

View File

@@ -12,12 +12,14 @@ import re
import sys
import tempfile
from collections import OrderedDict
from collections.abc import Iterable
from datetime import datetime, timedelta, timezone
from pprint import pformat
from typing import List
import bitmath
import photoscript
from rich import print
from .._constants import (
_DB_TABLE_NAMES,
@@ -26,11 +28,15 @@ from .._constants import (
_PHOTOS_3_VERSION,
_PHOTOS_4_ALBUM_KIND,
_PHOTOS_4_ROOT_FOLDER,
_PHOTOS_4_TOP_LEVEL_ALBUM,
_PHOTOS_4_TOP_LEVEL_ALBUMS,
_PHOTOS_4_ALBUM_TYPE_ALBUM,
_PHOTOS_4_ALBUM_TYPE_PROJECT,
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
_PHOTOS_4_VERSION,
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
_PHOTOS_5_PROJECT_ALBUM_KIND,
_PHOTOS_5_ROOT_FOLDER_KIND,
_PHOTOS_5_SHARED_ALBUM_KIND,
_TESTED_OS_VERSIONS,
@@ -40,7 +46,7 @@ from .._constants import (
TIME_DELTA,
)
from .._version import __version__
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo
from ..albuminfo import AlbumInfo, FolderInfo, ImportInfo, ProjectInfo
from ..datetime_utils import datetime_has_tz, datetime_naive_to_local
from ..fileutil import FileUtil
from ..personinfo import PersonInfo
@@ -250,6 +256,10 @@ class PhotosDB:
# Dict to hold information on volume names (Photos 5+)
self._db_filesystem_volumes = {}
# Dict to hold information on moments (Photos 5+)
# key is Z_PK of ZMOMENT table and values are the moment info
self._db_moment_pk = {}
if _debug():
logging.debug(f"dbfile = {dbfile}")
@@ -330,6 +340,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)"""
@@ -421,7 +433,7 @@ class PhotosDB:
for folder, detail in self._dbfolder_details.items()
if not detail["intrash"]
and not detail["isMagic"]
and detail["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
and detail["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
]
else:
folders = [
@@ -442,7 +454,7 @@ class PhotosDB:
for folder in self._dbfolder_details.values()
if not folder["intrash"]
and not folder["isMagic"]
and folder["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
and folder["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
]
else:
folder_names = [
@@ -521,6 +533,18 @@ class PhotosDB:
]
return self._import_info
@property
def project_info(self):
"""return list of AlbumInfo projects for each project in the database"""
try:
return self._project_info
except AttributeError:
self._project_info = [
ProjectInfo(db=self, uuid=album)
for album in self._get_album_uuids(project=True)
]
return self._project_info
@property
def db_version(self):
"""return the database version as stored in LiGlobals table"""
@@ -790,8 +814,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
@@ -840,11 +864,10 @@ class PhotosDB:
# build folder hierarchy
for album, details in self._dbalbum_details.items():
parent_folder = details["folderUuid"]
if details[
"albumSubclass"
] == _PHOTOS_4_ALBUM_KIND and parent_folder not in [
_PHOTOS_4_TOP_LEVEL_ALBUM
]:
if (
details["albumSubclass"] == _PHOTOS_4_ALBUM_KIND
and parent_folder not in _PHOTOS_4_TOP_LEVEL_ALBUMS
):
folder_hierarchy = self._build_album_folder_hierarchy_4(parent_folder)
self._dbalbum_folders[album] = folder_hierarchy
else:
@@ -1104,6 +1127,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]["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
@@ -1194,6 +1220,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:
@@ -1568,7 +1597,7 @@ class PhotosDB:
if parent_uuid is None:
return folders
if parent_uuid == _PHOTOS_4_TOP_LEVEL_ALBUM:
if parent_uuid in _PHOTOS_4_TOP_LEVEL_ALBUMS:
if not folders:
# this is a top-level folder with no sub-folders
folders = {uuid: None}
@@ -1921,7 +1950,9 @@ class PhotosDB:
{asset_table}.ZVISIBILITYSTATE,
{asset_table}.ZTRASHEDDATE,
{asset_table}.ZSAVEDASSETTYPE,
{asset_table}.ZADDEDDATE
{asset_table}.ZADDEDDATE,
{asset_table}.Z_PK,
{asset_table}.ZCLOUDOWNERHASHEDPERSONID
FROM {asset_table}
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
ORDER BY {asset_table}.ZUUID """
@@ -1970,6 +2001,8 @@ class PhotosDB:
# 39 ZGENERICASSET.ZTRASHEDDATE -- date item placed in the trash or null if not in trash
# 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]
@@ -2154,6 +2187,9 @@ class PhotosDB:
except (ValueError, TypeError):
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
info["import_session"] = None
@@ -2476,6 +2512,10 @@ class PhotosDB:
verbose("Processing comments and likes for shared photos.")
self._process_comments()
# process moments
verbose("Processing moments.")
self._process_moments()
# done processing, dump debug data if requested
verbose("Done processing details from Photos library.")
if _debug():
@@ -2521,6 +2561,109 @@ class PhotosDB:
logging.debug("Burst Photos (dbphotos_burst:")
logging.debug(pformat(self._dbphotos_burst))
def _process_moments(self):
"""Process data from ZMOMENT table"""
# _db_moment_pk is dict in form {pk: {moment info}} by ZMOMENT.Z_PK
if self._db_version <= _PHOTOS_4_VERSION:
raise NotImplementedError(
f"Moment info implemented for this database version"
)
else:
self._process_moment_5()
def _process_moment_5(self):
"""Process moment info for Photos 5 databases"""
self._db_moment_pk = {}
results = self.execute(
f"""
SELECT
Z_PK,
ZTIMEZONEOFFSET,
ZTRASHEDSTATE,
ZAPPROXIMATELATITUDE,
ZAPPROXIMATELONGITUDE,
ZENDDATE,
ZMODIFICATIONDATE,
ZREPRESENTATIVEDATE,
ZSTARTDATE,
ZSUBTITLE,
ZTITLE,
ZUUID
FROM ZMOMENT"""
)
# results
# 0 Z_PK,
# 1 ZTIMEZONEOFFSET,
# 2 ZTRASHEDSTATE,
# 3 ZAPPROXIMATELATITUDE,
# 4 ZAPPROXIMATELONGITUDE,
# 5 ZENDDATE,
# 6 ZMODIFICATIONDATE,
# 7 ZREPRESENTATIVEDATE,
# 8 ZSTARTDATE,
# 9 ZSUBTITLE,
# 10 ZTITLE,
# 11 ZUUID
for row in results:
moment_info = {}
moment_info["pk"] = row[0]
moment_info["timezoneOffset"] = row[1]
moment_info["trashedState"] = row[2]
moment_info["approximateLatitude"] = row[3]
moment_info["approximateLongitude"] = row[4]
moment_info["endDate"] = row[5]
moment_info["modificationDate"] = row[6]
moment_info["representativeDate"] = row[7]
moment_info["startDate"] = row[8]
moment_info["subtitle"] = row[9]
moment_info["title"] = row[10]
moment_info["uuid"] = row[11]
# if both lat/lon == -180, then it means location undefined
if (
moment_info["approximateLatitude"] == -180.0
and moment_info["approximateLongitude"] == -180.0
):
moment_info["latitude"] = None
moment_info["longitude"] = None
else:
moment_info["latitude"] = moment_info["approximateLatitude"]
moment_info["longitude"] = moment_info["approximateLongitude"]
# process date stamps
offset_seconds = moment_info["timezoneOffset"] or 0
delta = timedelta(seconds=offset_seconds)
tz = timezone(delta)
for date_name in [
"startDate",
"endDate",
"modificationDate",
"representativeDate",
]:
date_stamp = moment_info[date_name]
try:
moment_date = datetime.fromtimestamp(date_stamp + TIME_DELTA)
# save raw time stamp valu
moment_info[date_name + "_timestamp"] = moment_info[date_name]
moment_info[date_name] = moment_date.astimezone(tz=tz)
except ValueError:
# sometimes imageDate is invalid so use 1 Jan 1970 in UTC as image date
moment_date = datetime(1970, 1, 1)
tz = timezone(timedelta(0))
moment_info[date_name + "_timestamp"] = date_stamp
moment_info[date_name] = moment_date.astimezone(tz=tz)
# process title/subtitle
moment_info["title"] = moment_info["title"] or ""
moment_info["subtitle"] = moment_info["subtitle"] or ""
self._db_moment_pk[moment_info["pk"]] = moment_info
def _build_album_folder_hierarchy_5(self, uuid, folders=None):
"""recursively build folder/album hierarchy
uuid: uuid of the album/folder being processed
@@ -2697,7 +2840,7 @@ class PhotosDB:
hierarchy = _recurse_folder_hierarchy(folders)
return hierarchy
def _get_album_uuids(self, shared=False, import_session=False):
def _get_album_uuids(self, shared=False, import_session=False, project=False):
"""Return list of album UUIDs found in photos database
Filters out albums in the trash and any special album types
@@ -2705,20 +2848,21 @@ class PhotosDB:
Args:
shared: boolean; if True, returns shared albums, else normal albums
import_session: boolean, if True, returns import session albums, else normal or shared albums
project: boolean, if True, returns albums that are part of My Projects
Note: flags (shared, import_session) are mutually exclusive
Raises:
ValueError: raised if mutually exclusive flags passed
Returns: list of album UUIDs
"""
if shared and import_session:
if sum(bool(x) for x in [shared, import_session, project]) > 1:
raise ValueError(
"flags are mutually exclusive: pass zero or one of shared, import_session"
"flags are mutually exclusive: pass zero or one of shared, import_session, projects"
)
if self._db_version <= _PHOTOS_4_VERSION:
version4 = True
if shared:
logging.warning(
f"Shared albums not implemented for Photos library version {self._db_version}"
@@ -2729,16 +2873,44 @@ class PhotosDB:
f"Import sessions not implemented for Photos library version {self._db_version}"
)
return [] # not implemented for _PHOTOS_4_VERSION
else:
elif project:
album_type = [
_PHOTOS_4_ALBUM_TYPE_PROJECT,
_PHOTOS_4_ALBUM_TYPE_SLIDESHOW,
]
album_kind = _PHOTOS_4_ALBUM_KIND
else:
version4 = False
if shared:
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND
elif import_session:
album_kind = _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND
else:
album_kind = _PHOTOS_5_ALBUM_KIND
album_type = [_PHOTOS_4_ALBUM_TYPE_ALBUM]
album_kind = _PHOTOS_4_ALBUM_KIND
album_list = []
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
for album, detail in self._dbalbum_details.items():
if (
detail["kind"] == album_kind
and detail["albumType"] in album_type
and not detail["intrash"]
and (
(shared and detail["cloudownerhashedpersonid"] is not None)
or (not shared and detail["cloudownerhashedpersonid"] is None)
)
and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER
# in Photos <= 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
# but should not be listed here; they can be distinguished by looking
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
):
album_list.append(album)
return album_list
# Photos version 5+
if shared:
album_kind = _PHOTOS_5_SHARED_ALBUM_KIND
elif import_session:
album_kind = _PHOTOS_5_IMPORT_SESSION_ALBUM_KIND
elif project:
album_kind = _PHOTOS_5_PROJECT_ALBUM_KIND
else:
album_kind = _PHOTOS_5_ALBUM_KIND
album_list = []
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
@@ -2750,13 +2922,6 @@ class PhotosDB:
(shared and detail["cloudownerhashedpersonid"] is not None)
or (not shared and detail["cloudownerhashedpersonid"] is None)
)
and (
not version4
# in Photos 4, special albums like "printAlbum" have kind _PHOTOS_4_ALBUM_KIND
# but should not be listed here; they can be distinguished by looking
# for folderUuid of _PHOTOS_4_ROOT_FOLDER as opposed to _PHOTOS_4_TOP_LEVEL_ALBUM
or (version4 and detail["folderUuid"] != _PHOTOS_4_ROOT_FOLDER)
)
):
album_list.append(album)
return album_list
@@ -3281,9 +3446,9 @@ class PhotosDB:
if options.regex:
flags = re.IGNORECASE if options.ignore_case else 0
render_options = RenderOptions(none_str="")
photo_list = []
for regex, template in options.regex:
regex = re.compile(regex, flags)
photo_list = []
for p in photos:
rendered, _ = p.render_template(template, render_options)
for value in rendered:
@@ -3343,12 +3508,45 @@ class PhotosDB:
# selection only works if photos selected in main media browser
photos = []
if options.exif:
matching_photos = []
for p in photos:
if not p.exiftool:
continue
exifdata = p.exiftool.asdict(normalized=True)
exifdata.update(p.exiftool.asdict(tag_groups=False, normalized=True))
for exiftag, exifvalue in options.exif:
if options.ignore_case:
exifvalue = exifvalue.lower()
exifdata_value = exifdata.get(exiftag.lower(), "")
if isinstance(exifdata_value, str):
exifdata_value = exifdata_value.lower()
elif isinstance(exifdata_value, Iterable):
exifdata_value = [v.lower() for v in exifdata_value]
else:
exifdata_value = str(exifdata_value)
if exifvalue in exifdata_value:
matching_photos.append(p)
else:
exifdata_value = exifdata.get(exiftag.lower(), "")
if not isinstance(exifdata_value, (str, Iterable)):
exifdata_value = str(exifdata_value)
if exifvalue in exifdata_value:
matching_photos.append(p)
photos = matching_photos
if options.function:
for function in options.function:
photos = function[0](photos)
return photos
def execute(self, sql):
"""Execute sql statement and return cursor"""
self._db_connection, _ = self.get_db_connection()
return self._db_connection.cursor().execute(sql)
def _duplicate_signature(self, uuid):
"""Compute a signature for finding possible duplicates"""
return (
@@ -3376,6 +3574,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

@@ -4,7 +4,11 @@ import logging
import plistlib
from .._constants import (
_PHOTOS_2_VERSION,
_PHOTOS_3_VERSION,
_PHOTOS_4_VERSION,
_PHOTOS_5_MODEL_VERSION,
_PHOTOS_5_VERSION,
_PHOTOS_6_MODEL_VERSION,
_PHOTOS_7_MODEL_VERSION,
_TESTED_DB_VERSIONS,
@@ -83,3 +87,32 @@ def get_db_model_version(db_file):
logging.warning(f"Unknown model version: {model_ver}")
# cross our fingers and try latest version
return 7
class UnknownLibraryVersion(Exception):
pass
def get_photos_library_version(library_path):
"""Return int indicating which Photos version a library was created with """
library_path = pathlib.Path(library_path)
db_ver = get_db_version(str(library_path / "database" / "photos.db"))
db_ver = int(db_ver)
if db_ver == int(_PHOTOS_2_VERSION):
return 2
if db_ver == int(_PHOTOS_3_VERSION):
return 3
if db_ver == int(_PHOTOS_4_VERSION):
return 4
if db_ver != int(_PHOTOS_5_VERSION):
raise UnknownLibraryVersion(f"db_ver = {db_ver}")
model_ver = get_model_version(str(library_path / "database" / "Photos.sqlite"))
model_ver = int(model_ver)
if _PHOTOS_5_MODEL_VERSION[0] <= model_ver <= _PHOTOS_5_MODEL_VERSION[1]:
return 5
if _PHOTOS_6_MODEL_VERSION[0] <= model_ver <= _PHOTOS_6_MODEL_VERSION[1]:
return 6
if _PHOTOS_7_MODEL_VERSION[0] <= model_ver <= _PHOTOS_7_MODEL_VERSION[1]:
return 7
raise UnknownLibraryVersion(f"db_ver = {db_ver}, model_ver = {model_ver}")

View File

@@ -1,4 +1,4 @@
The templating system converts one or template statements, written in osxphotos templating language, to one or more rendered values using information from the photo being processed.
The templating system converts one or template statements, written in osxphotos metadata templating language, to one or more rendered values using information from the photo being processed.
In its simplest form, a template statement has the form: `"{template_field}"`, for example `"{title}"` which would resolve to the title of the photo.

View File

@@ -1,7 +1,9 @@
""" Custom template system for osxphotos, implements osxphotos template language (OTL) """
""" Custom template system for osxphotos, implements metadata template language (MTL) """
import datetime
import json
import locale
import logging
import os
import pathlib
import shlex
@@ -11,11 +13,13 @@ from typing import Optional
from textx import TextXSyntaxError, metamodel_from_file
from ._constants import _UNKNOWN_PERSON
from ._constants import _UNKNOWN_PERSON, TEXT_DETECTION_CONFIDENCE_THRESHOLD
from ._version import __version__
from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifToolCaching
from .export_db import ExportDB_ABC, ExportDBInMemory
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .text_detection import detect_text
from .utils import expand_and_validate_filepath, load_function
# TODO: a lot of values are passed from function to function like path_sep--make these all class properties
@@ -23,7 +27,7 @@ from .utils import expand_and_validate_filepath, load_function
# ensure locale set to user's locale
locale.setlocale(locale.LC_ALL, "")
OTL_GRAMMAR_MODEL = str(pathlib.Path(__file__).parent / "phototemplate.tx")
MTL_GRAMMAR_MODEL = str(pathlib.Path(__file__).parent / "phototemplate.tx")
"""TextX metamodel for osxphotos template language """
@@ -127,6 +131,29 @@ TEMPLATE_SUBSTITUTIONS = {
"{exif.camera_model}": "Camera model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s'",
"{exif.lens_model}": "Lens model from original photo's EXIF information as imported by Photos, e.g. 'iPhone 6s back camera 4.15mm f/2.2'",
"{uuid}": "Photo's internal universally unique identifier (UUID) for the photo, a 36-character string unique to the photo, e.g. '128FB4C6-0B16-4E7D-9108-FB2E90DA1546'",
"{id}": "A unique number for the photo based on its primary key in the Photos database. "
+ "A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. "
+ "May be formatted using a python string format code. "
+ "For example, to format as a 5-digit integer and pad with zeros, use '{id:05d}' which results in "
+ "00001, 00002, 00003...etc. ",
"{album_seq}": "An integer, starting at 0, indicating the photo's index (sequence) in the containing album. "
+ "Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. "
+ 'For example \'--directory "{folder_album}" --filename "{album_seq}_{original_name}"\'. '
+ "To start counting at a value other than 0, append append a period and the starting value to the field name. "
+ "For example, to start counting at 1 instead of 0: '{album_seq.1}'. "
+ "May be formatted using a python string format code. "
+ "For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in "
+ "00000, 00001, 00002...etc. "
+ "This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.",
"{folder_album_seq}": "An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. "
+ "Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. "
+ 'For example \'--directory "{folder_album}" --filename "{folder_album_seq}_{original_name}"\'. '
+ "To start counting at a value other than 0, append append a period and the starting value to the field name. "
+ "For example, to start counting at 1 instead of 0: '{folder_album_seq.1}' "
+ "May be formatted using a python string format code. "
+ "For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in "
+ "00000, 00001, 00002...etc. "
+ "This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'.",
"{comma}": "A comma: ','",
"{semicolon}": "A semicolon: ';'",
"{questionmark}": "A question mark: '?'",
@@ -154,6 +181,9 @@ TEMPLATE_SUBSTITUTIONS_PATHLIB = {
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
"{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",
"{project}": "Project(s) photo is contained in (such as greeting cards, calendars, slideshows)",
"{album_project}": "Album(s) and project(s) photo is contained in; treats projects as regular albums",
"{folder_album_project}": "Folder path + album (includes projects as albums) photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder",
"{keyword}": "Keyword(s) assigned to photo",
"{person}": "Person(s) / face(s) in a photo",
"{label}": "Image categorization label associated with a photo (Photos 5+ only). "
@@ -174,7 +204,15 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
+ "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}'; "
+ f"The default confidence threshold is {TEXT_DETECTION_CONFIDENCE_THRESHOLD}. "
+ "'{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. "
@@ -253,6 +291,7 @@ class RenderOptions:
dest_path: set to the destination path of the photo (for use by {function} template), only valid with --filename
filepath: set to value for filepath of the exported photo if you want to evaluate {filepath} template
quote: quote path templates for execution in the shell
exportdb: ExportDB object
"""
none_str: str = "_"
@@ -267,6 +306,7 @@ class RenderOptions:
dest_path: Optional[str] = None
filepath: Optional[str] = None
quote: bool = False
exportdb: Optional[ExportDB_ABC] = None
class PhotoTemplateParser:
@@ -287,12 +327,17 @@ class PhotoTemplateParser:
if hasattr(self, "metamodel"):
return
self.metamodel = metamodel_from_file(OTL_GRAMMAR_MODEL, skipws=False)
self.metamodel = metamodel_from_file(MTL_GRAMMAR_MODEL, skipws=False)
def parse(self, template_statement):
"""Parse a template_statement string"""
return self.metamodel.model_from_str(template_statement)
def fields(self, template_statement):
"""Return list of fields found in a template statement; does not verify that fields are valid"""
model = self.parse(template_statement)
return [ts.template.field for ts in model.template_strings if ts.template]
class PhotoTemplate:
"""PhotoTemplate class to render a template string from a PhotoInfo object"""
@@ -317,6 +362,7 @@ class PhotoTemplate:
# initialize render options
# this will be done in render() but for testing, some of the lookup functions are called directly
options = RenderOptions()
self.options = options
self.path_sep = options.path_sep
self.inplace_sep = options.inplace_sep
self.edited_version = options.edited_version
@@ -328,6 +374,8 @@ class PhotoTemplate:
self.export_dir = options.export_dir
self.filepath = options.filepath
self.quote = options.quote
self.dest_path = options.dest_path
self.exportdb = options.exportdb or ExportDBInMemory(None)
def render(
self,
@@ -347,6 +395,7 @@ class PhotoTemplate:
if type(template) is not str:
raise TypeError(f"template must be type str, not {type(template)}")
self.options = options
self.path_sep = options.path_sep
self.inplace_sep = options.inplace_sep
self.edited_version = options.edited_version
@@ -359,7 +408,8 @@ class PhotoTemplate:
self.dest_path = options.dest_path
self.filepath = options.filepath
self.quote = options.quote
self.options = options
self.dest_path = options.dest_path
self.exportdb = options.exportdb or self.exportdb
try:
model = self.parser.parse(template)
@@ -487,10 +537,14 @@ class PhotoTemplate:
conditional_value = []
vals = []
if field in SINGLE_VALUE_SUBSTITUTIONS:
if (
field in SINGLE_VALUE_SUBSTITUTIONS
or field.split(".")[0] in SINGLE_VALUE_SUBSTITUTIONS
):
vals = self.get_template_value(
field,
default=default,
subfield=subfield,
# delim=delim or self.inplace_sep,
# path_sep=path_sep,
)
@@ -512,7 +566,7 @@ class PhotoTemplate:
)
elif field in MULTI_VALUE_SUBSTITUTIONS or field.startswith("photo"):
vals = self.get_template_value_multi(
field, path_sep=path_sep, default=default
field, subfield, path_sep=path_sep, default=default
)
elif field.split(".")[0] in PATHLIB_SUBSTITUTIONS:
vals = self.get_template_value_pathlib(field)
@@ -524,7 +578,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)
@@ -565,12 +619,8 @@ class PhotoTemplate:
f"comparison operators may only be used with a single value: {vals} {conditional_value}"
)
try:
match = (
True
if test_function(
float(vals[0]), float(conditional_value[0])
)
else False
match = bool(
test_function(float(vals[0]), float(conditional_value[0]))
)
if (match and not negation) or (negation and not match):
return ["True"]
@@ -645,6 +695,7 @@ class PhotoTemplate:
self,
field,
default,
subfield=None,
# bool_val=None,
# delim=None,
# path_sep=None,
@@ -657,6 +708,7 @@ class PhotoTemplate:
bool_val: True value if expression is boolean
delim: delimiter for expand in place
path_sep: path separator for fields that are path-like
subfield: subfield (value after : in field)
Returns:
The matching template value (which may be None).
@@ -668,9 +720,6 @@ class PhotoTemplate:
if self.photo.uuid is None:
return []
if field not in FIELD_NAMES:
raise ValueError(f"SyntaxError: Unknown field: {field}")
# initialize today with current date/time if needed
if self.today is None:
self.today = datetime.datetime.now()
@@ -923,6 +972,26 @@ class PhotoTemplate:
value = self.photo.exif_info.lens_model if self.photo.exif_info else None
elif field == "uuid":
value = self.photo.uuid
elif field == "id":
value = format_str_value(self.photo._info["pk"], subfield)
elif field.startswith("album_seq") or field.startswith("folder_album_seq"):
dest_path = self.dest_path
if not dest_path:
value = None
else:
if field.startswith("album_seq"):
album = pathlib.Path(dest_path).name
album_info = _get_album_by_name(self.photo, album)
else:
album_info = _get_album_by_path(self.photo, dest_path)
value = album_info.photo_index(self.photo) if album_info else None
if value is not None:
try:
start_id = field.split(".", 1)
value = int(value) + int(start_id[1])
except IndexError:
pass
value = format_str_value(value, subfield)
elif field in PUNCTUATION:
value = PUNCTUATION[field]
elif field == "osxphotos_version":
@@ -938,6 +1007,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):
@@ -1023,11 +1095,12 @@ class PhotoTemplate:
value = []
return value
def get_template_value_multi(self, field, path_sep, default):
def get_template_value_multi(self, field, subfield, path_sep, default):
"""lookup value for template field (multi-value template substitutions)
Args:
field: template field to find value for.
subfield: the template subfield value
path_sep: path separator to use for folder_album field
default: value of default field
@@ -1046,6 +1119,11 @@ class PhotoTemplate:
values = []
if field == "album":
values = self.photo.burst_albums if self.photo.burst else self.photo.albums
elif field == "project":
values = [p.title for p in self.photo.project_info]
elif field == "album_project":
values = self.photo.burst_albums if self.photo.burst else self.photo.albums
values += [p.title for p in self.photo.project_info]
elif field == "keyword":
values = self.photo.keywords
elif field == "person":
@@ -1056,13 +1134,15 @@ class PhotoTemplate:
values = self.photo.labels
elif field == "label_normalized":
values = self.photo.labels_normalized
elif field == "folder_album":
elif field in ["folder_album", "folder_album_project"]:
values = []
# photos must be in an album to be in a folder
if self.photo.burst:
album_info = self.photo.burst_album_info
else:
album_info = self.photo.album_info
if field == "folder_album_project":
album_info += self.photo.project_info
for album in album_info:
if album.folder_names:
# album in folder
@@ -1076,12 +1156,10 @@ class PhotoTemplate:
folder = path_sep.join(album.folder_names)
folder += path_sep + album.title
values.append(folder)
elif self.dirname:
values.append(sanitize_dirname(album.title))
else:
# album not in folder
if self.dirname:
values.append(sanitize_dirname(album.title))
else:
values.append(album.title)
values.append(album.title)
elif field == "comment":
values = [
f"{comment.user}: {comment.text}" for comment in self.photo.comments
@@ -1098,6 +1176,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(".")
@@ -1123,14 +1203,16 @@ class PhotoTemplate:
elif isinstance(obj, (str, int, float)):
values = [str(obj)]
else:
values = [val for val in obj]
values = list(obj)
elif field == "detected_text":
values = _get_detected_text(self.photo, self.exportdb, confidence=subfield)
else:
raise ValueError(f"Unhandled template value: {field}")
# sanitize directory names if needed, folder_album handled differently above
if self.filename:
values = [sanitize_pathpart(value) for value in values]
elif self.dirname and field != "folder_album":
elif self.dirname and field not in ["folder_album", "folder_album_project"]:
# skip folder_album because it would have been handled above
values = [sanitize_dirname(value) for value in values]
@@ -1336,3 +1418,51 @@ def _get_pathlib_value(field, value, quote):
return val_str
except AttributeError:
raise ValueError("Illegal value for path template: {attribute}")
def format_str_value(value, format_str):
"""Format value based on format code in field in format id:02d"""
if not format_str:
return str(value)
format_str = "{0:" + f"{format_str}" + "}"
return format_str.format(value)
def _get_album_by_name(photo, album):
"""Finds first album named album that photo is in and returns the AlbumInfo object, otherwise returns None"""
for album_info in photo.album_info:
if album_info.title == album:
return album_info
return None
def _get_album_by_path(photo, folder_album_path):
"""finds the first album whose folder_album path matches and folder_album_path and returns the AlbumInfo object, otherwise, returns None"""
for album_info in photo.album_info:
# following code is how {folder_album} builds the folder path
folder = "/".join(sanitize_dirname(f) for f in album_info.folder_names)
folder += "/" + sanitize_dirname(album_info.title)
if folder_album_path.endswith(folder):
return album_info
return None
def _get_detected_text(photo, exportdb, confidence=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Returns the detected text for a photo
{detected_text} uses this instead of PhotoInfo.detected_text() to cache the text for all confidence values
"""
if not photo.isphoto:
return []
confidence = (
float(confidence)
if confidence is not None
else TEXT_DETECTION_CONFIDENCE_THRESHOLD
)
# _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

@@ -1,4 +1,4 @@
// OSXPhotos Template Language (OTL)
// OSXPhotos Metadata Template Language (MTL)
// a TemplateString has format:
// pre{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}post
// a TemplateStatement may contain zero or more TemplateStrings

131
osxphotos/pyrepl.py Normal file
View File

@@ -0,0 +1,131 @@
""" Custom Python REPL based on ptpython that allows quitting with custom keywords instead of `quit()` """
""" This file is distributed under the same license as the ptpython package:
Copyright (c) 2015, Jonathan Slenders (ptpython), (c) 2021 Rhet Turnbull (this file)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of the {organization} nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import sys
from typing import Callable, List, Optional
from ptpython.repl import (
ContextManager,
DummyContext,
PythonRepl,
builtins,
patch_stdout_context,
)
class PyReplQuitter(PythonRepl):
"""Custom pypython repl that allows quitting REPL with custom commands"""
def __init__(self, *args, quit_words: Optional[List[str]] = None, **kwargs):
super().__init__(*args, **kwargs)
self.quit_words = quit_words or ["quit", "q"]
def eval(self, line: str) -> object:
if line.strip() in self.quit_words:
sys.exit(0)
return super().eval(line)
def embed_repl(
globals=None,
locals=None,
configure: Optional[Callable[[PythonRepl], None]] = None,
vi_mode: bool = False,
history_filename: Optional[str] = None,
title: Optional[str] = None,
startup_paths=None,
patch_stdout: bool = False,
return_asyncio_coroutine: bool = False,
quit_words: Optional[List[str]] = None,
) -> None:
"""
Call this to embed Python shell at the current point in your program.
It's similar to `IPython.embed` and `bpython.embed`. ::
from prompt_toolkit.contrib.repl import embed
embed(globals(), locals())
:param vi_mode: Boolean. Use Vi instead of Emacs key bindings.
:param configure: Callable that will be called with the `PythonRepl` as a first
argument, to trigger configuration.
:param title: Title to be displayed in the terminal titlebar. (None or string.)
:param patch_stdout: When true, patch `sys.stdout` so that background
threads that are printing will print nicely above the prompt.
"""
# Default globals/locals
if globals is None:
globals = {
"__name__": "__main__",
"__package__": None,
"__doc__": None,
"__builtins__": builtins,
}
locals = locals or globals
def get_globals():
return globals
def get_locals():
return locals
# Create REPL.
repl = PyReplQuitter(
get_globals=get_globals,
get_locals=get_locals,
vi_mode=vi_mode,
history_filename=history_filename,
startup_paths=startup_paths,
quit_words=quit_words,
)
if title:
repl.terminal_title = title
if configure:
configure(repl)
# Start repl.
patch_context: ContextManager = (
patch_stdout_context() if patch_stdout else DummyContext()
)
if return_asyncio_coroutine:
async def coroutine():
with patch_context:
await repl.run_async()
return coroutine()
else:
with patch_context:
repl.run()

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

@@ -84,6 +84,7 @@ class QueryOptions:
no_location: Optional[bool] = None
function: Optional[List[Tuple[callable, str]]] = None
selected: Optional[bool] = None
exif: Optional[Iterable[Tuple[str, str]]] = None
def asdict(self):
return asdict(self)

View File

@@ -0,0 +1,89 @@
""" Use Apple's Vision Framework via PyObjC to perform text detection on images (macOS 10.15+ only) """
import logging
from typing import List, Optional
import objc
import Quartz
from Cocoa import NSURL
from Foundation import NSDictionary
# needed to capture system-level stderr
from wurlitzer import pipes
from .utils import _get_os_version
ver, major, minor = _get_os_version()
if ver == "10" and int(major) < 15:
vision = False
else:
import Vision
vision = True
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 []
with objc.autorelease_pool():
input_url = NSURL.fileURLWithPath_(img_path)
with pipes() as (out, err):
# capture stdout and stderr from system calls
# otherwise, Quartz.CIImage.imageWithContentsOfURL_
# prints to stderr something like:
# 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
# 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
vision_options = NSDictionary.dictionaryWithDictionary_({})
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 = (
Vision.VNRecognizeTextRequest.alloc().initWithCompletionHandler_(handler)
)
error = vision_handler.performRequests_error_([vision_request], None)
vision_request.dealloc()
vision_handler.dealloc()
for result in results:
result[0] = str(result[0])
return results
def make_request_handler(results):
"""results: list to store results"""
if not isinstance(results, list):
raise ValueError("results must be a list")
def handler(request, error):
if error:
print(f"Error! {error}")
else:
observations = request.results()
for text_observation in observations:
recognized_text = text_observation.topCandidates_(1)[0]
results.append([recognized_text.string(), recognized_text.confidence()])
return handler

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

@@ -528,35 +528,38 @@ def _get_uti_from_mdls(extension):
# mdls -name kMDItemContentType foo.3fr
# kMDItemContentType = "com.hasselblad.3fr-raw-image"
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
output = subprocess.check_output(
[
"/usr/bin/mdls",
"-name",
"kMDItemContentType",
temp.name,
]
).splitlines()
output = output[0].decode("UTF-8") if output else None
if not output:
return None
try:
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
output = subprocess.check_output(
[
"/usr/bin/mdls",
"-name",
"kMDItemContentType",
temp.name,
]
).splitlines()
output = output[0].decode("UTF-8") if output else None
if not output:
return None
match = re.match(r'kMDItemContentType\s+\=\s+"(.*)"', output)
if match:
return match.group(1)
match = re.match(r'kMDItemContentType\s+\=\s+"(.*)"', output)
if match:
return match.group(1)
return None
except Exception:
return None
def _get_uti_from_ext_dict(ext):
try:
return EXT_UTI_DICT[ext]
return EXT_UTI_DICT[ext.lower()]
except KeyError:
return None
def _get_ext_from_uti_dict(uti):
try:
return UTI_EXT_DICT[uti]
return UTI_EXT_DICT[uti.lower()]
except KeyError:
return None

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,22 +1,26 @@
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
Click==8.0.1
PyYAML==5.4.1
Mako==1.1.4
Click>=8.0.1,<9.0
Mako>=1.1.4,<1.2.0
PyYAML>=5.4.1,<6.0.0
bitmath>=1.3.3.1,<1.4.0.0
bpylist2==3.0.2
pathvalidate==2.4.1
dataclasses==0.7;python_version<'3.7'
wurlitzer==2.1.0
photoscript==0.1.3
toml==0.10.2
osxmetadata==0.99.25
textx==2.3.0
rich==10.2.2
bitmath==1.3.3.1
more-itertools==8.8.0
more-itertools>=8.8.0,<9.0.0
objexplore>=1.5.5,<1.6.0
osxmetadata>=0.99.34,<1.0.0
pathvalidate>=2.4.1,<2.5.0
photoscript>=0.1.4,<0.2.0
ptpython>=3.0.20,<3.1.0
pyobjc-core>=7.3,<9.0
pyobjc-framework-AVFoundation>=7.3,<9.0
pyobjc-framework-AppleScriptKit>=7.3,<9.0
pyobjc-framework-AppleScriptObjC>=7.3,<9.0
pyobjc-framework-Cocoa>=7.3,<9.0
pyobjc-framework-CoreServices>=7.2,<9.0
pyobjc-framework-Metal>=7.3,<9.0
pyobjc-framework-Photos>=7.3,<9.0
pyobjc-framework-Quartz>=7.3,<9.0
pyobjc-framework-Vision>=7.3,<9.0
rich>=10.6.0,<=11.0.0
textx>=2.3.0,<2.4.0
toml>=0.10.2,<0.11.0
wurlitzer>=2.1.0,<2.2.0

View File

@@ -70,31 +70,36 @@ setup(
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Software Development :: Libraries :: Python Modules",
],
install_requires=[
"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",
"Click==8.0.1",
"PyYAML==5.4.1",
"Mako==1.1.4",
"Click>=8.0.1,<9.0",
"Mako>=1.1.4,<1.2.0",
"PyYAML>=5.4.1,<5.5.0",
"bitmath>=1.3.3.1,<1.4.0.0",
"bpylist2==3.0.2",
"pathvalidate==2.4.1",
"dataclasses==0.7;python_version<'3.7'",
"wurlitzer==2.1.0",
"photoscript==0.1.3",
"toml==0.10.2",
"osxmetadata==0.99.25",
"textx==2.3.0",
"rich==10.2.2",
"bitmath==1.3.3.1",
"more-itertools==8.8.0",
"more-itertools>=8.8.0,<9.0.0",
"objexplore>=1.5.5,<1.6.0",
"osxmetadata>=0.99.34,<1.0.0",
"pathvalidate>=2.4.1,<3.0.0",
"photoscript>=0.1.4,<0.2.0",
"ptpython>=3.0.20,<4.0.0",
"pyobjc-core>=7.3,<9.0",
"pyobjc-framework-AVFoundation>=7.3,<9.0",
"pyobjc-framework-AppleScriptKit>=7.3,<9.0",
"pyobjc-framework-AppleScriptObjC>=7.3,<9.0",
"pyobjc-framework-Cocoa>=7.3,<9.0",
"pyobjc-framework-CoreServices>=7.2,<9.0",
"pyobjc-framework-Metal>=7.3,<9.0",
"pyobjc-framework-Photos>=7.3,<9.0",
"pyobjc-framework-Quartz>=7.3,<9.0",
"pyobjc-framework-Vision>=7.3,<9.0",
"rich>=10.6.0,<=11.0.0",
"textx>=2.3.0,<3.0.0",
"toml>=0.10.2,<0.11.0",
"wurlitzer>=2.1.0,<3.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

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