Compare commits
232 Commits
v0.42.70
...
multiproce
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bdd52df25 | ||
|
|
3cde0b79c9 | ||
|
|
e2bd262f75 | ||
|
|
db26532bab | ||
|
|
7a73b9168d | ||
|
|
a43bfc5a33 | ||
|
|
1d6bc4e09e | ||
|
|
3e14b718ef | ||
|
|
1ae6270561 | ||
|
|
55a601c07e | ||
|
|
7d67b81879 | ||
|
|
cd02144ac3 | ||
|
|
9b247acd1c | ||
|
|
942126ea3d | ||
|
|
2b9ea11701 | ||
|
|
b3d3e14ffe | ||
|
|
62ae5db9fd | ||
|
|
77a49a09a1 | ||
|
|
06c5bbfcfd | ||
|
|
f3063d35be | ||
|
|
e32090bf39 | ||
|
|
79dcfb38a8 | ||
|
|
7ab500740b | ||
|
|
911bd30d28 | ||
|
|
282857eae0 | ||
|
|
d8c2f99c06 | ||
|
|
16d3f74366 | ||
|
|
5fc28139ea | ||
|
|
b7b6876688 | ||
|
|
235dea329c | ||
|
|
5afdf6fc20 | ||
|
|
385059e973 | ||
|
|
62aed02070 | ||
|
|
6843b8661d | ||
|
|
9da747ea9d | ||
|
|
22964afc69 | ||
|
|
3bc53fd92b | ||
|
|
bd31120569 | ||
|
|
6af124e4d3 | ||
|
|
b3b1d8f193 | ||
|
|
785580115b | ||
|
|
b4bd04c146 | ||
|
|
e88c6b8a59 | ||
|
|
74868238f3 | ||
|
|
61a300250d | ||
|
|
d8dbc0866f | ||
|
|
586d96ae74 | ||
|
|
81032a5745 | ||
|
|
c2d726beaf | ||
|
|
3bafdf7bfd | ||
|
|
edcc7ea34f | ||
|
|
6261a7b5c9 | ||
|
|
881832c92d | ||
|
|
47d4dc7ef0 | ||
|
|
10ce81bf98 | ||
|
|
98b3d9f81e | ||
|
|
81cbb7dcc4 | ||
|
|
9517876bd0 | ||
|
|
231d132792 | ||
|
|
9ada5dfea4 | ||
|
|
476c94407f | ||
|
|
458da0e9b2 | ||
|
|
66673012ac | ||
|
|
46f8b6dc5a | ||
|
|
ee81e69ece | ||
|
|
3927f05267 | ||
|
|
a010ab5a29 | ||
|
|
c49bebd412 | ||
|
|
5a8105f5a0 | ||
|
|
df66adeef6 | ||
|
|
4e2367c868 | ||
|
|
53c701cc0e | ||
|
|
92fced75da | ||
|
|
4dd838b8bc | ||
|
|
0a3c375943 | ||
|
|
64a0760a47 | ||
|
|
2e7db47806 | ||
|
|
d2d56a7f71 | ||
|
|
b4897ff1b5 | ||
|
|
661a573bf5 | ||
|
|
0c9bd87602 | ||
|
|
896d888710 | ||
|
|
76aee7f189 | ||
|
|
147b30f973 | ||
|
|
a73dc72558 | ||
|
|
c99cf5518d | ||
|
|
1391675a3a | ||
|
|
a3b2784f31 | ||
|
|
cbe79ee98c | ||
|
|
eb7a2988bf | ||
|
|
42426b95ee | ||
|
|
262a6f31e7 | ||
|
|
04930c3644 | ||
|
|
44594a8e43 | ||
|
|
690d981f31 | ||
|
|
06ea8d1e6c | ||
|
|
c4e3c5a8be | ||
|
|
03f4e7cc34 | ||
|
|
0e54a08ae0 | ||
|
|
b71c752e9d | ||
|
|
521848f955 | ||
|
|
debb17c952 | ||
|
|
7819740f70 | ||
|
|
b9ffb0d8de | ||
|
|
d59852f594 | ||
|
|
085f482820 | ||
|
|
1cb8da96ce | ||
|
|
50016a9eca | ||
|
|
924f7325b4 | ||
|
|
181f678d9e | ||
|
|
6ce1b83ca2 | ||
|
|
a08a653f20 | ||
|
|
e1f1772080 | ||
|
|
d2a1f792e9 | ||
|
|
e7bd80e05f | ||
|
|
9089c0323c | ||
|
|
197e5663df | ||
|
|
f6dedaa619 | ||
|
|
0906dbe637 | ||
|
|
0629b3f6d6 | ||
|
|
55dfc0ec7d | ||
|
|
b7f8b26f1d | ||
|
|
9ca7dd50bc | ||
|
|
0e73d57bdf | ||
|
|
9de2c17e47 | ||
|
|
1bae6d33f1 | ||
|
|
a52b4d2f43 | ||
|
|
3e038bf124 | ||
|
|
870ed9c435 | ||
|
|
68e7ca3277 | ||
|
|
c9142c2156 | ||
|
|
7d923590ae | ||
|
|
5383ced1ca | ||
|
|
0e6c92dbd9 | ||
|
|
b00978c61a | ||
|
|
fb583e28e0 | ||
|
|
760386e3d7 | ||
|
|
51ba54971a | ||
|
|
2ffcf1e82b | ||
|
|
818f4f45a4 | ||
|
|
2cf19f6af1 | ||
|
|
ef82c6e32b | ||
|
|
0e9b9d6251 | ||
|
|
419b34ea73 | ||
|
|
f64c4ed374 | ||
|
|
1677f404d2 | ||
|
|
a612a363ed | ||
|
|
202bc1144b | ||
|
|
a0c654e43f | ||
|
|
2bb677dc19 | ||
|
|
e33805fe42 | ||
|
|
04ac0a1121 | ||
|
|
d2b0bd4e28 | ||
|
|
d754899563 | ||
|
|
4a81e643a7 | ||
|
|
b23e74f8f5 | ||
|
|
5dc766249a | ||
|
|
a895833c7f | ||
|
|
3f81a3c179 | ||
|
|
f1235f745f | ||
|
|
1ddb1de998 | ||
|
|
c472698b1d | ||
|
|
4e021a0731 | ||
|
|
bfbc156821 | ||
|
|
bfd6274602 | ||
|
|
3abaa5ae84 | ||
|
|
65115a50a9 | ||
|
|
06138e15d0 | ||
|
|
14710e3178 | ||
|
|
f705f09749 | ||
|
|
82c445f41e | ||
|
|
1b40e9d65f | ||
|
|
725f7c8735 | ||
|
|
7cc8578148 | ||
|
|
6adafb8ce7 | ||
|
|
ac47df8475 | ||
|
|
f680cf78ab | ||
|
|
c86e84c534 | ||
|
|
3fb611825c | ||
|
|
1cfdad0176 | ||
|
|
59ba325273 | ||
|
|
c4b7c2623f | ||
|
|
e5b2d2ee45 | ||
|
|
64c226b855 | ||
|
|
e3e1da2fd8 | ||
|
|
57b2f8a413 | ||
|
|
5a76a511db | ||
|
|
283f049780 | ||
|
|
c4743cc867 | ||
|
|
c429a860b1 | ||
|
|
1f748c829b | ||
|
|
dd08c7f701 | ||
|
|
77103193c0 | ||
|
|
16335a6bd6 | ||
|
|
e0f6d8ecf2 | ||
|
|
59c31ff88d | ||
|
|
93bf0c210c | ||
|
|
4f7642b1d2 | ||
|
|
773dca8494 | ||
|
|
3cd26e2e38 | ||
|
|
271761cf04 | ||
|
|
6eea552fb9 | ||
|
|
81dd1a7530 | ||
|
|
2eb6e70e57 | ||
|
|
6bcc67634c | ||
|
|
062d8eb206 | ||
|
|
f0d7496bc6 | ||
|
|
8e2b768236 | ||
|
|
48bf326994 | ||
|
|
159d1102aa | ||
|
|
dbb4dbc0a7 | ||
|
|
777e768243 | ||
|
|
70999a70b8 | ||
|
|
3a6b2c2c35 | ||
|
|
dfb80ba8d6 | ||
|
|
94b818b156 | ||
|
|
f1cea1498b | ||
|
|
345678577a | ||
|
|
fb4138cfe6 | ||
|
|
db5b34d589 | ||
|
|
8963af9229 | ||
|
|
2041789ff4 | ||
|
|
aec86f93ea | ||
|
|
57bfb03e05 | ||
|
|
c2b2476e38 | ||
|
|
fa2027d453 | ||
|
|
9d980e4917 | ||
|
|
673243c6cd | ||
|
|
7376223eb8 | ||
|
|
ecd0b8e22f | ||
|
|
c4a608b5bd | ||
|
|
4d9e21ea16 |
@@ -241,6 +241,91 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"data"
|
"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",
|
||||||
|
"ideas",
|
||||||
|
"test"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "ahti123",
|
||||||
|
"name": "Ahti Liin",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/22232632?v=4",
|
||||||
|
"profile": "https://github.com/ahti123",
|
||||||
|
"contributions": [
|
||||||
|
"code",
|
||||||
|
"bug"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "xwu64",
|
||||||
|
"name": "Xiaoliang Wu",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/10580396?v=4",
|
||||||
|
"profile": "https://github.com/xwu64",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
|||||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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
@@ -16,3 +16,4 @@ cli.spec
|
|||||||
*.pyc
|
*.pyc
|
||||||
docsrc/_build/
|
docsrc/_build/
|
||||||
venv/
|
venv/
|
||||||
|
.python-version
|
||||||
|
|||||||
402
CHANGELOG.md
@@ -4,6 +4,408 @@ 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).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [v0.45.8](https://github.com/RhetTbull/osxphotos/compare/v0.45.6...v0.45.8)
|
||||||
|
|
||||||
|
> 5 February 2022
|
||||||
|
|
||||||
|
- Fixed exiftool to ignore unsupported file types, #615 [`1ae6270`](https://github.com/RhetTbull/osxphotos/commit/1ae627056113fc4655f1b24cfbbdf0efc04489e7)
|
||||||
|
- Updated tests [`55a601c`](https://github.com/RhetTbull/osxphotos/commit/55a601c07ea1384623c55d5c1d26b568df5d7823)
|
||||||
|
- Additional fix for #615 [`1d6bc4e`](https://github.com/RhetTbull/osxphotos/commit/1d6bc4e09e3c2359a21f842fadd781920606812e)
|
||||||
|
|
||||||
|
#### [v0.45.6](https://github.com/RhetTbull/osxphotos/compare/v0.45.5...v0.45.6)
|
||||||
|
|
||||||
|
> 5 February 2022
|
||||||
|
|
||||||
|
- Fix for unicode in query strings, #618 [`9b247ac`](https://github.com/RhetTbull/osxphotos/commit/9b247acd1cc4b2def59fdd18a6fb3c8eb9914f11)
|
||||||
|
- Fix for --name searching only original_filename on Photos 5+, #594 [`cd02144`](https://github.com/RhetTbull/osxphotos/commit/cd02144ac33cc1c13a20358133971c84d35b8a57)
|
||||||
|
|
||||||
|
#### [v0.45.5](https://github.com/RhetTbull/osxphotos/compare/v0.45.4...v0.45.5)
|
||||||
|
|
||||||
|
> 5 February 2022
|
||||||
|
|
||||||
|
- Fix for #561, no really, I mean it this time [`b3d3e14`](https://github.com/RhetTbull/osxphotos/commit/b3d3e14ffe41fbb22edb614b24f3985f379766a2)
|
||||||
|
- Updated docs [skip ci] [`2b9ea11`](https://github.com/RhetTbull/osxphotos/commit/2b9ea11701799af9a661a8e2af70fca97235f487)
|
||||||
|
- Updated tests for #561 [skip ci] [`77a49a0`](https://github.com/RhetTbull/osxphotos/commit/77a49a09a1bee74113a7114c543fbc25fa410ffc)
|
||||||
|
|
||||||
|
#### [v0.45.4](https://github.com/RhetTbull/osxphotos/compare/v0.45.3...v0.45.4)
|
||||||
|
|
||||||
|
> 3 February 2022
|
||||||
|
|
||||||
|
- docs: add oPromessa as a contributor for ideas, test [`#611`](https://github.com/RhetTbull/osxphotos/pull/611)
|
||||||
|
- Fix for filenames with special characters, #561, #618 [`f3063d3`](https://github.com/RhetTbull/osxphotos/commit/f3063d35be3c96342d83dbd87ddd614a2001bff4)
|
||||||
|
- Updated docs [skip ci] [`06c5bbf`](https://github.com/RhetTbull/osxphotos/commit/06c5bbfcfdf591a4a5d43f1456adaa27385fe01a)
|
||||||
|
- Added progress counter, #601 [`7ab5007`](https://github.com/RhetTbull/osxphotos/commit/7ab500740b28594dcd778140e10991f839220e9d)
|
||||||
|
- Updated known issues [skip ci] [`e32090b`](https://github.com/RhetTbull/osxphotos/commit/e32090bf39cb786171b49443f878ffdbab774420)
|
||||||
|
|
||||||
|
#### [v0.45.3](https://github.com/RhetTbull/osxphotos/compare/v0.45.2...v0.45.3)
|
||||||
|
|
||||||
|
> 29 January 2022
|
||||||
|
|
||||||
|
- Added --timestamp option for --verbose, #600 [`d8c2f99`](https://github.com/RhetTbull/osxphotos/commit/d8c2f99c06bc6f72bf2cb1a13c5765824fe3cbba)
|
||||||
|
- Updated docs [skip ci] [`5fc2813`](https://github.com/RhetTbull/osxphotos/commit/5fc28139ea0374bc3e228c0432b8a41ada430389)
|
||||||
|
- Updated formatting for elapsed time, #604 [`16d3f74`](https://github.com/RhetTbull/osxphotos/commit/16d3f743664396d43b3b3028a5e7a919ec56d9e1)
|
||||||
|
|
||||||
|
#### [v0.45.2](https://github.com/RhetTbull/osxphotos/compare/v0.45.0...v0.45.2)
|
||||||
|
|
||||||
|
> 29 January 2022
|
||||||
|
|
||||||
|
- Implemented #605, refactor out export2 [`235dea3`](https://github.com/RhetTbull/osxphotos/commit/235dea329c98ab8fa61565c09a1b4a83e5d99043)
|
||||||
|
- Fix for #564, --preview with --download-missing [`5afdf6f`](https://github.com/RhetTbull/osxphotos/commit/5afdf6fc20a3cb6eb2b0217d8b3be20295eb7ba4)
|
||||||
|
|
||||||
|
#### [v0.45.0](https://github.com/RhetTbull/osxphotos/compare/v0.44.13...v0.45.0)
|
||||||
|
|
||||||
|
> 28 January 2022
|
||||||
|
|
||||||
|
- Performance improvements and refactoring, #462, partial for #591 [`22964af`](https://github.com/RhetTbull/osxphotos/commit/22964afc6988166218413125d7a62348bb858a83)
|
||||||
|
- Refactored photoexporter for performance, #591 [`6843b86`](https://github.com/RhetTbull/osxphotos/commit/6843b8661d41d42368794c77304fc07194e7af18)
|
||||||
|
- Performance improvements, partial for #591 [`3bc53fd`](https://github.com/RhetTbull/osxphotos/commit/3bc53fd92b3222c6959e7aa12310811db41b83fe)
|
||||||
|
|
||||||
|
#### [v0.44.13](https://github.com/RhetTbull/osxphotos/compare/v0.44.12...v0.44.13)
|
||||||
|
|
||||||
|
> 24 January 2022
|
||||||
|
|
||||||
|
- Removed exportdb requirement from PhotoTemplate [`6af124e`](https://github.com/RhetTbull/osxphotos/commit/6af124e4d3a0e26c48f435452920020cd42afa1c)
|
||||||
|
- Version bump [`bd31120`](https://github.com/RhetTbull/osxphotos/commit/bd3112056920806f565be2c0c12caf4f2aff5231)
|
||||||
|
|
||||||
|
#### [v0.44.12](https://github.com/RhetTbull/osxphotos/compare/v0.44.11...v0.44.12)
|
||||||
|
|
||||||
|
> 23 January 2022
|
||||||
|
|
||||||
|
- Added query options to repl, #597 [`7855801`](https://github.com/RhetTbull/osxphotos/commit/785580115b29f5ccb895de22be1243f56dbb43dc)
|
||||||
|
- Added run command, #598 [`b4bd04c`](https://github.com/RhetTbull/osxphotos/commit/b4bd04c1461d0b427937f541403305bc979bcf4f)
|
||||||
|
- Bug fix for get_photos_library_version [`e88c6b8`](https://github.com/RhetTbull/osxphotos/commit/e88c6b8a59dfd947f6cf3c7eac9c92519ab781a3)
|
||||||
|
|
||||||
|
#### [v0.44.11](https://github.com/RhetTbull/osxphotos/compare/v0.44.10...v0.44.11)
|
||||||
|
|
||||||
|
> 23 January 2022
|
||||||
|
|
||||||
|
- creat unit test for __all__ [`#599`](https://github.com/RhetTbull/osxphotos/pull/599)
|
||||||
|
- Performance improvements, added --profile [`7486823`](https://github.com/RhetTbull/osxphotos/commit/74868238f3b1ee18feb744f137f5c14ef8e36ffc)
|
||||||
|
|
||||||
|
#### [v0.44.10](https://github.com/RhetTbull/osxphotos/compare/v0.44.9...v0.44.10)
|
||||||
|
|
||||||
|
> 22 January 2022
|
||||||
|
|
||||||
|
- Create __all__ for all python files [`#589`](https://github.com/RhetTbull/osxphotos/pull/589)
|
||||||
|
- Create __all__ for the file cli.py [`#587`](https://github.com/RhetTbull/osxphotos/pull/587)
|
||||||
|
- docs: add xwu64 as a contributor for code [`#585`](https://github.com/RhetTbull/osxphotos/pull/585)
|
||||||
|
- add __all__ to files "adjustmentsinfo.py" and "albuminfo.py" [`#584`](https://github.com/RhetTbull/osxphotos/pull/584)
|
||||||
|
- More refactoring of export code, #462 [`6261a7b`](https://github.com/RhetTbull/osxphotos/commit/6261a7b5c96ac43aece66b72b9e27a90854accfa)
|
||||||
|
- Added ExportOptions to photoexporter.py, #462 [`9517876`](https://github.com/RhetTbull/osxphotos/commit/9517876bd06572238648a6362a309063b86007e7)
|
||||||
|
- Blackified files [`3bafdf7`](https://github.com/RhetTbull/osxphotos/commit/3bafdf7bfd5f7992b2e0c12496c55e7be1f57455)
|
||||||
|
- More refactoring of export code, #462 [`c2d726b`](https://github.com/RhetTbull/osxphotos/commit/c2d726beafabe76cf4d5fb3213447c900129b8c0)
|
||||||
|
- Refactored photoexporter sidecar writing, #462 [`458da0e`](https://github.com/RhetTbull/osxphotos/commit/458da0e9b2b82a78cec30191c5bf1ee2ed993acf)
|
||||||
|
|
||||||
|
#### [v0.44.9](https://github.com/RhetTbull/osxphotos/compare/v0.44.8...v0.44.9)
|
||||||
|
|
||||||
|
> 9 January 2022
|
||||||
|
|
||||||
|
- Added diff command [`3927f05`](https://github.com/RhetTbull/osxphotos/commit/3927f052670b2a1c31cced1f8278a0ffe519a3eb)
|
||||||
|
- Added uuid command [`a010ab5`](https://github.com/RhetTbull/osxphotos/commit/a010ab5a299470782b938e689a7ddc336513065e)
|
||||||
|
|
||||||
|
#### [v0.44.8](https://github.com/RhetTbull/osxphotos/compare/v0.44.7...v0.44.8)
|
||||||
|
|
||||||
|
> 9 January 2022
|
||||||
|
|
||||||
|
- docs: add ahti123 as a contributor for code, bug [`#578`](https://github.com/RhetTbull/osxphotos/pull/578)
|
||||||
|
- changing photos_5 version constant to satisfy version 5001 [`#577`](https://github.com/RhetTbull/osxphotos/pull/577)
|
||||||
|
- Added grep command to CLI [`4dd838b`](https://github.com/RhetTbull/osxphotos/commit/4dd838b8bcb639eba3df9cb60a7cd28f45b22833)
|
||||||
|
- Added test for #576 [`92fced7`](https://github.com/RhetTbull/osxphotos/commit/92fced75da38f1c47be8d3d9d4ee22463ad029b9)
|
||||||
|
- Added sqlgrep [`53c701c`](https://github.com/RhetTbull/osxphotos/commit/53c701cc0ebd38db255c1ce694391b38dbb5fe01)
|
||||||
|
- Fix for #575, database version 5001 [`5a8105f`](https://github.com/RhetTbull/osxphotos/commit/5a8105f5a02080368ad22717c064afcb0748f646)
|
||||||
|
- Updated docs [skip ci] [`64a0760`](https://github.com/RhetTbull/osxphotos/commit/64a0760a47205a452e015a860f39f45bba67164a)
|
||||||
|
|
||||||
|
#### [v0.44.7](https://github.com/RhetTbull/osxphotos/compare/v0.44.6...v0.44.7)
|
||||||
|
|
||||||
|
> 8 January 2022
|
||||||
|
|
||||||
|
- Fix for #576, error exporting edited live photos [`2e7db47`](https://github.com/RhetTbull/osxphotos/commit/2e7db47806683fdd0db4d1d75e42471d2f127d4d)
|
||||||
|
|
||||||
|
#### [v0.44.6](https://github.com/RhetTbull/osxphotos/compare/v0.44.5...v0.44.6)
|
||||||
|
|
||||||
|
> 6 January 2022
|
||||||
|
|
||||||
|
- Fix for burst images with pick type = 0, partial fix for #571 [`d2d56a7`](https://github.com/RhetTbull/osxphotos/commit/d2d56a7f7118aeffa7ac81cc474fdd4fb4843065)
|
||||||
|
|
||||||
|
#### [v0.44.5](https://github.com/RhetTbull/osxphotos/compare/v0.44.4...v0.44.5)
|
||||||
|
|
||||||
|
> 6 January 2022
|
||||||
|
|
||||||
|
- More refactoring of export code, #462 [`0c9bd87`](https://github.com/RhetTbull/osxphotos/commit/0c9bd8760261770e11b0fa59153f49f2d65e2c2f)
|
||||||
|
- Fix for #570 [`661a573`](https://github.com/RhetTbull/osxphotos/commit/661a573bf50353fb2393c604080ffe0790ade59c)
|
||||||
|
- version bump [skip ci] [`b4897ff`](https://github.com/RhetTbull/osxphotos/commit/b4897ff1b5d2bc00f34158345b2b5fe85f1490ac)
|
||||||
|
|
||||||
|
#### [v0.44.4](https://github.com/RhetTbull/osxphotos/compare/v0.44.3...v0.44.4)
|
||||||
|
|
||||||
|
> 4 January 2022
|
||||||
|
|
||||||
|
- Refactored photoinfo, photoexporter; #462 [`a73dc72`](https://github.com/RhetTbull/osxphotos/commit/a73dc72558b77152f4c90f143b6a60924b8905c8)
|
||||||
|
- More refactoring of export code, #462 [`147b30f`](https://github.com/RhetTbull/osxphotos/commit/147b30f97308db65868dc7a8d177d77ad0d0ad40)
|
||||||
|
- Export DB can now reside outside export directory, #568 [`76aee7f`](https://github.com/RhetTbull/osxphotos/commit/76aee7f189b4b32e2e263a4e798711713ed17a14)
|
||||||
|
|
||||||
|
#### [v0.44.3](https://github.com/RhetTbull/osxphotos/compare/v0.44.2...v0.44.3)
|
||||||
|
|
||||||
|
> 31 December 2021
|
||||||
|
|
||||||
|
- ImageConverter now uses generic context; #562 [`a3b2784`](https://github.com/RhetTbull/osxphotos/commit/a3b2784f3177a753b78965b8ca205ca9bbb08168)
|
||||||
|
- Updated tests and docs [`1391675`](https://github.com/RhetTbull/osxphotos/commit/1391675a3a45be0d6800a68c8bcc6d0d55d1ab7a)
|
||||||
|
- Updated docs [skip ci] [`cbe79ee`](https://github.com/RhetTbull/osxphotos/commit/cbe79ee98cae68e0789df275220f5a5870a8bd91)
|
||||||
|
|
||||||
|
#### [v0.44.2](https://github.com/RhetTbull/osxphotos/compare/v0.44.1...v0.44.2)
|
||||||
|
|
||||||
|
> 31 December 2021
|
||||||
|
|
||||||
|
- Bug fix for #559 [`42426b9`](https://github.com/RhetTbull/osxphotos/commit/42426b95ee786b2d53482d3d931a0b962a4db20d)
|
||||||
|
|
||||||
|
#### [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)
|
#### [v0.42.69](https://github.com/RhetTbull/osxphotos/compare/v0.42.67...v0.42.69)
|
||||||
|
|
||||||
> 28 July 2021
|
> 28 July 2021
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
include osxphotos/*.json
|
||||||
|
include osxphotos/*.md
|
||||||
|
include osxphotos/phototemplate.tx
|
||||||
|
include osxphotos/queries/*
|
||||||
|
include osxphotos/templates/*
|
||||||
include README.md
|
include README.md
|
||||||
include README.rst
|
include README.rst
|
||||||
include osxphotos/templates/*
|
|
||||||
include osxphotos/phototemplate.tx
|
|
||||||
include osxphotos/phototemplate.md
|
|
||||||
350
README.md
@@ -4,7 +4,9 @@
|
|||||||
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
||||||

|

|
||||||
[](https://pepy.tech/project/osxphotos)
|
[](https://pepy.tech/project/osxphotos)
|
||||||
[](#contributors)
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
|
[](#contributors)
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||||
|
|
||||||
@@ -12,7 +14,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
|||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
* [Supported operating systems](#supported-operating-systems)
|
* [Supported operating systems](#supported-operating-systems)
|
||||||
* [Installation instructions](#installation-instructions)
|
* [Installation](#installation)
|
||||||
* [Command Line Usage](#command-line-usage)
|
* [Command Line Usage](#command-line-usage)
|
||||||
+ [Command line examples](#command-line-examples)
|
+ [Command line examples](#command-line-examples)
|
||||||
+ [Tutorial](#tutorial)
|
+ [Tutorial](#tutorial)
|
||||||
@@ -23,6 +25,7 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
|||||||
+ [ExifInfo](#exifinfo)
|
+ [ExifInfo](#exifinfo)
|
||||||
+ [AlbumInfo](#albuminfo)
|
+ [AlbumInfo](#albuminfo)
|
||||||
+ [ImportInfo](#importinfo)
|
+ [ImportInfo](#importinfo)
|
||||||
|
+ [ProjectInfo](#projectinfo)
|
||||||
+ [FolderInfo](#folderinfo)
|
+ [FolderInfo](#folderinfo)
|
||||||
+ [PlaceInfo](#placeinfo)
|
+ [PlaceInfo](#placeinfo)
|
||||||
+ [ScoreInfo](#scoreinfo)
|
+ [ScoreInfo](#scoreinfo)
|
||||||
@@ -35,6 +38,8 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
|||||||
+ [Raw Photos](#raw-photos)
|
+ [Raw Photos](#raw-photos)
|
||||||
+ [Template System](#template-system)
|
+ [Template System](#template-system)
|
||||||
+ [ExifTool](#exiftoolExifTool)
|
+ [ExifTool](#exiftoolExifTool)
|
||||||
|
+ [PhotoExporter](#photoexporter)
|
||||||
|
+ [Text Detection](#textdetection)
|
||||||
+ [Utility Functions](#utility-functions)
|
+ [Utility Functions](#utility-functions)
|
||||||
* [Examples](#examples)
|
* [Examples](#examples)
|
||||||
* [Related Projects](#related-projects)
|
* [Related Projects](#related-projects)
|
||||||
@@ -49,13 +54,12 @@ OSXPhotos provides the ability to interact with and query Apple's Photos.app lib
|
|||||||
|
|
||||||
## Supported operating systems
|
## Supported operating systems
|
||||||
|
|
||||||
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) until macOS Big Sur (10.16/11.3).
|
Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through macOS Monterey (12.0.1). Tested on both x86 and Apple silicon (M1).
|
||||||
|
|
||||||
If you have access to the macOS 12 / Monterey beta and would like to help ensure osxphotos is compatible, please visit the [Discussions](https://github.com/RhetTbull/osxphotos/discussions) page and let me know!
|
|
||||||
|
|
||||||
| macOS Version | macOS name | Photos.app version |
|
| macOS Version | macOS name | Photos.app version |
|
||||||
| ----------------- |------------|:-------------------|
|
| ----------------- |------------|:-------------------|
|
||||||
| 12.0 | Monterey | ?.0 UNKNOWN |
|
| 12.0 | Monterey | 7.0 ✅ |
|
||||||
| 10.16, 11.0-11.4 | Big Sur | 6.0 ✅ |
|
| 10.16, 11.0-11.4 | Big Sur | 6.0 ✅ |
|
||||||
| 10.15.1 - 10.15.7 | Catalina | 5.0 ✅ |
|
| 10.15.1 - 10.15.7 | Catalina | 5.0 ✅ |
|
||||||
| 10.14.5, 10.14.6 | Mojave | 4.0 ✅ |
|
| 10.14.5, 10.14.6 | Mojave | 4.0 ✅ |
|
||||||
@@ -137,20 +141,25 @@ Options:
|
|||||||
-h, --help Show this message and exit.
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
about Print information about osxphotos including license.
|
about Print information about osxphotos including license.
|
||||||
albums Print out albums found in the Photos library.
|
albums Print out albums found in the Photos library.
|
||||||
dump Print list of all photos & associated info from the Photos...
|
diff Compare two Photos databases and print out differences
|
||||||
export Export photos from the Photos database.
|
dump Print list of all photos & associated info from the Photos...
|
||||||
help Print help; for help on commands: help <command>.
|
export Export photos from the Photos database.
|
||||||
info Print out descriptive info of the Photos library database.
|
help Print help; for help on commands: help <command>.
|
||||||
keywords Print out keywords found in the Photos library.
|
info Print out descriptive info of the Photos library database.
|
||||||
labels Print out image classification labels found in the Photos...
|
install Install Python packages into the same environment as osxphotos
|
||||||
list Print list of Photos libraries found on the system.
|
keywords Print out keywords found in the Photos library.
|
||||||
persons Print out persons (faces) found in the Photos library.
|
labels Print out image classification labels found in the Photos...
|
||||||
places Print out places found in the Photos library.
|
list Print list of Photos libraries found on the system.
|
||||||
query Query the Photos database using 1 or more search options; if...
|
persons Print out persons (faces) found in the Photos library.
|
||||||
repl Run interactive osxphotos shell
|
places Print out places found in the Photos library.
|
||||||
tutorial Display osxphotos tutorial.
|
query Query the Photos database using 1 or more search options; if...
|
||||||
|
repl Run interactive osxphotos REPL shell (useful for debugging,...
|
||||||
|
snap Create snapshot of Photos database to use with diff command
|
||||||
|
tutorial Display osxphotos tutorial.
|
||||||
|
uninstall Uninstall Python packages from the osxphotos environment
|
||||||
|
uuid Print out unique IDs (UUID) of photos selected in Photos
|
||||||
```
|
```
|
||||||
|
|
||||||
To get help on a specific command, use `osxphotos help <command_name>`
|
To get help on a specific command, use `osxphotos help <command_name>`
|
||||||
@@ -450,15 +459,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:
|
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:
|
Explanation of the template string:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
{title}{title?{descr?{newline},},}{descr}
|
{title,}{title?{descr?{newline},},}{descr,}
|
||||||
│ │ │ │ │ │ │
|
│ │ │ │ │ │ │
|
||||||
│ │ │ │ │ │ │
|
│ │ │ │ │ │ │
|
||||||
└──> insert title │ │ │ │ │
|
└──> insert title (or nothing if no title)
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │
|
||||||
└───> is there a title?
|
└───> is there a title?
|
||||||
│ │ │ │ │
|
│ │ │ │ │
|
||||||
@@ -471,6 +480,7 @@ Explanation of the template string:
|
|||||||
└───> if title is blank, insert nothing
|
└───> 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.
|
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.
|
||||||
@@ -591,6 +601,7 @@ Options:
|
|||||||
library, 2. system library, 3.
|
library, 2. system library, 3.
|
||||||
~/Pictures/Photos Library.photoslibrary
|
~/Pictures/Photos Library.photoslibrary
|
||||||
-V, --verbose Print verbose output.
|
-V, --verbose Print verbose output.
|
||||||
|
--timestamp Add time stamp to verbose output
|
||||||
--keyword KEYWORD Search for photos with keyword KEYWORD. If
|
--keyword KEYWORD Search for photos with keyword KEYWORD. If
|
||||||
more than one keyword, treated as "OR", e.g.
|
more than one keyword, treated as "OR", e.g.
|
||||||
find photos matching any keyword
|
find photos matching any keyword
|
||||||
@@ -609,7 +620,8 @@ Options:
|
|||||||
FILENAME. If more than one --name options is
|
FILENAME. If more than one --name options is
|
||||||
specified, they are treated as "OR", e.g. find
|
specified, they are treated as "OR", e.g. find
|
||||||
photos matching any FILENAME.
|
photos matching any FILENAME.
|
||||||
--uuid UUID Search for photos with UUID(s).
|
--uuid UUID Search for photos with UUID(s). May be
|
||||||
|
repeated to include multiple UUIDs.
|
||||||
--uuid-from-file FILE Search for photos with UUID(s) loaded from
|
--uuid-from-file FILE Search for photos with UUID(s) loaded from
|
||||||
FILE. Format is a single UUID per line. Lines
|
FILE. Format is a single UUID per line. Lines
|
||||||
preceded with # are ignored.
|
preceded with # are ignored.
|
||||||
@@ -726,6 +738,15 @@ Options:
|
|||||||
repeating '--regex' with different arguments.
|
repeating '--regex' with different arguments.
|
||||||
--selected Filter for photos that are currently selected
|
--selected Filter for photos that are currently selected
|
||||||
in Photos.
|
in Photos.
|
||||||
|
--exif EXIF_TAG VALUE 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.
|
||||||
--query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA
|
--query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA
|
||||||
will be evaluated in context of the following
|
will be evaluated in context of the following
|
||||||
python list comprehension: `photos = [photo
|
python list comprehension: `photos = [photo
|
||||||
@@ -822,6 +843,11 @@ Options:
|
|||||||
photos if the RAW photo does not have an
|
photos if the RAW photo does not have an
|
||||||
associated JPEG image (e.g. the RAW file was
|
associated JPEG image (e.g. the RAW file was
|
||||||
imported to Photos without a JPEG preview).
|
imported to Photos without a JPEG preview).
|
||||||
|
--skip-uuid UUID Skip photos with UUID(s) during export. May be
|
||||||
|
repeated to include multiple UUIDs.
|
||||||
|
--skip-uuid-from-file FILE Skip photos with UUID(s) loaded from FILE.
|
||||||
|
Format is a single UUID per line. Lines
|
||||||
|
preceded with # are ignored.
|
||||||
--current-name Use photo's current filename instead of
|
--current-name Use photo's current filename instead of
|
||||||
original filename for export. Note: Starting
|
original filename for export. Note: Starting
|
||||||
with Photos 5, all photos are renamed upon
|
with Photos 5, all photos are renamed upon
|
||||||
@@ -1133,14 +1159,13 @@ Options:
|
|||||||
You can run more than one function by
|
You can run more than one function by
|
||||||
repeating the '--post-function' option with
|
repeating the '--post-function' option with
|
||||||
different arguments. See Post Function below.
|
different arguments. See Post Function below.
|
||||||
--exportdb EXPORTDB_FILE Specify alternate name for database file which
|
--exportdb EXPORTDB_FILE Specify alternate path for database file which
|
||||||
stores state information for export and
|
stores state information for export and
|
||||||
--update. If --exportdb is not specified,
|
--update. If --exportdb is not specified,
|
||||||
export database will be saved to
|
export database will be saved to
|
||||||
'.osxphotos_export.db' in the export
|
'.osxphotos_export.db' in the export
|
||||||
directory. Must be specified as filename
|
directory. If --exportdb is specified, it
|
||||||
only, not a path, as export database will be
|
will be saved to the specified file.
|
||||||
saved in export directory.
|
|
||||||
--load-config <config file path>
|
--load-config <config file path>
|
||||||
Load options from file as written with --save-
|
Load options from file as written with --save-
|
||||||
config. This allows you to save a complex
|
config. This allows you to save a complex
|
||||||
@@ -1155,6 +1180,9 @@ Options:
|
|||||||
--save-config <config file path>
|
--save-config <config file path>
|
||||||
Save options to file for use with --load-
|
Save options to file for use with --load-
|
||||||
config. File format is TOML.
|
config. File format is TOML.
|
||||||
|
-M, --multiprocess NUMBER_OF_PROCESSES
|
||||||
|
Run export in parallel using
|
||||||
|
NUMBER_OF_PROCESSES processes. [x>=1]
|
||||||
--help Show this message and exit.
|
--help Show this message and exit.
|
||||||
|
|
||||||
** Export **
|
** Export **
|
||||||
@@ -1256,8 +1284,8 @@ s
|
|||||||
** Templating System **
|
** Templating System **
|
||||||
|
|
||||||
The templating system converts one or template statements, written in osxphotos
|
The templating system converts one or template statements, written in osxphotos
|
||||||
templating language, to one or more rendered values using information from the
|
metadata templating language, to one or more rendered values using information
|
||||||
photo being processed.
|
from the photo being processed.
|
||||||
|
|
||||||
In its simplest form, a template statement has the form: "{template_field}", for
|
In its simplest form, a template statement has the form: "{template_field}", for
|
||||||
example "{title}" which would resolve to the title of the photo.
|
example "{title}" which would resolve to the title of the photo.
|
||||||
@@ -1700,7 +1728,7 @@ Substitution Description
|
|||||||
{lf} A line feed: '\n', alias for {newline}
|
{lf} A line feed: '\n', alias for {newline}
|
||||||
{cr} A carriage return: '\r'
|
{cr} A carriage return: '\r'
|
||||||
{crlf} a carriage return + line feed: '\r\n'
|
{crlf} a carriage return + line feed: '\r\n'
|
||||||
{osxphotos_version} The osxphotos version, e.g. '0.42.70'
|
{osxphotos_version} The osxphotos version, e.g. '0.45.8'
|
||||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||||
|
|
||||||
The following substitutions may result in multiple values. Thus if specified for
|
The following substitutions may result in multiple values. Thus if specified for
|
||||||
@@ -1715,6 +1743,13 @@ Substitution Description
|
|||||||
{folder_album} Folder path + album photo is contained in. e.g.
|
{folder_album} Folder path + album photo is contained in. e.g.
|
||||||
'Folder/Subfolder/Album' or just 'Album' if no
|
'Folder/Subfolder/Album' or just 'Album' if no
|
||||||
enclosing folder
|
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
|
{keyword} Keyword(s) assigned to photo
|
||||||
{person} Person(s) / face(s) in a photo
|
{person} Person(s) / face(s) in a photo
|
||||||
{label} Image categorization label associated with a photo
|
{label} Image categorization label associated with a photo
|
||||||
@@ -1769,13 +1804,16 @@ Substitution Description
|
|||||||
not need to reprocess each photo. You may pass a
|
not need to reprocess each photo. You may pass a
|
||||||
confidence threshold value between 0.0 and 1.0 after
|
confidence threshold value between 0.0 and 1.0 after
|
||||||
a colon as in '{detected_text:0.5}'; The default
|
a colon as in '{detected_text:0.5}'; The default
|
||||||
confidence threshold is 0.75. Note: this feature is
|
confidence threshold is 0.75. '{detected_text}' works
|
||||||
not the same thing as Live Text in macOS Monterey,
|
only on macOS Catalina (10.15) or later. Note: this
|
||||||
which osxphotos does not yet support.
|
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
|
{shell_quote} Use in form '{shell_quote,TEMPLATE}'; quotes the
|
||||||
rendered TEMPLATE value(s) for safe usage in the
|
rendered TEMPLATE value(s) for safe usage in the
|
||||||
shell, e.g. My file.jpeg => 'My file.jpeg'; only adds
|
shell, e.g. My file.jpeg => 'My file.jpeg'; only adds
|
||||||
quotes if needed.
|
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
|
{function} Execute a python function from an external file and
|
||||||
use return value as template substitution. Use in
|
use return value as template substitution. Use in
|
||||||
format: {function:file.py::function_name} where
|
format: {function:file.py::function_name} where
|
||||||
@@ -1815,7 +1853,7 @@ COMMAND is an osxphotos template string which will be rendered and passed to the
|
|||||||
shell for execution. CATEGORY is the category of file to pass to COMMAND. The
|
shell for execution. CATEGORY is the category of file to pass to COMMAND. The
|
||||||
following categories are available:
|
following categories are available:
|
||||||
|
|
||||||
Catgory Description
|
Category Description
|
||||||
exported All exported files
|
exported All exported files
|
||||||
new When used with '--update', all newly exported files
|
new When used with '--update', all newly exported files
|
||||||
updated When used with '--update', all files which were
|
updated When used with '--update', all files which were
|
||||||
@@ -1856,13 +1894,13 @@ Both the '{shell_quote}' template and the '|shell_quote' template filter are
|
|||||||
available for this purpose. For example, the following command outputs the full
|
available for this purpose. For example, the following command outputs the full
|
||||||
path of newly exported files to file 'new.txt':
|
path of newly exported files to file 'new.txt':
|
||||||
|
|
||||||
--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}"
|
||||||
|
|
||||||
In the above command, the 'shell_quote' filter is used to ensure
|
In the above command, the 'shell_quote' filter is used to ensure '{filepath}' is
|
||||||
'{filepath.name}' is properly quoted and the '{shell_quote}' template ensures
|
properly quoted and the '{shell_quote}' template ensures the constructed path of
|
||||||
the constructed path of '{exported_dir}/exported.txt' is properly quoted. If
|
'{exported_dir}/exported.txt' is properly quoted. If '{filepath}' is 'IMG
|
||||||
'{filepath.name}' is 'IMG 1234.jpeg' and '{export_dir}' is '/Volumes/Photo
|
1234.jpeg' and '{export_dir}' is '/Volumes/Photo Export', the command thus
|
||||||
Export', the command thus renders to:
|
renders to:
|
||||||
|
|
||||||
echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'
|
echo 'IMG 1234.jpeg' >> '/Volumes/Photo Export/exported.txt'
|
||||||
|
|
||||||
@@ -2086,7 +2124,7 @@ keywords = photosdb.keywords
|
|||||||
|
|
||||||
Returns a list of the keywords found in the Photos library
|
Returns a list of the keywords found in the Photos library
|
||||||
|
|
||||||
#### `album_info`
|
#### <a name="photosdbalbuminfo">`album_info`</a>
|
||||||
```python
|
```python
|
||||||
# assumes photosdb is a PhotosDB object (see above)
|
# assumes photosdb is a PhotosDB object (see above)
|
||||||
albums = photosdb.album_info
|
albums = photosdb.album_info
|
||||||
@@ -2116,6 +2154,10 @@ Returns list of shared album names found in photos database (e.g. albums shared
|
|||||||
|
|
||||||
Returns a list of [ImportInfo](#importinfo) objects representing the import sessions for the database.
|
Returns a list of [ImportInfo](#importinfo) objects representing the import sessions for the database.
|
||||||
|
|
||||||
|
#### `project_info`
|
||||||
|
|
||||||
|
Returns a list of [ProjectInfo](#projectinfo) objects representing the projects/creations (cards, calendars, etc.) in the database.
|
||||||
|
|
||||||
#### `folder_info`
|
#### `folder_info`
|
||||||
```python
|
```python
|
||||||
# assumes photosdb is a PhotosDB object (see above)
|
# assumes photosdb is a PhotosDB object (see above)
|
||||||
@@ -2368,6 +2410,8 @@ For example, in my library, Photos says I have 19,386 photos and 474 movies. Ho
|
|||||||
#### <a name="getphoto">`get_photo(uuid)`</A>
|
#### <a name="getphoto">`get_photo(uuid)`</A>
|
||||||
Returns a single PhotoInfo instance for photo with UUID matching `uuid` or None if no photo is found matching `uuid`. If you know the UUID of a photo, `get_photo()` is much faster than `photos`. See also [photos()](#photos).
|
Returns a single PhotoInfo instance for photo with UUID matching `uuid` or None if no photo is found matching `uuid`. If you know the UUID of a photo, `get_photo()` is much faster than `photos`. See also [photos()](#photos).
|
||||||
|
|
||||||
|
#### `execute(sql)`
|
||||||
|
Execute sql statement against the Photos database and return a sqlite cursor with the results.
|
||||||
|
|
||||||
### PhotoInfo
|
### PhotoInfo
|
||||||
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
|
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
|
||||||
@@ -2403,11 +2447,15 @@ Returns a list of keywords (e.g. tags) applied to the photo
|
|||||||
Returns a list of albums the photo is contained in. See also [album_info](#album_info).
|
Returns a list of albums the photo is contained in. See also [album_info](#album_info).
|
||||||
|
|
||||||
#### `album_info`
|
#### `album_info`
|
||||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the photo is contained in. See also [albums](#albums).
|
Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the photo is contained in or empty list of the photo is not in any albums. See also [albums](#albums).
|
||||||
|
|
||||||
#### `import_info`
|
#### `import_info`
|
||||||
Returns an [ImportInfo](#importinfo) object representing the import session associated with the photo or `None` if there is no associated import session.
|
Returns an [ImportInfo](#importinfo) object representing the import session associated with the photo or `None` if there is no associated import session.
|
||||||
|
|
||||||
|
#### `project_info`
|
||||||
|
Returns a list of [ProjectInfo](#projectinfo) objects representing projects/creations (cards, calendars, etc.) the photo is contained in or empty list if there are no projects associated with the photo.
|
||||||
|
|
||||||
|
|
||||||
#### `persons`
|
#### `persons`
|
||||||
Returns a list of the names of the persons in the photo
|
Returns a list of the names of the persons in the photo
|
||||||
|
|
||||||
@@ -2511,7 +2559,12 @@ Returns a [PlaceInfo](#PlaceInfo) object with reverse geolocation data or None i
|
|||||||
#### `shared`
|
#### `shared`
|
||||||
Returns True if photo is in a shared album, otherwise False.
|
Returns True if photo is in a shared album, otherwise False.
|
||||||
|
|
||||||
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None instead of True/False.
|
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
|
||||||
|
|
||||||
|
#### `owner`
|
||||||
|
Returns full name of the photo owner (person who shared the photo) for shared photos or None if photo is not shared. Also returns None if you are the person who shared the photo.
|
||||||
|
|
||||||
|
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
|
||||||
|
|
||||||
#### `comments`
|
#### `comments`
|
||||||
Returns list of [CommentInfo](#commentinfo) objects for comments on shared photos or empty list if no comments.
|
Returns list of [CommentInfo](#commentinfo) objects for comments on shared photos or empty list if no comments.
|
||||||
@@ -2718,25 +2771,27 @@ Returns a JSON representation of all photo info.
|
|||||||
Returns a dictionary representation of all photo info.
|
Returns a dictionary representation of all photo info.
|
||||||
|
|
||||||
#### `export()`
|
#### `export()`
|
||||||
`export(dest, filename=None, edited=False, live_photo=False, export_as_hardlink=False, overwrite=False, increment=True, sidecar_json=False, sidecar_exiftool=False, sidecar_xmp=False, use_photos_export=False, timeout=120, exiftool=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
|
`export(dest, filename=None, edited=False, live_photo=False, export_as_hardlink=False, overwrite=False, increment=True, sidecar_json=False, sidecar_exiftool=False, sidecar_xmp=False, download_missing=False, use_photos_export=False, use_photokit=True, timeout=120, exiftool=False, use_albums_as_keywords=False, use_persons_as_keywords=False)`
|
||||||
|
|
||||||
Export photo from the Photos library to another destination on disk.
|
Export photo from the Photos library to another destination on disk.
|
||||||
- dest: must be valid destination path as str (or exception raised).
|
- dest: must be valid destination path as str (or exception raised).
|
||||||
- filename (optional): name of picture as str; if not provided, will use current filename. **NOTE**: if provided, user must ensure file extension (suffix) is correct. For example, if photo is .CR2 file, edited image may be .jpeg. If you provide an extension different than what the actual file is, export will print a warning but will happily export the photo using the incorrect file extension. e.g. to get the extension of the edited photo, look at [PhotoInfo.path_edited](#path_edited).
|
- filename (optional): name of picture as str; if not provided, will use current filename. **NOTE**: if provided, user must ensure file extension (suffix) is correct. For example, if photo is .CR2 file, edited image may be .jpeg. If you provide an extension different than what the actual file is, export will print a warning but will happily export the photo using the incorrect file extension. e.g. to get the extension of the edited photo, look at [PhotoInfo.path_edited](#path_edited).
|
||||||
- edited: boolean; if True (default=False), will export the edited version of the photo (or raise exception if no edited version)
|
- edited: bool; if True (default=False), will export the edited version of the photo (or raise exception if no edited version)
|
||||||
- export_as_hardlink: boolean; if True (default=False), will hardlink files instead of copying them
|
- export_as_hardlink: bool; if True (default=False), will hardlink files instead of copying them
|
||||||
- overwrite: boolean; if True (default=False), will overwrite files if they alreay exist
|
- overwrite: bool; if True (default=False), will overwrite files if they alreay exist
|
||||||
- live_photo: boolean; if True (default=False), will also export the associted .mov for live photos; exported live photo will be named filename.mov
|
- live_photo: bool; if True (default=False), will also export the associted .mov for live photos; exported live photo will be named filename.mov
|
||||||
- increment: boolean; if True (default=True), will increment file name until a non-existent name is found
|
- increment: bool; if True (default=True), will increment file name until a non-existent name is found
|
||||||
- sidecar_json: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name
|
- sidecar_json: (bool, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name
|
||||||
- sidecar_json: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will include tag group names (e.g. `exiftool -G -j`)
|
- sidecar_json: (bool, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will include tag group names (e.g. `exiftool -G -j`)
|
||||||
- sidecar_exiftool: (boolean, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will not include tag group names (e.g. `exiftool -j`)
|
- sidecar_exiftool: (bool, default = False); if True will also write a json sidecar with metadata in format readable by exiftool; sidecar filename will be dest/filename.json where filename is the stem of the photo name; resulting json file will not include tag group names (e.g. `exiftool -j`)
|
||||||
- sidecar_xmp: (boolean, default = False); if True will also write a XMP sidecar with metadata; sidecar filename will be dest/filename.xmp where filename is the stem of the photo name
|
- sidecar_xmp: (bool, default = False); if True will also write a XMP sidecar with metadata; sidecar filename will be dest/filename.xmp where filename is the stem of the photo name
|
||||||
- use_photos_export: boolean; (default=False), if True will attempt to export photo via applescript interaction with Photos; useful for forcing download of missing photos. This only works if the Photos library being used is the default library (last opened by Photos) as applescript will directly interact with whichever library Photos is currently using.
|
- use_photos_export: (bool, default=False); if True will attempt to export photo via AppleScript or PhotoKit interaction with Photos
|
||||||
|
- download_missing: (bool, default=False); if True will attempt to export photo via AppleScript or PhotoKit interaction with Photos if missing
|
||||||
|
- use_photokit: (bool, default=True); if True will attempt to export photo via photokit instead of AppleScript when used with use_photos_export or download_missing
|
||||||
- timeout: (int, default=120) timeout in seconds used with use_photos_export
|
- timeout: (int, default=120) timeout in seconds used with use_photos_export
|
||||||
- exiftool: (boolean, default = False) if True, will use [exiftool](https://exiftool.org/) to write metadata directly to the exported photo; exiftool must be installed and in the system path
|
- exiftool: (bool, default = False) if True, will use [exiftool](https://exiftool.org/) to write metadata directly to the exported photo; exiftool must be installed and in the system path
|
||||||
- use_albums_as_keywords: (boolean, default = False); if True, will use album names as keywords when exporting metadata with exiftool or sidecar
|
- use_albums_as_keywords: (bool, default = False); if True, will use album names as keywords when exporting metadata with exiftool or sidecar
|
||||||
- use_persons_as_keywords: (boolean, default = False); if True, will use person names as keywords when exporting metadata with exiftool or sidecar
|
- use_persons_as_keywords: (bool, default = False); if True, will use person names as keywords when exporting metadata with exiftool or sidecar
|
||||||
|
|
||||||
Returns: list of paths to exported files. More than one file could be exported, for example if live_photo=True, both the original image and the associated .mov file will be exported
|
Returns: list of paths to exported files. More than one file could be exported, for example if live_photo=True, both the original image and the associated .mov file will be exported
|
||||||
|
|
||||||
@@ -2761,7 +2816,7 @@ If overwrite=False and increment=False, export will fail if destination file alr
|
|||||||
|
|
||||||
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
|
Render template string for photo. none_str is used if template substitution results in None value and no default specified.
|
||||||
|
|
||||||
- `template_str`: str in osxphotos template language (OTL) format. See also [Template System](#template-system) table. See notes below regarding specific details of the syntax.
|
- `template_str`: str in metadata template language (MTL) format. See also [Template System](#template-system) table. See notes below regarding specific details of the syntax.
|
||||||
- `options`: an optional osxphotos.phototemplate.RenderOptions object specifying the options to pass to the rendering engine.
|
- `options`: an optional osxphotos.phototemplate.RenderOptions object specifying the options to pass to the rendering engine.
|
||||||
|
|
||||||
`RenderOptions` has the following properties:
|
`RenderOptions` has the following properties:
|
||||||
@@ -2787,7 +2842,8 @@ Some substitutions, notably `album`, `keyword`, and `person` could return multip
|
|||||||
|
|
||||||
See [Template System](#template-system) for additional details.
|
See [Template System](#template-system) for additional details.
|
||||||
|
|
||||||
#### `detected_text(confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD)`
|
|
||||||
|
#### <a name="detected_text_method">`detected_text(confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD)`</a>
|
||||||
|
|
||||||
Detects text in photo and returns lists of results as (detected text, confidence)
|
Detects text in photo and returns lists of results as (detected text, confidence)
|
||||||
|
|
||||||
@@ -2797,7 +2853,9 @@ If photo is edited, uses the edited photo, otherwise the original; falls back to
|
|||||||
|
|
||||||
Returns: list of (detected text, confidence) tuples.
|
Returns: list of (detected text, confidence) tuples.
|
||||||
|
|
||||||
Note: This is *not* the same as Live Text in macOS Monterey. When using `detected_text()`, osxphotos will use Apple's [Vision framework](https://developer.apple.com/documentation/vision/recognizing_text_in_images?language=objc) to perform text detection on the image. On my circa 2013 MacBook Pro, this takes about 2 seconds per image. `detected_text()` does memoize the results for a given `confidence_threshold` so repeated calls will not re-process the photo.
|
Note: This is *not* the same as Live Text in macOS Monterey. When using `detected_text()`, osxphotos will use Apple's [Vision framework](https://developer.apple.com/documentation/vision/recognizing_text_in_images?language=objc) to perform text detection on the image. On my circa 2013 MacBook Pro, this takes about 2 seconds per image. `detected_text()` does memoize the results for a given `confidence_threshold` so repeated calls will not re-process the photo. This works only on macOS Catalina (10.15) or later.
|
||||||
|
|
||||||
|
See also [Text Detection](#textdetection).
|
||||||
|
|
||||||
### ExifInfo
|
### ExifInfo
|
||||||
[PhotosInfo.exif_info](#exif-info) returns an `ExifInfo` object with some EXIF data about the photo (Photos 5 only). `ExifInfo` contains the following properties:
|
[PhotosInfo.exif_info](#exif-info) returns an `ExifInfo` object with some EXIF data about the photo (Photos 5 only). `ExifInfo` contains the following properties:
|
||||||
@@ -2882,6 +2940,11 @@ Photos Library
|
|||||||
#### `parent`
|
#### `parent`
|
||||||
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
||||||
|
|
||||||
|
#### `owner`
|
||||||
|
Returns full name of the album owner (person who shared the album) for shared albums or None if album is not shared.
|
||||||
|
|
||||||
|
**Note**: *Only valid on Photos 5 / MacOS 10.15+; on Photos <= 4, returns None.
|
||||||
|
|
||||||
### ImportInfo
|
### ImportInfo
|
||||||
PhotosDB.import_info returns a list of ImportInfo objects. Each ImportInfo object represents an import session in the library. PhotoInfo.import_info returns a single ImportInfo object representing the import session for the photo (or `None` if no associated import session).
|
PhotosDB.import_info returns a list of ImportInfo objects. Each ImportInfo object represents an import session in the library. PhotoInfo.import_info returns a single ImportInfo object representing the import session for the photo (or `None` if no associated import session).
|
||||||
|
|
||||||
@@ -2902,6 +2965,23 @@ Returns the start date as a timezone aware datetime.datetime object for when the
|
|||||||
#### `end_date`
|
#### `end_date`
|
||||||
Returns the end date as a timezone aware datetime.datetime object for when the import session completed.
|
Returns the end date as a timezone aware datetime.datetime object for when the import session completed.
|
||||||
|
|
||||||
|
### ProjectInfo
|
||||||
|
PhotosDB.projcet_info returns a list of ProjectInfo objects. Each ProjectInfo object represents a project in the library. PhotoInfo.project_info returns a list of ProjectInfo objects for each project the photo is contained in.
|
||||||
|
|
||||||
|
Projects (found under "My Projects" in Photos) are projects or creations such as cards, calendars, and slideshows created in Photos. osxphotos provides only very basic information about projects and projects created with third party plugins may not accessible to osxphotos.
|
||||||
|
|
||||||
|
#### `uuid`
|
||||||
|
Returns the universally unique identifier (uuid) of the project. This is how Photos keeps track of individual objects within the database.
|
||||||
|
|
||||||
|
#### `title`
|
||||||
|
Returns the title or name of the project.
|
||||||
|
|
||||||
|
#### <a name="projectphotos">`photos`</a>
|
||||||
|
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the project.
|
||||||
|
|
||||||
|
#### `creation_date`
|
||||||
|
Returns the creation date as a timezone aware datetime.datetime object of the project.
|
||||||
|
|
||||||
### FolderInfo
|
### FolderInfo
|
||||||
PhotosDB.folder_info returns a list of FolderInfo objects representing the top level folders in the library. Each FolderInfo object represents a single folder in the Photos library.
|
PhotosDB.folder_info returns a list of FolderInfo objects representing the top level folders in the library. Each FolderInfo object represents a single folder in the Photos library.
|
||||||
|
|
||||||
@@ -3249,7 +3329,6 @@ The following additional properties are also available but are not yet fully doc
|
|||||||
- `manual`:
|
- `manual`:
|
||||||
- `face_type`:
|
- `face_type`:
|
||||||
- `age_type`:
|
- `age_type`:
|
||||||
- `bald_type`:
|
|
||||||
- `eye_makeup_type`:
|
- `eye_makeup_type`:
|
||||||
- `eye_state`:
|
- `eye_state`:
|
||||||
- `facial_hair_type`:
|
- `facial_hair_type`:
|
||||||
@@ -3332,7 +3411,7 @@ To get the path of every raw photo, whether it's a single raw photo or a raw+JPE
|
|||||||
### Template System
|
### Template System
|
||||||
|
|
||||||
<!-- OSXPHOTOS-TEMPLATE-HELP:START - Do not remove or modify this section -->
|
<!-- OSXPHOTOS-TEMPLATE-HELP:START - Do not remove or modify this section -->
|
||||||
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.
|
In its simplest form, a template statement has the form: `"{template_field}"`, for example `"{title}"` which would resolve to the title of the photo.
|
||||||
|
|
||||||
@@ -3553,10 +3632,13 @@ The following template field substitutions are availabe for use the templating s
|
|||||||
|{lf}|A line feed: '\n', alias for {newline}|
|
|{lf}|A line feed: '\n', alias for {newline}|
|
||||||
|{cr}|A carriage return: '\r'|
|
|{cr}|A carriage return: '\r'|
|
||||||
|{crlf}|a carriage return + line feed: '\r\n'|
|
|{crlf}|a carriage return + line feed: '\r\n'|
|
||||||
|{osxphotos_version}|The osxphotos version, e.g. '0.42.70'|
|
|{osxphotos_version}|The osxphotos version, e.g. '0.45.8'|
|
||||||
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|
||||||
|{album}|Album(s) photo is contained in|
|
|{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|
|
|{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|
|
|{keyword}|Keyword(s) assigned to photo|
|
||||||
|{person}|Person(s) / face(s) in a photo|
|
|{person}|Person(s) / face(s) in a photo|
|
||||||
|{label}|Image categorization label associated with a photo (Photos 5+ only). Labels are added automatically by Photos using machine learning algorithms to categorize images. These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.|
|
|{label}|Image categorization label associated with a photo (Photos 5+ only). Labels are added automatically by Photos using machine learning algorithms to categorize images. These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.|
|
||||||
@@ -3568,8 +3650,9 @@ The following template field substitutions are availabe for use the templating s
|
|||||||
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
|{searchinfo.venue}|Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||||
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
|{searchinfo.venue_type}|Venue types associated with a photo, e.g. 'Restaurant'; (Photos 5+ only, applied automatically by Photos' image categorization algorithms).|
|
||||||
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|
|{photo}|Provides direct access to the PhotoInfo object for the photo. Must be used in format '{photo.property}' where 'property' represents a PhotoInfo property. For example: '{photo.favorite}' is the same as '{favorite}' and '{photo.place.name}' is the same as '{place.name}'. '{photo}' provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.|
|
||||||
|{detected_text}|List of text strings found in the image after performing text detection. Using '{detected_text}' will cause osxphotos to perform text detection on your photos using the built-in macOS text detection algorithms which will slow down your export. The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; The default confidence threshold is 0.75. Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.|
|
|{detected_text}|List of text strings found in the image after performing text detection. Using '{detected_text}' will cause osxphotos to perform text detection on your photos using the built-in macOS text detection algorithms which will slow down your export. The results for each photo will be cached in the export database so that future exports with '--update' do not need to reprocess each photo. You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in '{detected_text:0.5}'; The default confidence threshold is 0.75. '{detected_text}' works only on macOS Catalina (10.15) or later. Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.|
|
||||||
|{shell_quote}|Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.|
|
|{shell_quote}|Use in form '{shell_quote,TEMPLATE}'; quotes the rendered TEMPLATE value(s) for safe usage in the shell, e.g. My file.jpeg => 'My file.jpeg'; only adds quotes if needed.|
|
||||||
|
|{strip}|Use in form '{strip,TEMPLATE}'; strips whitespace from begining and end of rendered TEMPLATE value(s).|
|
||||||
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
|
|{function}|Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where 'file.py' is the name of the python file and 'function_name' is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.|
|
||||||
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
|
<!-- OSXPHOTOS-TEMPLATE-TABLE:END -->
|
||||||
|
|
||||||
@@ -3633,6 +3716,113 @@ osxphotos.exiftool also provides an `ExifToolCaching` class which caches all met
|
|||||||
|
|
||||||
`ExifTool()` runs `exiftool` as a subprocess using the `-stay_open True` flag to keep the process running in the background. The subprocess will be cleaned up when your main script terminates. `ExifTool()` uses a singleton pattern to ensure that only one instance of `exiftool` is created. Multiple instances of `ExifTool()` will all use the same `exiftool` subprocess.
|
`ExifTool()` runs `exiftool` as a subprocess using the `-stay_open True` flag to keep the process running in the background. The subprocess will be cleaned up when your main script terminates. `ExifTool()` uses a singleton pattern to ensure that only one instance of `exiftool` is created. Multiple instances of `ExifTool()` will all use the same `exiftool` subprocess.
|
||||||
|
|
||||||
|
### <a name="photoexporter">PhotoExporter</a>
|
||||||
|
|
||||||
|
[PhotoInfo.export()](#photoinfo) provides a simple method to export a photo. This method actually calls `PhotoExporter.export()` to do the export. `PhotoExporter` provides many more options to configure the export and report results and this is what the osxphotos command line export tools uses.
|
||||||
|
|
||||||
|
#### `export(dest, filename=None, options: Optional[ExportOptions]=None) -> ExportResults`
|
||||||
|
|
||||||
|
Export a photo.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
|
||||||
|
- dest: must be valid destination path or exception raised
|
||||||
|
- filename: (optional): name of exported picture; if not provided, will use current filename
|
||||||
|
- options (ExportOptions): optional ExportOptions instance
|
||||||
|
|
||||||
|
Returns: ExportResults instance
|
||||||
|
|
||||||
|
*Note*: to use dry run mode, you must set options.dry_run=True and also pass in memory version of export_db, and no-op fileutil (e.g. ExportDBInMemory and FileUtilNoOp) in options.export_db and options.fileutil respectively.
|
||||||
|
|
||||||
|
#### `ExportOptions`
|
||||||
|
|
||||||
|
Options class for exporting photos with `export`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
|
||||||
|
- convert_to_jpeg (bool): if True, converts non-jpeg images to jpeg
|
||||||
|
- description_template (str): optional template string that will be rendered for use as photo description
|
||||||
|
- download_missing: (bool, default=False): if True will attempt to export photo via applescript interaction with Photos if missing (see also use_photokit, use_photos_export)
|
||||||
|
- dry_run: (bool, default=False): set to True to run in "dry run" mode
|
||||||
|
- edited: (bool, default=False): if True will export the edited version of the photo otherwise exports the original version
|
||||||
|
- exiftool_flags (list of str): optional list of flags to pass to exiftool when using exiftool option, e.g ["-m", "-F"]
|
||||||
|
- exiftool: (bool, default = False): if True, will use exiftool to write metadata to export file
|
||||||
|
- export_as_hardlink: (bool, default=False): if True, will hardlink files instead of copying them
|
||||||
|
- export_db: (ExportDB_ABC): instance of a class that conforms to ExportDB_ABC with methods for getting/setting data related to exported files to compare update state
|
||||||
|
- fileutil: (FileUtilABC): class that conforms to FileUtilABC with various file utilities
|
||||||
|
- ignore_date_modified (bool): for use with sidecar and exiftool; if True, sets EXIF:ModifyDate to EXIF:DateTimeOriginal even if date_modified is set
|
||||||
|
- ignore_signature (bool, default=False): ignore file signature when used with update (look only at filename)
|
||||||
|
- increment (bool, default=True): if True, will increment file name until a non-existant name is found if overwrite=False and increment=False, export will fail if destination file already exists
|
||||||
|
- jpeg_ext (str): if set, will use this value for extension on jpegs converted to jpeg with convert_to_jpeg; if not set, uses jpeg; do not include the leading "."
|
||||||
|
- jpeg_quality (float in range 0.0 <= jpeg_quality <= 1.0): a value of 1.0 specifies use best quality, a value of 0.0 specifies use maximum compression.
|
||||||
|
- keyword_template (list of str): list of template strings that will be rendered as used as keywords
|
||||||
|
- live_photo (bool, default=False): if True, will also export the associated .mov for live photos
|
||||||
|
- location (bool): if True, include location in exported metadata
|
||||||
|
- merge_exif_keywords (bool): if True, merged keywords found in file's exif data (requires exiftool)
|
||||||
|
- merge_exif_persons (bool): if True, merged persons found in file's exif data (requires exiftool)
|
||||||
|
- overwrite (bool, default=False): if True will overwrite files if they already exist
|
||||||
|
- persons (bool): if True, include persons in exported metadata
|
||||||
|
- preview_suffix (str): optional string to append to end of filename for preview images
|
||||||
|
- preview (bool): if True, also exports preview image
|
||||||
|
- raw_photo (bool, default=False): if True, will also export the associated RAW photo
|
||||||
|
- render_options (RenderOptions): optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates
|
||||||
|
- replace_keywords (bool): if True, keyword_template replaces any keywords, otherwise it's additive
|
||||||
|
- sidecar_drop_ext (bool, default=False): if True, drops the photo's extension from sidecar filename (e.g. 'IMG_1234.json' instead of 'IMG_1234.JPG.json')
|
||||||
|
- sidecar: bit field (int): set to one or more of SIDECAR_XMP, SIDECAR_JSON, SIDECAR_EXIFTOOL
|
||||||
|
- SIDECAR_JSON: if set will write a json sidecar with data in format readable by exiftool sidecar filename will be dest/filename.json; includes exiftool tag group names (e.g. `exiftool -G -j`)
|
||||||
|
- SIDECAR_EXIFTOOL: if set will write a json sidecar with data in format readable by exiftool sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. `exiftool -j`)
|
||||||
|
- SIDECAR_XMP: if set will write an XMP sidecar with IPTC data sidecar filename will be dest/filename.xmp
|
||||||
|
- strip (bool): if True, strip whitespace from rendered templates
|
||||||
|
- timeout (int, default=120): timeout in seconds used with use_photos_export
|
||||||
|
- touch_file (bool, default=False): if True, sets file's modification time upon photo date
|
||||||
|
- update (bool, default=False): if True export will run in update mode, that is, it will not export the photo if the current version already exists in the destination
|
||||||
|
- use_albums_as_keywords (bool, default = False): if True, will include album names in keywords when exporting metadata with exiftool or sidecar
|
||||||
|
- use_persons_as_keywords (bool, default = False): if True, will include person names in keywords when exporting metadata with exiftool or sidecar
|
||||||
|
- use_photos_export (bool, default=False): if True will attempt to export photo via applescript interaction with Photos even if not missing (see also use_photokit, download_missing)
|
||||||
|
- use_photokit (bool, default=False): if True, will use photokit to export photos when use_photos_export is True
|
||||||
|
- verbose (Callable): optional callable function to use for printing verbose text during processing; if None (default), does not print output.
|
||||||
|
|
||||||
|
#### `ExportResults`
|
||||||
|
|
||||||
|
`PhotoExporter().export()` returns an instance of this class.
|
||||||
|
|
||||||
|
`ExportResults` has the following properties:
|
||||||
|
|
||||||
|
- exported: list of all exported files (A single call to export could export more than one file, e.g. original file, preview, live video, raw, etc.)
|
||||||
|
- new: list of new files exported when used with update=True
|
||||||
|
- updated: list of updated files when used with update=True
|
||||||
|
- skipped: list of skipped files when used with update=True
|
||||||
|
- exif_updated: list of updated files when used with update=True and exiftool
|
||||||
|
- touched: list of files touched during export (e.g. file date/time updated with touch_file=True)
|
||||||
|
- to_touch: Reserved for internal use of export
|
||||||
|
- converted_to_jpeg: list of files converted to jpeg when convert_to_jpeg=True
|
||||||
|
- sidecar_json_written: list of JSON sidecars written
|
||||||
|
- sidecar_json_skipped: list of JSON sidecars skipped when update=True
|
||||||
|
- sidecar_exiftool_written: list of exiftool sidecars written
|
||||||
|
- sidecar_exiftool_skipped: list of exiftool sidecars skipped when update=True
|
||||||
|
- sidecar_xmp_written: list of XMP sidecars written
|
||||||
|
- sidecar_xmp_skipped: list of XMP sidecars skipped when update=True
|
||||||
|
- missing: list of missing files
|
||||||
|
- error: list of tuples containing (filename, error) if error generated during export
|
||||||
|
- exiftool_warning: list of warnings generated by exiftool during export
|
||||||
|
- exiftool_error: list of errors generated by exiftool during export
|
||||||
|
- xattr_written: list of files with extended attributes written during export
|
||||||
|
- xattr_skipped: list of files where extended attributes were skipped when update=True
|
||||||
|
- deleted_files: reserved for use by osxphotos CLI
|
||||||
|
- deleted_directories: reserved for use by osxphotos CLI
|
||||||
|
- exported_album: reserved for use by osxphotos CLI
|
||||||
|
- skipped_album: reserved for use by osxphotos CLI
|
||||||
|
- missing_album: reserved for use by osxphotos CLI
|
||||||
|
|
||||||
|
|
||||||
|
### <a name="textdetection">Text Detection</a>
|
||||||
|
|
||||||
|
The [PhotoInfo.detected_text()](#detected_text_method) and the `{detected_text}` template will perform text detection on the photos in your library. Text detection is a slow process so to avoid unnecessary re-processing of photos, osxphotos will cache the results of the text detection process as an extended attribute on the photo image file. Extended attributes do not modify the actual file. The extended attribute is named `osxphotos.metadata:detected_text` and can be viewed using the built-in [xattr](https://ss64.com/osx/xattr.html) command or my [osxmetadata](https://github.com/RhetTbull/osxmetadata) tool. If you want to remove the cached attribute, you can do so with osxmetadata as follows:
|
||||||
|
|
||||||
|
`osxmetadata --clear osxphotos.metadata:detected_text --walk ~/Pictures/Photos\ Library.photoslibrary/`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Utility Functions
|
### Utility Functions
|
||||||
|
|
||||||
The following functions are located in osxphotos.utils
|
The following functions are located in osxphotos.utils
|
||||||
@@ -3649,13 +3839,6 @@ Returns path to last opened Photo Library as string.
|
|||||||
|
|
||||||
Returns list of Photos libraries found on the system. **Note**: On MacOS 10.15, this appears to list all libraries. On older systems, it may not find some libraries if they are not located in ~/Pictures. Provided for convenience but do not rely on this to find all libraries on the system.
|
Returns list of Photos libraries found on the system. **Note**: On MacOS 10.15, this appears to list all libraries. On older systems, it may not find some libraries if they are not located in ~/Pictures. Provided for convenience but do not rely on this to find all libraries on the system.
|
||||||
|
|
||||||
#### `dd_to_dms_str(lat, lon)`
|
|
||||||
Convert latitude, longitude in degrees to degrees, minutes, seconds as string.
|
|
||||||
- `lat`: latitude in degrees
|
|
||||||
- `lon`: longitude in degrees
|
|
||||||
returns: string tuple in format ("51 deg 30' 12.86\\" N", "0 deg 7' 54.50\\" W")
|
|
||||||
This is the same format used by exiftool's json format.
|
|
||||||
|
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -3711,15 +3894,10 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
## Related Projects
|
## Related Projects
|
||||||
|
|
||||||
- [rhettbull/photosmeta](https://github.com/rhettbull/photosmeta): uses osxphotos and [exiftool](https://exiftool.org/) to apply metadata from Photos as exif data in the photo files. Can also export photos while preserving metadata and also apply Photos keywords as spotlight tags to make it easier to search for photos using spotlight. This is mostly made obsolete by osxphotos. The one feature that photosmeta has that osxphotos does not is ability to update the metadata of the actual photo files in the Photos library without exporting them. (Use with caution!)
|
- [rhettbull/exif2findertags](https://github.com/RhetTbull/exif2findertags): Read EXIF metadata from image and video files and convert it to macOS Finder tags and/or Finder comments and other extended attributes.
|
||||||
|
- [rhettbull/photos_time_warp](https://github.com/RhetTbull/photos_time_warp): Batch adjust the date, time, or timezone of photos in Apple Photos.
|
||||||
- [rhettbull/PhotoScript](https://github.com/RhetTbull/PhotoScript): python wrapper around Photos' applescript API allowing automation of Photos (including creation/deletion of items) from python.
|
- [rhettbull/PhotoScript](https://github.com/RhetTbull/PhotoScript): python wrapper around Photos' applescript API allowing automation of Photos (including creation/deletion of items) from python.
|
||||||
- [patrikhson/photo-export](https://github.com/patrikhson/photo-export): Exports older versions of Photos databases. Provided the inspiration for osxphotos.
|
- [ndbroadbent/icloud_photos_downloader](https://github.com/ndbroadbent/icloud_photos_downloader): Download photos from iCloud.
|
||||||
- [doersino/apple-photos-export](https://github.com/doersino/apple-photos-export): Photos export script for Mojave.
|
|
||||||
- [orangeturtle739/photos-export](https://github.com/orangeturtle739/photos-export): Set of scripts to export Photos libraries.
|
|
||||||
- [ndbroadbent/icloud_photos_downloader](https://github.com/ndbroadbent/icloud_photos_downloader): Download photos from iCloud. Currently unmaintained.
|
|
||||||
- [AaronVanGeffen/ExportPhotosLibrary](https://github.com/AaronVanGeffen/ExportPhotosLibrary): Another python script for exporting older versions of Photos libraries.
|
|
||||||
- [MossieurPropre/PhotosAlbumExporter](https://github.com/MossieurPropre/PhotosAlbumExporter): Javascript script to export photos while maintaining album structure.
|
|
||||||
- [ajslater/magritte](https://github.com/ajslater/magritte): Another python command line script for exporting photos from older versions of Photos libraries.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -3772,6 +3950,17 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center"><a href="https://github.com/kaduskj"><img src="https://avatars.githubusercontent.com/u/983067?v=4?s=75" width="75px;" alt=""/><br /><sub><b>kaduskj</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Akaduskj" title="Bug reports">🐛</a></td>
|
<td align="center"><a href="https://github.com/kaduskj"><img src="https://avatars.githubusercontent.com/u/983067?v=4?s=75" width="75px;" alt=""/><br /><sub><b>kaduskj</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Akaduskj" title="Bug reports">🐛</a></td>
|
||||||
<td align="center"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt=""/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a> <a href="#example-mkirkland4874" title="Examples">💡</a></td>
|
<td align="center"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt=""/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a> <a href="#example-mkirkland4874" title="Examples">💡</a></td>
|
||||||
<td align="center"><a href="https://github.com/jcommisso07"><img src="https://avatars.githubusercontent.com/u/3111054?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Joseph Commisso</b></sub></a><br /><a href="#data-jcommisso07" title="Data">🔣</a></td>
|
<td align="center"><a href="https://github.com/jcommisso07"><img src="https://avatars.githubusercontent.com/u/3111054?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Joseph Commisso</b></sub></a><br /><a href="#data-jcommisso07" title="Data">🔣</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/dssinger"><img src="https://avatars.githubusercontent.com/u/1817903?v=4?s=75" width="75px;" alt=""/><br /><sub><b>David Singer</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Adssinger" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/oPromessa"><img src="https://avatars.githubusercontent.com/u/21261491?v=4?s=75" width="75px;" alt=""/><br /><sub><b>oPromessa</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3AoPromessa" title="Bug reports">🐛</a> <a href="#ideas-oPromessa" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=oPromessa" title="Tests">⚠️</a></td>
|
||||||
|
<td align="center"><a href="http://spencerchang.me"><img src="https://avatars.githubusercontent.com/u/14796580?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Spencer Chang</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aspencerc99" title="Bug reports">🐛</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><a href="https://www.cs.purdue.edu/homes/dgleich"><img src="https://avatars.githubusercontent.com/u/33995?v=4?s=75" width="75px;" alt=""/><br /><sub><b>David Gleich</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dgleich" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://alandefreitas.github.io/alandefreitas/"><img src="https://avatars.githubusercontent.com/u/5369819?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Alan de Freitas</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aalandefreitas" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center"><a href="https://hyfen.net"><img src="https://avatars.githubusercontent.com/u/6291?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Andrew Louis</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Code">💻</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/neebah"><img src="https://avatars.githubusercontent.com/u/71442026?v=4?s=75" width="75px;" alt=""/><br /><sub><b>neebah</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aneebah" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/ahti123"><img src="https://avatars.githubusercontent.com/u/22232632?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Ahti Liin</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=ahti123" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aahti123" title="Bug reports">🐛</a></td>
|
||||||
|
<td align="center"><a href="https://github.com/xwu64"><img src="https://avatars.githubusercontent.com/u/10580396?v=4?s=75" width="75px;" alt=""/><br /><sub><b>Xiaoliang Wu</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=xwu64" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@@ -3788,7 +3977,6 @@ My goal is make osxphotos as reliable and comprehensive as possible. The test s
|
|||||||
|
|
||||||
- Audio-only files are not handled. It is possible to store audio-only files in Photos. osxphotos currently only handles images and videos. See [Issue #436](https://github.com/RhetTbull/osxphotos/issues/436)
|
- Audio-only files are not handled. It is possible to store audio-only files in Photos. osxphotos currently only handles images and videos. See [Issue #436](https://github.com/RhetTbull/osxphotos/issues/436)
|
||||||
- Face coordinates (mouth, left eye, right eye) may not be correct for images where the head is tilted. See [Issue #196](https://github.com/RhetTbull/osxphotos/issues/196).
|
- Face coordinates (mouth, left eye, right eye) may not be correct for images where the head is tilted. See [Issue #196](https://github.com/RhetTbull/osxphotos/issues/196).
|
||||||
- Raw images imported to Photos with an associated jpeg preview are not handled correctly by osxphotos. osxphotos query and export will operate on the jpeg preview instead of the raw image as will `PhotoInfo.path`. If the user selects "Use RAW as original" in Photos, the raw image will be exported or operated on but the jpeg will be ignored. See [Issue #101](https://github.com/RhetTbull/osxphotos/issues/101). Note: Beta version of fix for this bug is implemented in the current version of osxphotos.
|
|
||||||
- The `--download-missing` option for `osxphotos export` does not work correctly with burst images. It will download the primary image but not the other burst images. See [Issue #75](https://github.com/RhetTbull/osxphotos/issues/75).
|
- The `--download-missing` option for `osxphotos export` does not work correctly with burst images. It will download the primary image but not the other burst images. See [Issue #75](https://github.com/RhetTbull/osxphotos/issues/75).
|
||||||
|
|
||||||
## Implementation Notes
|
## Implementation Notes
|
||||||
@@ -3813,6 +4001,8 @@ For additional details about how osxphotos is implemented or if you would like t
|
|||||||
- [textx](https://github.com/textX/textX)
|
- [textx](https://github.com/textX/textX)
|
||||||
- [bitmath](https://github.com/tbielawa/bitmath)
|
- [bitmath](https://github.com/tbielawa/bitmath)
|
||||||
- [more-itertools](https://github.com/more-itertools/more-itertools)
|
- [more-itertools](https://github.com/more-itertools/more-itertools)
|
||||||
|
- [ptpython](https://github.com/prompt-toolkit/ptpython)
|
||||||
|
- [objexplore](https://github.com/kylepollina/objexplore)
|
||||||
|
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|||||||
5
build.sh
@@ -3,9 +3,10 @@
|
|||||||
# script to help build osxphotos release
|
# script to help build osxphotos release
|
||||||
# this is unique to my own dev setup
|
# this is unique to my own dev setup
|
||||||
|
|
||||||
source venv/bin/activate
|
# source venv/bin/activate
|
||||||
rm -rf dist; rm -rf build
|
rm -rf dist; rm -rf build
|
||||||
python3 utils/update_readme.py
|
python3 utils/update_readme.py
|
||||||
(cd docsrc && make github && make pdf)
|
(cd docsrc && make github && make pdf)
|
||||||
python3 setup.py sdist bdist_wheel
|
# python3 setup.py sdist bdist_wheel
|
||||||
|
python3 -m build
|
||||||
./make_cli_exe.sh
|
./make_cli_exe.sh
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
build
|
||||||
|
m2r2
|
||||||
|
pyinstaller==4.4
|
||||||
|
pytest-mock
|
||||||
pytest==6.2.4
|
pytest==6.2.4
|
||||||
pytest-mock==3.6.1
|
sphinx_click
|
||||||
Sphinx==4.0.2
|
sphinx_rtd_theme
|
||||||
sphinx-rtd-theme==0.5.2
|
twine
|
||||||
wheel==0.36.2
|
wheel
|
||||||
twine==3.4.1
|
Sphinx
|
||||||
pyinstaller==4.3
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Sphinx build info version 1
|
# 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.
|
# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done.
|
||||||
config: 23e7c9cd300c96ffa7fce04034b83f61
|
config: bf43bf49b725c31ce72a8823e4f8012b
|
||||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Overview: module code — osxphotos 0.42.69 documentation</title>
|
<title>Overview: module code — osxphotos 0.43.9 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../_static/alabaster.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 data-url_root="../" id="documentation_options" src="../_static/documentation_options.js"></script>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
<h3 id="searchlabel">Quick search</h3>
|
<h3 id="searchlabel">Quick search</h3>
|
||||||
<div class="searchformwrapper">
|
<div class="searchformwrapper">
|
||||||
<form class="search" action="../search.html" method="get">
|
<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" />
|
<input type="submit" value="Go" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.2</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.42.69 documentation</title>
|
<title>osxphotos.photoinfo._photoinfo_export — osxphotos 0.43.6 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.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 data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">RenderOptions</span>
|
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">RenderOptions</span>
|
||||||
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span>
|
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span>
|
||||||
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">findfiles</span><span class="p">,</span> <span class="n">lineno</span><span class="p">,</span> <span class="n">noop</span>
|
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">increment_filename</span><span class="p">,</span> <span class="n">increment_filename_with_count</span><span class="p">,</span> <span class="n">lineno</span>
|
||||||
|
|
||||||
<span class="c1"># retry if use_photos_export fails the first time (which sometimes it does)</span>
|
<span class="c1"># retry if use_photos_export fails the first time (which sometimes it does)</span>
|
||||||
<span class="n">MAX_PHOTOSCRIPT_RETRIES</span> <span class="o">=</span> <span class="mi">3</span>
|
<span class="n">MAX_PHOTOSCRIPT_RETRIES</span> <span class="o">=</span> <span class="mi">3</span>
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
|
|
||||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">exported_files</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">filename</span><span class="p">:</span>
|
<span class="k">if</span> <span class="ow">not</span> <span class="n">exported_files</span> <span class="ow">or</span> <span class="ow">not</span> <span class="n">filename</span><span class="p">:</span>
|
||||||
<span class="c1"># nothing got exported</span>
|
<span class="c1"># nothing got exported</span>
|
||||||
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not export photo </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not export photo </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)"</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="c1"># need to find actual filename as sometimes Photos renames JPG to jpeg on export</span>
|
<span class="c1"># need to find actual filename as sometimes Photos renames JPG to jpeg on export</span>
|
||||||
<span class="c1"># may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)</span>
|
<span class="c1"># may be more than one file exported (e.g. if Live Photo, Photos exports both .jpeg and .mov)</span>
|
||||||
@@ -563,6 +563,7 @@
|
|||||||
<span class="n">preview</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
<span class="n">preview</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
<span class="n">preview_suffix</span><span class="o">=</span><span class="n">DEFAULT_PREVIEW_SUFFIX</span><span class="p">,</span>
|
<span class="n">preview_suffix</span><span class="o">=</span><span class="n">DEFAULT_PREVIEW_SUFFIX</span><span class="p">,</span>
|
||||||
<span class="n">render_options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
|
<span class="n">render_options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
<span class="p">):</span>
|
<span class="p">):</span>
|
||||||
<span class="sd">"""export photo, like export but with update and dry_run options</span>
|
<span class="sd">"""export photo, like export but with update and dry_run options</span>
|
||||||
<span class="sd"> dest: must be valid destination path or exception raised</span>
|
<span class="sd"> dest: must be valid destination path or exception raised</span>
|
||||||
@@ -621,6 +622,7 @@
|
|||||||
<span class="sd"> preview: if True, also exports preview image</span>
|
<span class="sd"> preview: if True, also exports preview image</span>
|
||||||
<span class="sd"> preview_suffix: optional string to append to end of filename for preview images</span>
|
<span class="sd"> preview_suffix: optional string to append to end of filename for preview images</span>
|
||||||
<span class="sd"> render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates</span>
|
<span class="sd"> render_options: optional osxphotos.phototemplate.RenderOptions instance to specify options for rendering templates</span>
|
||||||
|
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
|
||||||
|
|
||||||
<span class="sd"> Returns: ExportResults class</span>
|
<span class="sd"> Returns: ExportResults class</span>
|
||||||
<span class="sd"> ExportResults has attributes:</span>
|
<span class="sd"> ExportResults has attributes:</span>
|
||||||
@@ -714,15 +716,12 @@
|
|||||||
<span class="c1"># e.g. exporting sidecar for file1.png and file1.jpeg</span>
|
<span class="c1"># e.g. exporting sidecar for file1.png and file1.jpeg</span>
|
||||||
<span class="c1"># if file1.png exists and exporting file1.jpeg,</span>
|
<span class="c1"># if file1.png exists and exporting file1.jpeg,</span>
|
||||||
<span class="c1"># dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision</span>
|
<span class="c1"># dest will be file1 (1).jpeg even though file1.jpeg doesn't exist to prevent sidecar collision</span>
|
||||||
<span class="n">count</span> <span class="o">=</span> <span class="mi">0</span>
|
<span class="n">increment_file_count</span> <span class="o">=</span> <span class="mi">0</span>
|
||||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
|
||||||
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">findfiles</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">*"</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest_original</span><span class="o">.</span><span class="n">parent</span><span class="p">))</span>
|
<span class="n">dest_original</span><span class="p">,</span> <span class="n">increment_file_count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span>
|
||||||
<span class="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
|
<span class="n">dest_original</span>
|
||||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span>
|
<span class="p">)</span>
|
||||||
<span class="k">while</span> <span class="n">dest_new</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
|
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest_original</span><span class="p">)</span>
|
||||||
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
|
|
||||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest_original</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)"</span>
|
|
||||||
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">dest_original</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}{</span><span class="n">dest_original</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">"</span>
|
|
||||||
|
|
||||||
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
|
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
|
||||||
<span class="k">if</span> <span class="p">(</span>
|
<span class="k">if</span> <span class="p">(</span>
|
||||||
@@ -737,17 +736,11 @@
|
|||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="n">export_edited</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">export_edited</span><span class="p">:</span>
|
||||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">increment</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span><span class="p">:</span>
|
||||||
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">findfiles</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest_edited</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">*"</span><span class="p">,</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest_edited</span><span class="o">.</span><span class="n">parent</span><span class="p">))</span>
|
<span class="n">dest_edited</span><span class="p">,</span> <span class="n">increment_file_count</span> <span class="o">=</span> <span class="n">increment_filename_with_count</span><span class="p">(</span>
|
||||||
<span class="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
|
<span class="n">dest_edited</span><span class="p">,</span> <span class="n">increment_file_count</span>
|
||||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">stem</span>
|
<span class="p">)</span>
|
||||||
<span class="k">if</span> <span class="n">count</span><span class="p">:</span>
|
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">dest_edited</span><span class="p">)</span>
|
||||||
<span class="c1"># incremented above when checking original destination</span>
|
|
||||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)"</span>
|
|
||||||
<span class="k">while</span> <span class="n">dest_new</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
|
|
||||||
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
|
|
||||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)"</span>
|
|
||||||
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}{</span><span class="n">dest_edited</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">"</span>
|
|
||||||
|
|
||||||
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
|
<span class="c1"># if overwrite==False and #increment==False, export should fail if file exists</span>
|
||||||
<span class="k">if</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">exists</span><span class="p">()</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">increment</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">dest_edited</span><span class="o">.</span><span class="n">exists</span><span class="p">()</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">update</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">overwrite</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">increment</span><span class="p">:</span>
|
||||||
@@ -831,20 +824,16 @@
|
|||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
|
||||||
<span class="c1"># not the right file, find the right one</span>
|
<span class="c1"># not the right file, find the right one</span>
|
||||||
<span class="n">count</span> <span class="o">=</span> <span class="mi">1</span>
|
|
||||||
<span class="n">glob_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (*</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
<span class="n">glob_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (*</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">glob_str</span><span class="p">)</span>
|
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">glob_str</span><span class="p">)</span>
|
||||||
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">False</span>
|
|
||||||
<span class="k">for</span> <span class="n">file_</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
|
<span class="k">for</span> <span class="n">file_</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
|
||||||
<span class="n">dest_uuid</span> <span class="o">=</span> <span class="n">export_db</span><span class="o">.</span><span class="n">get_uuid_for_file</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
|
<span class="n">dest_uuid</span> <span class="o">=</span> <span class="n">export_db</span><span class="o">.</span><span class="n">get_uuid_for_file</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
|
||||||
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">dest_uuid</span> <span class="o">==</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">:</span>
|
||||||
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
|
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
|
||||||
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">True</span>
|
|
||||||
<span class="k">break</span>
|
<span class="k">break</span>
|
||||||
<span class="k">elif</span> <span class="n">dest_uuid</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">cmp</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">file_</span><span class="p">):</span>
|
<span class="k">elif</span> <span class="n">dest_uuid</span> <span class="ow">is</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">cmp</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">file_</span><span class="p">):</span>
|
||||||
<span class="c1"># files match, update the UUID</span>
|
<span class="c1"># files match, update the UUID</span>
|
||||||
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
|
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">file_</span><span class="p">)</span>
|
||||||
<span class="n">found_match</span> <span class="o">=</span> <span class="kc">True</span>
|
|
||||||
<span class="n">export_db</span><span class="o">.</span><span class="n">set_data</span><span class="p">(</span>
|
<span class="n">export_db</span><span class="o">.</span><span class="n">set_data</span><span class="p">(</span>
|
||||||
<span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="p">,</span>
|
<span class="n">filename</span><span class="o">=</span><span class="n">dest</span><span class="p">,</span>
|
||||||
<span class="n">uuid</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
<span class="n">uuid</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
||||||
@@ -856,18 +845,14 @@
|
|||||||
<span class="n">exif_json</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
<span class="n">exif_json</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="k">break</span>
|
<span class="k">break</span>
|
||||||
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">found_match</span><span class="p">:</span>
|
|
||||||
<span class="c1"># increment the destination file</span>
|
<span class="c1"># increment the destination file</span>
|
||||||
<span class="n">count</span> <span class="o">=</span> <span class="mi">1</span>
|
<span class="n">dest</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">increment_filename</span><span class="p">(</span><span class="n">dest</span><span class="p">))</span>
|
||||||
<span class="n">glob_str</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">*"</span><span class="p">)</span>
|
|
||||||
<span class="n">dest_files</span> <span class="o">=</span> <span class="n">glob</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="n">glob_str</span><span class="p">)</span>
|
<span class="k">if</span> <span class="n">export_original</span><span class="p">:</span>
|
||||||
<span class="n">dest_files</span> <span class="o">=</span> <span class="p">[</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">f</span><span class="p">)</span><span class="o">.</span><span class="n">stem</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">]</span>
|
<span class="n">dest_original</span> <span class="o">=</span> <span class="n">dest</span>
|
||||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">stem</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="k">while</span> <span class="n">dest_new</span> <span class="ow">in</span> <span class="n">dest_files</span><span class="p">:</span>
|
<span class="n">dest_edited</span> <span class="o">=</span> <span class="n">dest</span>
|
||||||
<span class="n">dest_new</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">count</span><span class="si">}</span><span class="s2">)"</span>
|
|
||||||
<span class="n">count</span> <span class="o">+=</span> <span class="mi">1</span>
|
|
||||||
<span class="n">dest</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest_new</span><span class="si">}{</span><span class="n">dest</span><span class="o">.</span><span class="n">suffix</span><span class="si">}</span><span class="s2">"</span>
|
|
||||||
|
|
||||||
<span class="c1"># export the dest file</span>
|
<span class="c1"># export the dest file</span>
|
||||||
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
|
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
|
||||||
@@ -960,6 +945,14 @@
|
|||||||
<span class="n">preview_path</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_derivatives</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
|
<span class="n">preview_path</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path_derivatives</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
|
||||||
<span class="n">preview_ext</span> <span class="o">=</span> <span class="n">preview_path</span><span class="o">.</span><span class="n">suffix</span>
|
<span class="n">preview_ext</span> <span class="o">=</span> <span class="n">preview_path</span><span class="o">.</span><span class="n">suffix</span>
|
||||||
<span class="n">preview_name</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}{</span><span class="n">preview_suffix</span><span class="si">}{</span><span class="n">preview_ext</span><span class="si">}</span><span class="s2">"</span>
|
<span class="n">preview_name</span> <span class="o">=</span> <span class="n">dest</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">dest</span><span class="o">.</span><span class="n">stem</span><span class="si">}{</span><span class="n">preview_suffix</span><span class="si">}{</span><span class="n">preview_ext</span><span class="si">}</span><span class="s2">"</span>
|
||||||
|
<span class="c1"># if original is missing, the filename won't have been incremented so</span>
|
||||||
|
<span class="c1"># need to check here to make sure there aren't duplicate preview files in</span>
|
||||||
|
<span class="c1"># the export directory</span>
|
||||||
|
<span class="n">preview_name</span> <span class="o">=</span> <span class="p">(</span>
|
||||||
|
<span class="n">preview_name</span>
|
||||||
|
<span class="k">if</span> <span class="n">overwrite</span> <span class="ow">or</span> <span class="n">update</span>
|
||||||
|
<span class="k">else</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">increment_filename</span><span class="p">(</span><span class="n">preview_name</span><span class="p">))</span>
|
||||||
|
<span class="p">)</span>
|
||||||
<span class="k">if</span> <span class="n">preview_path</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">preview_path</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||||
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
|
<span class="n">results</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_export_photo</span><span class="p">(</span>
|
||||||
<span class="n">preview_path</span><span class="p">,</span>
|
<span class="n">preview_path</span><span class="p">,</span>
|
||||||
@@ -1002,6 +995,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
|
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
|
||||||
<span class="p">(</span>
|
<span class="p">(</span>
|
||||||
@@ -1028,6 +1022,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
|
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
|
||||||
<span class="p">(</span>
|
<span class="p">(</span>
|
||||||
@@ -1050,6 +1045,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
|
<span class="n">sidecars</span><span class="o">.</span><span class="n">append</span><span class="p">(</span>
|
||||||
<span class="p">(</span>
|
<span class="p">(</span>
|
||||||
@@ -1120,6 +1116,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
|
<span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
|
||||||
<span class="k">if</span> <span class="n">old_data</span> <span class="o">!=</span> <span class="n">current_data</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">old_data</span> <span class="o">!=</span> <span class="n">current_data</span><span class="p">:</span>
|
||||||
@@ -1143,6 +1140,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
|
||||||
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
|
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
|
||||||
@@ -1163,6 +1161,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">),</span>
|
<span class="p">),</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
|
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
|
||||||
@@ -1188,6 +1187,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">warning_</span><span class="p">:</span>
|
||||||
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
|
<span class="n">all_results</span><span class="o">.</span><span class="n">exiftool_warning</span><span class="o">.</span><span class="n">append</span><span class="p">((</span><span class="n">exported_file</span><span class="p">,</span> <span class="n">warning_</span><span class="p">))</span>
|
||||||
@@ -1208,6 +1208,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">),</span>
|
<span class="p">),</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
|
<span class="n">export_db</span><span class="o">.</span><span class="n">set_stat_exif_for_file</span><span class="p">(</span>
|
||||||
@@ -1298,6 +1299,7 @@
|
|||||||
<span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
|
<span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
|
||||||
<span class="n">version</span><span class="o">=</span><span class="n">PHOTOS_VERSION_CURRENT</span><span class="p">,</span>
|
<span class="n">version</span><span class="o">=</span><span class="n">PHOTOS_VERSION_CURRENT</span><span class="p">,</span>
|
||||||
<span class="n">overwrite</span><span class="o">=</span><span class="n">overwrite</span><span class="p">,</span>
|
<span class="n">overwrite</span><span class="o">=</span><span class="n">overwrite</span><span class="p">,</span>
|
||||||
|
<span class="n">video</span><span class="o">=</span><span class="n">live_photo</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">all_results</span><span class="o">.</span><span class="n">exported</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">exported</span><span class="p">)</span>
|
<span class="n">all_results</span><span class="o">.</span><span class="n">exported</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">exported</span><span class="p">)</span>
|
||||||
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||||
@@ -1345,6 +1347,7 @@
|
|||||||
<span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
|
<span class="n">dest</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
|
||||||
<span class="n">version</span><span class="o">=</span><span class="n">PHOTOS_VERSION_ORIGINAL</span><span class="p">,</span>
|
<span class="n">version</span><span class="o">=</span><span class="n">PHOTOS_VERSION_ORIGINAL</span><span class="p">,</span>
|
||||||
<span class="n">overwrite</span><span class="o">=</span><span class="n">overwrite</span><span class="p">,</span>
|
<span class="n">overwrite</span><span class="o">=</span><span class="n">overwrite</span><span class="p">,</span>
|
||||||
|
<span class="n">video</span><span class="o">=</span><span class="n">live_photo</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">all_results</span><span class="o">.</span><span class="n">exported</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">exported</span><span class="p">)</span>
|
<span class="n">all_results</span><span class="o">.</span><span class="n">exported</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">exported</span><span class="p">)</span>
|
||||||
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||||
@@ -1554,16 +1557,31 @@
|
|||||||
<span class="n">edited_stat</span> <span class="o">=</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">file_sig</span><span class="p">(</span><span class="n">src</span><span class="p">)</span> <span class="k">if</span> <span class="n">edited</span> <span class="k">else</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="kc">None</span><span class="p">)</span>
|
<span class="n">edited_stat</span> <span class="o">=</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">file_sig</span><span class="p">(</span><span class="n">src</span><span class="p">)</span> <span class="k">if</span> <span class="n">edited</span> <span class="k">else</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="kc">None</span><span class="p">)</span>
|
||||||
<span class="k">if</span> <span class="n">dest_exists</span> <span class="ow">and</span> <span class="p">(</span><span class="n">update</span> <span class="ow">or</span> <span class="n">overwrite</span><span class="p">):</span>
|
<span class="k">if</span> <span class="n">dest_exists</span> <span class="ow">and</span> <span class="p">(</span><span class="n">update</span> <span class="ow">or</span> <span class="n">overwrite</span><span class="p">):</span>
|
||||||
<span class="c1"># need to remove the destination first</span>
|
<span class="c1"># need to remove the destination first</span>
|
||||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">unlink</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
|
<span class="k">try</span><span class="p">:</span>
|
||||||
|
<span class="n">fileutil</span><span class="o">.</span><span class="n">unlink</span><span class="p">(</span><span class="n">dest</span><span class="p">)</span>
|
||||||
|
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||||
|
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
|
||||||
|
<span class="sa">f</span><span class="s2">"Error removing file </span><span class="si">{</span><span class="n">dest</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> ((</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)"</span>
|
||||||
|
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
|
||||||
<span class="k">if</span> <span class="n">export_as_hardlink</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">export_as_hardlink</span><span class="p">:</span>
|
||||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">hardlink</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest</span><span class="p">)</span>
|
<span class="k">try</span><span class="p">:</span>
|
||||||
|
<span class="n">fileutil</span><span class="o">.</span><span class="n">hardlink</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest</span><span class="p">)</span>
|
||||||
|
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||||
|
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
|
||||||
|
<span class="sa">f</span><span class="s2">"Error hardlinking </span><span class="si">{</span><span class="n">src</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)"</span>
|
||||||
|
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
|
||||||
<span class="k">elif</span> <span class="n">convert_to_jpeg</span><span class="p">:</span>
|
<span class="k">elif</span> <span class="n">convert_to_jpeg</span><span class="p">:</span>
|
||||||
<span class="c1"># use convert_to_jpeg to export the file</span>
|
<span class="c1"># use convert_to_jpeg to export the file</span>
|
||||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">convert_to_jpeg</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">,</span> <span class="n">compression_quality</span><span class="o">=</span><span class="n">jpeg_quality</span><span class="p">)</span>
|
<span class="n">fileutil</span><span class="o">.</span><span class="n">convert_to_jpeg</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">,</span> <span class="n">compression_quality</span><span class="o">=</span><span class="n">jpeg_quality</span><span class="p">)</span>
|
||||||
<span class="n">converted_stat</span> <span class="o">=</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">file_sig</span><span class="p">(</span><span class="n">dest_str</span><span class="p">)</span>
|
<span class="n">converted_stat</span> <span class="o">=</span> <span class="n">fileutil</span><span class="o">.</span><span class="n">file_sig</span><span class="p">(</span><span class="n">dest_str</span><span class="p">)</span>
|
||||||
<span class="n">converted_to_jpeg_files</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">dest_str</span><span class="p">)</span>
|
<span class="n">converted_to_jpeg_files</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">dest_str</span><span class="p">)</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="n">fileutil</span><span class="o">.</span><span class="n">copy</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">)</span>
|
<span class="k">try</span><span class="p">:</span>
|
||||||
|
<span class="n">fileutil</span><span class="o">.</span><span class="n">copy</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dest_str</span><span class="p">)</span>
|
||||||
|
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||||
|
<span class="k">raise</span> <span class="n">ExportError</span><span class="p">(</span>
|
||||||
|
<span class="sa">f</span><span class="s2">"Error copying file </span><span class="si">{</span><span class="n">src</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest_str</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">lineno</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="si">}</span><span class="s2">)"</span>
|
||||||
|
<span class="p">)</span> <span class="kn">from</span> <span class="nn">e</span>
|
||||||
|
|
||||||
<span class="n">export_db</span><span class="o">.</span><span class="n">set_data</span><span class="p">(</span>
|
<span class="n">export_db</span><span class="o">.</span><span class="n">set_data</span><span class="p">(</span>
|
||||||
<span class="n">filename</span><span class="o">=</span><span class="n">dest_str</span><span class="p">,</span>
|
<span class="n">filename</span><span class="o">=</span><span class="n">dest_str</span><span class="p">,</span>
|
||||||
@@ -1613,6 +1631,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
<span class="p">):</span>
|
<span class="p">):</span>
|
||||||
<span class="sd">"""write exif data to image file at filepath</span>
|
<span class="sd">"""write exif data to image file at filepath</span>
|
||||||
|
|
||||||
@@ -1626,6 +1645,7 @@
|
|||||||
<span class="sd"> persons: if True, write person data to metadata</span>
|
<span class="sd"> persons: if True, write person data to metadata</span>
|
||||||
<span class="sd"> location: if True, write location data to metadata</span>
|
<span class="sd"> location: if True, write location data to metadata</span>
|
||||||
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
||||||
|
<span class="sd"> strip: if True, strip any leading or trailing whitespace from rendered templates</span>
|
||||||
|
|
||||||
<span class="sd"> Returns:</span>
|
<span class="sd"> Returns:</span>
|
||||||
<span class="sd"> (warning, error) of warning and error strings if exiftool produces warnings or errors</span>
|
<span class="sd"> (warning, error) of warning and error strings if exiftool produces warnings or errors</span>
|
||||||
@@ -1643,6 +1663,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
|
|
||||||
<span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">flags</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span>
|
<span class="k">with</span> <span class="n">ExifTool</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="n">flags</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_exiftool_path</span><span class="p">)</span> <span class="k">as</span> <span class="n">exiftool</span><span class="p">:</span>
|
||||||
@@ -1668,6 +1689,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
<span class="p">):</span>
|
<span class="p">):</span>
|
||||||
<span class="sd">"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
|
<span class="sd">"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
|
||||||
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
|
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
|
||||||
@@ -1684,6 +1706,7 @@
|
|||||||
<span class="sd"> persons: if True, include person data</span>
|
<span class="sd"> persons: if True, include person data</span>
|
||||||
<span class="sd"> location: if True, include location data</span>
|
<span class="sd"> location: if True, include location data</span>
|
||||||
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
||||||
|
<span class="sd"> strip: if True, strip any rendered templates</span>
|
||||||
|
|
||||||
<span class="sd"> Returns: dict with exiftool tags / values</span>
|
<span class="sd"> Returns: dict with exiftool tags / values</span>
|
||||||
|
|
||||||
@@ -1731,6 +1754,8 @@
|
|||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">description_template</span><span class="p">,</span> <span class="n">options</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
|
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">description_template</span><span class="p">,</span> <span class="n">options</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
|
||||||
<span class="n">description</span> <span class="o">=</span> <span class="s2">" "</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">""</span>
|
<span class="n">description</span> <span class="o">=</span> <span class="s2">" "</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">""</span>
|
||||||
|
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
|
||||||
|
<span class="n">description</span> <span class="o">=</span> <span class="n">description</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"EXIF:ImageDescription"</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
|
<span class="n">exif</span><span class="p">[</span><span class="s2">"EXIF:ImageDescription"</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
|
||||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"XMP:Description"</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
|
<span class="n">exif</span><span class="p">[</span><span class="s2">"XMP:Description"</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
|
||||||
<span class="n">exif</span><span class="p">[</span><span class="s2">"IPTC:Caption-Abstract"</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
|
<span class="n">exif</span><span class="p">[</span><span class="s2">"IPTC:Caption-Abstract"</span><span class="p">]</span> <span class="o">=</span> <span class="n">description</span>
|
||||||
@@ -1778,6 +1803,9 @@
|
|||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
|
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
|
||||||
|
|
||||||
|
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
|
||||||
|
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">rendered_keywords</span><span class="p">]</span>
|
||||||
|
|
||||||
<span class="c1"># filter out any template values that didn't match by looking for sentinel</span>
|
<span class="c1"># filter out any template values that didn't match by looking for sentinel</span>
|
||||||
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
|
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
|
||||||
<span class="n">keyword</span>
|
<span class="n">keyword</span>
|
||||||
@@ -1884,12 +1912,6 @@
|
|||||||
<span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">date_modified</span>
|
||||||
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S"</span><span class="p">)</span>
|
<span class="p">)</span><span class="o">.</span><span class="n">strftime</span><span class="p">(</span><span class="s2">"%Y:%m:</span><span class="si">%d</span><span class="s2"> %H:%M:%S"</span><span class="p">)</span>
|
||||||
|
|
||||||
<span class="c1"># remove any new lines in any fields</span>
|
|
||||||
<span class="k">for</span> <span class="n">field</span><span class="p">,</span> <span class="n">val</span> <span class="ow">in</span> <span class="n">exif</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
|
|
||||||
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">str</span><span class="p">:</span>
|
|
||||||
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="o">=</span> <span class="n">val</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">" "</span><span class="p">)</span>
|
|
||||||
<span class="k">elif</span> <span class="nb">type</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="o">==</span> <span class="nb">list</span><span class="p">:</span>
|
|
||||||
<span class="n">exif</span><span class="p">[</span><span class="n">field</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">v</span><span class="p">)</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">" "</span><span class="p">)</span> <span class="k">for</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">val</span> <span class="k">if</span> <span class="n">v</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">]</span>
|
|
||||||
<span class="k">return</span> <span class="n">exif</span>
|
<span class="k">return</span> <span class="n">exif</span>
|
||||||
|
|
||||||
|
|
||||||
@@ -1942,6 +1964,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
<span class="p">):</span>
|
<span class="p">):</span>
|
||||||
<span class="sd">"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
|
<span class="sd">"""Return dict of EXIF details for building exiftool JSON sidecar or sending commands to ExifTool.</span>
|
||||||
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
|
<span class="sd"> Does not include all the EXIF fields as those are likely already in the image.</span>
|
||||||
@@ -1959,6 +1982,7 @@
|
|||||||
<span class="sd"> persons: if True, include person data</span>
|
<span class="sd"> persons: if True, include person data</span>
|
||||||
<span class="sd"> location: if True, include location data</span>
|
<span class="sd"> location: if True, include location data</span>
|
||||||
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
||||||
|
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
|
||||||
|
|
||||||
<span class="sd"> Returns: dict with exiftool tags / values</span>
|
<span class="sd"> Returns: dict with exiftool tags / values</span>
|
||||||
|
|
||||||
@@ -1998,6 +2022,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="n">persons</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="n">location</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="n">replace_keywords</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="n">strip</span><span class="p">,</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
|
|
||||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span>
|
<span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span>
|
||||||
@@ -2023,6 +2048,7 @@
|
|||||||
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
<span class="n">persons</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||||
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
<span class="n">location</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||||
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
<span class="n">replace_keywords</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
|
<span class="n">strip</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span>
|
||||||
<span class="p">):</span>
|
<span class="p">):</span>
|
||||||
<span class="sd">"""returns string for XMP sidecar</span>
|
<span class="sd">"""returns string for XMP sidecar</span>
|
||||||
<span class="sd"> use_albums_as_keywords: treat album names as keywords</span>
|
<span class="sd"> use_albums_as_keywords: treat album names as keywords</span>
|
||||||
@@ -2035,6 +2061,7 @@
|
|||||||
<span class="sd"> persons: if True, include person data</span>
|
<span class="sd"> persons: if True, include person data</span>
|
||||||
<span class="sd"> location: if True, include location data</span>
|
<span class="sd"> location: if True, include location data</span>
|
||||||
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
<span class="sd"> replace_keywords: if True, keyword_template replaces any keywords, otherwise it's additive</span>
|
||||||
|
<span class="sd"> strip: if True, strip whitespace from rendered templates</span>
|
||||||
<span class="sd"> """</span>
|
<span class="sd"> """</span>
|
||||||
|
|
||||||
<span class="n">xmp_template_file</span> <span class="o">=</span> <span class="p">(</span>
|
<span class="n">xmp_template_file</span> <span class="o">=</span> <span class="p">(</span>
|
||||||
@@ -2052,6 +2079,8 @@
|
|||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">description_template</span><span class="p">,</span> <span class="n">options</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
|
<span class="n">rendered</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">render_template</span><span class="p">(</span><span class="n">description_template</span><span class="p">,</span> <span class="n">options</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
|
||||||
<span class="n">description</span> <span class="o">=</span> <span class="s2">" "</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">""</span>
|
<span class="n">description</span> <span class="o">=</span> <span class="s2">" "</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span> <span class="k">if</span> <span class="n">rendered</span> <span class="k">else</span> <span class="s2">""</span>
|
||||||
|
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
|
||||||
|
<span class="n">description</span> <span class="o">=</span> <span class="n">description</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="n">description</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="s2">""</span>
|
<span class="n">description</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">description</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="s2">""</span>
|
||||||
|
|
||||||
@@ -2093,6 +2122,9 @@
|
|||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
|
<span class="n">rendered_keywords</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">rendered</span><span class="p">)</span>
|
||||||
|
|
||||||
|
<span class="k">if</span> <span class="n">strip</span><span class="p">:</span>
|
||||||
|
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span><span class="n">keyword</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span> <span class="k">for</span> <span class="n">keyword</span> <span class="ow">in</span> <span class="n">rendered_keywords</span><span class="p">]</span>
|
||||||
|
|
||||||
<span class="c1"># filter out any template values that didn't match by looking for sentinel</span>
|
<span class="c1"># filter out any template values that didn't match by looking for sentinel</span>
|
||||||
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
|
<span class="n">rendered_keywords</span> <span class="o">=</span> <span class="p">[</span>
|
||||||
<span class="n">keyword</span>
|
<span class="n">keyword</span>
|
||||||
@@ -2180,7 +2212,7 @@
|
|||||||
<h3 id="searchlabel">Quick search</h3>
|
<h3 id="searchlabel">Quick search</h3>
|
||||||
<div class="searchformwrapper">
|
<div class="searchformwrapper">
|
||||||
<form class="search" action="../../../search.html" method="get">
|
<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" />
|
<input type="submit" value="Go" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -2202,7 +2234,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.42.69 documentation</title>
|
<title>osxphotos.photoinfo.photoinfo — osxphotos 0.43.6 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.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 data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
||||||
@@ -47,6 +47,7 @@
|
|||||||
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
|
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
|
||||||
|
|
||||||
<span class="kn">import</span> <span class="nn">yaml</span>
|
<span class="kn">import</span> <span class="nn">yaml</span>
|
||||||
|
<span class="kn">from</span> <span class="nn">osxmetadata</span> <span class="kn">import</span> <span class="n">OSXMetaData</span>
|
||||||
|
|
||||||
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
|
<span class="kn">from</span> <span class="nn">.._constants</span> <span class="kn">import</span> <span class="p">(</span>
|
||||||
<span class="n">_MOVIE_TYPE</span><span class="p">,</span>
|
<span class="n">_MOVIE_TYPE</span><span class="p">,</span>
|
||||||
@@ -67,9 +68,11 @@
|
|||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
<span class="kn">from</span> <span class="nn">..adjustmentsinfo</span> <span class="kn">import</span> <span class="n">AdjustmentsInfo</span>
|
<span class="kn">from</span> <span class="nn">..adjustmentsinfo</span> <span class="kn">import</span> <span class="n">AdjustmentsInfo</span>
|
||||||
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">ImportInfo</span>
|
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">ImportInfo</span>
|
||||||
|
<span class="kn">from</span> <span class="nn">..momentinfo</span> <span class="kn">import</span> <span class="n">MomentInfo</span>
|
||||||
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span>
|
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">FaceInfo</span><span class="p">,</span> <span class="n">PersonInfo</span>
|
||||||
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span><span class="p">,</span> <span class="n">RenderOptions</span>
|
<span class="kn">from</span> <span class="nn">..phototemplate</span> <span class="kn">import</span> <span class="n">PhotoTemplate</span><span class="p">,</span> <span class="n">RenderOptions</span>
|
||||||
<span class="kn">from</span> <span class="nn">..placeinfo</span> <span class="kn">import</span> <span class="n">PlaceInfo4</span><span class="p">,</span> <span class="n">PlaceInfo5</span>
|
<span class="kn">from</span> <span class="nn">..placeinfo</span> <span class="kn">import</span> <span class="n">PlaceInfo4</span><span class="p">,</span> <span class="n">PlaceInfo5</span>
|
||||||
|
<span class="kn">from</span> <span class="nn">..query_builder</span> <span class="kn">import</span> <span class="n">get_query</span>
|
||||||
<span class="kn">from</span> <span class="nn">..text_detection</span> <span class="kn">import</span> <span class="n">detect_text</span>
|
<span class="kn">from</span> <span class="nn">..text_detection</span> <span class="kn">import</span> <span class="n">detect_text</span>
|
||||||
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span><span class="p">,</span> <span class="n">get_uti_for_extension</span>
|
<span class="kn">from</span> <span class="nn">..uti</span> <span class="kn">import</span> <span class="n">get_preferred_uti_extension</span><span class="p">,</span> <span class="n">get_uti_for_extension</span>
|
||||||
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">_debug</span><span class="p">,</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">findfiles</span>
|
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="n">_debug</span><span class="p">,</span> <span class="n">_get_resource_loc</span><span class="p">,</span> <span class="n">findfiles</span>
|
||||||
@@ -525,6 +528,18 @@
|
|||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_faceinfo</span> <span class="o">=</span> <span class="p">[]</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">_faceinfo</span> <span class="o">=</span> <span class="p">[]</span>
|
||||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_faceinfo</span>
|
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_faceinfo</span>
|
||||||
|
|
||||||
|
<span class="nd">@property</span>
|
||||||
|
<span class="k">def</span> <span class="nf">moment</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||||
|
<span class="sd">"""Moment photo belongs to"""</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">_moment</span>
|
||||||
|
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||||
|
<span class="k">try</span><span class="p">:</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_moment</span> <span class="o">=</span> <span class="n">MomentInfo</span><span class="p">(</span><span class="n">db</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">,</span> <span class="n">moment_pk</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"momentID"</span><span class="p">])</span>
|
||||||
|
<span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_moment</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
|
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_moment</span>
|
||||||
|
|
||||||
<span class="nd">@property</span>
|
<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="k">def</span> <span class="nf">albums</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||||
<span class="sd">"""list of albums picture is contained in"""</span>
|
<span class="sd">"""list of albums picture is contained in"""</span>
|
||||||
@@ -596,7 +611,12 @@
|
|||||||
<span class="nd">@property</span>
|
<span class="nd">@property</span>
|
||||||
<span class="k">def</span> <span class="nf">title</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
<span class="k">def</span> <span class="nf">title</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||||
<span class="sd">"""name / title of picture"""</span>
|
<span class="sd">"""name / title of picture"""</span>
|
||||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"name"</span><span class="p">]</span>
|
<span class="c1"># if user sets then deletes title, Photos sets it to empty string in DB instead of NULL</span>
|
||||||
|
<span class="c1"># in this case, return None so result is the same as if title had never been set (which returns NULL)</span>
|
||||||
|
<span class="c1"># issue #512</span>
|
||||||
|
<span class="n">title</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"name"</span><span class="p">]</span>
|
||||||
|
<span class="n">title</span> <span class="o">=</span> <span class="kc">None</span> <span class="k">if</span> <span class="n">title</span> <span class="o">==</span> <span class="s2">""</span> <span class="k">else</span> <span class="n">title</span>
|
||||||
|
<span class="k">return</span> <span class="n">title</span>
|
||||||
|
|
||||||
<span class="nd">@property</span>
|
<span class="nd">@property</span>
|
||||||
<span class="k">def</span> <span class="nf">uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
<span class="k">def</span> <span class="nf">uuid</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||||
@@ -869,7 +889,7 @@
|
|||||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
|
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
|
||||||
<span class="n">live_model_id</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"live_model_id"</span><span class="p">]</span>
|
<span class="n">live_model_id</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"live_model_id"</span><span class="p">]</span>
|
||||||
<span class="k">if</span> <span class="n">live_model_id</span> <span class="o">==</span> <span class="kc">None</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">live_model_id</span> <span class="ow">is</span> <span class="kc">None</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">"missing live_model_id: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2">"</span><span class="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">"missing live_model_id: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
@@ -890,28 +910,20 @@
|
|||||||
<span class="c1"># photos 4 has "isOnDisk" column we could check</span>
|
<span class="c1"># photos 4 has "isOnDisk" column we could check</span>
|
||||||
<span class="c1"># or could do the actual check with "isfile"</span>
|
<span class="c1"># or could do the actual check with "isfile"</span>
|
||||||
<span class="c1"># TODO: should this be a warning or debug?</span>
|
<span class="c1"># TODO: should this be a warning or debug?</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">"MISSING PATH: live photo path for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist"</span>
|
|
||||||
<span class="p">)</span>
|
|
||||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">else</span><span class="p">:</span>
|
||||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
<span class="k">else</span><span class="p">:</span>
|
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
|
||||||
<span class="c1"># Photos 5</span>
|
<span class="n">filename</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
|
||||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
|
<span class="n">photopath</span> <span class="o">=</span> <span class="n">filename</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">joinpath</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filename</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">_3.mov"</span><span class="p">)</span>
|
||||||
<span class="n">filename</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">path</span><span class="p">)</span>
|
<span class="n">photopath</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span>
|
||||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">filename</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">joinpath</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filename</span><span class="o">.</span><span class="n">stem</span><span class="si">}</span><span class="s2">_3.mov"</span><span class="p">)</span>
|
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||||
<span class="n">photopath</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span>
|
<span class="c1"># In testing, I've seen occasional missing movie for live photo</span>
|
||||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
<span class="c1"># these appear to be valid -- e.g. video component not yet downloaded from iCloud</span>
|
||||||
<span class="c1"># In testing, I've seen occasional missing movie for live photo</span>
|
<span class="c1"># TODO: should this be a warning or debug?</span>
|
||||||
<span class="c1"># these appear to be valid -- e.g. video component not yet downloaded from iCloud</span>
|
|
||||||
<span class="c1"># TODO: should this be a warning or debug?</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">"MISSING PATH: live photo path for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist"</span>
|
|
||||||
<span class="p">)</span>
|
|
||||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
|
||||||
<span class="k">else</span><span class="p">:</span>
|
|
||||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
|
<span class="k">else</span><span class="p">:</span>
|
||||||
|
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
|
|
||||||
<span class="k">return</span> <span class="n">photopath</span>
|
<span class="k">return</span> <span class="n">photopath</span>
|
||||||
|
|
||||||
@@ -1081,15 +1093,15 @@
|
|||||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"orientation"</span><span class="p">]</span>
|
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"orientation"</span><span class="p">]</span>
|
||||||
|
|
||||||
<span class="c1"># For Photos 5+, try to get the adjusted orientation</span>
|
<span class="c1"># For Photos 5+, try to get the adjusted orientation</span>
|
||||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
|
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">:</span>
|
||||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="p">:</span>
|
|
||||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">adj_orientation</span>
|
|
||||||
<span class="k">else</span><span class="p">:</span>
|
|
||||||
<span class="c1"># can't reliably determine orientation for edited photo if adjustmentinfo not available</span>
|
|
||||||
<span class="k">return</span> <span class="mi">0</span>
|
|
||||||
<span class="k">else</span><span class="p">:</span>
|
|
||||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"orientation"</span><span class="p">]</span>
|
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"orientation"</span><span class="p">]</span>
|
||||||
|
|
||||||
|
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="p">:</span>
|
||||||
|
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">adjustments</span><span class="o">.</span><span class="n">adj_orientation</span>
|
||||||
|
<span class="k">else</span><span class="p">:</span>
|
||||||
|
<span class="c1"># can't reliably determine orientation for edited photo if adjustmentinfo not available</span>
|
||||||
|
<span class="k">return</span> <span class="mi">0</span>
|
||||||
|
|
||||||
<span class="nd">@property</span>
|
<span class="nd">@property</span>
|
||||||
<span class="k">def</span> <span class="nf">original_height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
<span class="k">def</span> <span class="nf">original_height</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||||
<span class="sd">"""returns height of the original photo version in pixels"""</span>
|
<span class="sd">"""returns height of the original photo version in pixels"""</span>
|
||||||
@@ -1125,6 +1137,26 @@
|
|||||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Did not find signature for </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> in _db_signatures"</span><span class="p">)</span>
|
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Did not find signature for </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> in _db_signatures"</span><span class="p">)</span>
|
||||||
<span class="k">return</span> <span class="n">duplicates</span>
|
<span class="k">return</span> <span class="n">duplicates</span>
|
||||||
|
|
||||||
|
<span class="nd">@property</span>
|
||||||
|
<span class="k">def</span> <span class="nf">owner</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||||
|
<span class="sd">"""Return name of photo owner for shared photos (Photos 5+ only), or None if not shared"""</span>
|
||||||
|
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||||
|
<span class="k">return</span> <span class="kc">None</span>
|
||||||
|
|
||||||
|
<span class="k">try</span><span class="p">:</span>
|
||||||
|
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_owner</span>
|
||||||
|
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||||
|
<span class="k">try</span><span class="p">:</span>
|
||||||
|
<span class="n">personid</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"cloudownerhashedpersonid"</span><span class="p">]</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_owner</span> <span class="o">=</span> <span class="p">(</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_hashed_person_id</span><span class="p">[</span><span class="n">personid</span><span class="p">][</span><span class="s2">"full_name"</span><span class="p">]</span>
|
||||||
|
<span class="k">if</span> <span class="n">personid</span>
|
||||||
|
<span class="k">else</span> <span class="kc">None</span>
|
||||||
|
<span class="p">)</span>
|
||||||
|
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_owner</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
|
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_owner</span>
|
||||||
|
|
||||||
<div class="viewcode-block" id="PhotoInfo.render_template"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotoInfo.render_template">[docs]</a> <span class="k">def</span> <span class="nf">render_template</span><span class="p">(</span>
|
<div class="viewcode-block" id="PhotoInfo.render_template"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotoInfo.render_template">[docs]</a> <span class="k">def</span> <span class="nf">render_template</span><span class="p">(</span>
|
||||||
<span class="bp">self</span><span class="p">,</span> <span class="n">template_str</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
<span class="bp">self</span><span class="p">,</span> <span class="n">template_str</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">options</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">RenderOptions</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||||
<span class="p">):</span>
|
<span class="p">):</span>
|
||||||
@@ -1151,6 +1183,28 @@
|
|||||||
|
|
||||||
<span class="sd"> Returns: list of (detected text, confidence) tuples</span>
|
<span class="sd"> Returns: list of (detected text, confidence) tuples</span>
|
||||||
<span class="sd"> """</span>
|
<span class="sd"> """</span>
|
||||||
|
|
||||||
|
<span class="k">try</span><span class="p">:</span>
|
||||||
|
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span>
|
||||||
|
<span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||||
|
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="ne">AttributeError</span><span class="p">):</span>
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span> <span class="o">=</span> <span class="p">{}</span>
|
||||||
|
|
||||||
|
<span class="k">try</span><span class="p">:</span>
|
||||||
|
<span class="n">detected_text</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">()</span>
|
||||||
|
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||||
|
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error detecting text in photo </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
|
<span class="n">detected_text</span> <span class="o">=</span> <span class="p">[]</span>
|
||||||
|
|
||||||
|
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
|
||||||
|
<span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">confidence</span><span class="p">)</span>
|
||||||
|
<span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">confidence</span> <span class="ow">in</span> <span class="n">detected_text</span>
|
||||||
|
<span class="k">if</span> <span class="n">confidence</span> <span class="o">>=</span> <span class="n">confidence_threshold</span>
|
||||||
|
<span class="p">]</span>
|
||||||
|
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text_cache</span><span class="p">[</span><span class="n">confidence_threshold</span><span class="p">]</span></div>
|
||||||
|
|
||||||
|
<span class="k">def</span> <span class="nf">_detected_text</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||||
|
<span class="sd">"""detect text in photo, either from cached extended attribute or by attempting text detection"""</span>
|
||||||
<span class="n">path</span> <span class="o">=</span> <span class="p">(</span>
|
<span class="n">path</span> <span class="o">=</span> <span class="p">(</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span>
|
<span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">hasadjustments</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_edited</span> <span class="k">else</span> <span class="bp">self</span><span class="o">.</span><span class="n">path</span>
|
||||||
<span class="p">)</span>
|
<span class="p">)</span>
|
||||||
@@ -1158,23 +1212,13 @@
|
|||||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="p">:</span>
|
<span class="k">if</span> <span class="ow">not</span> <span class="n">path</span><span class="p">:</span>
|
||||||
<span class="k">return</span> <span class="p">[]</span>
|
<span class="k">return</span> <span class="p">[]</span>
|
||||||
|
|
||||||
<span class="k">try</span><span class="p">:</span>
|
<span class="n">md</span> <span class="o">=</span> <span class="n">OSXMetaData</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
|
||||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span>
|
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">md</span><span class="o">.</span><span class="n">get_attribute</span><span class="p">(</span><span class="s2">"osxphotos_detected_text"</span><span class="p">)</span>
|
||||||
<span class="k">except</span> <span class="p">(</span><span class="ne">AttributeError</span><span class="p">,</span> <span class="ne">KeyError</span><span class="p">)</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
<span class="k">if</span> <span class="n">detected_text</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||||
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">e</span><span class="p">,</span> <span class="ne">AttributeError</span><span class="p">):</span>
|
<span class="n">orientation</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">orientation</span> <span class="ow">or</span> <span class="kc">None</span>
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span> <span class="o">=</span> <span class="p">{}</span>
|
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">orientation</span><span class="p">)</span>
|
||||||
|
<span class="n">md</span><span class="o">.</span><span class="n">set_attribute</span><span class="p">(</span><span class="s2">"osxphotos_detected_text"</span><span class="p">,</span> <span class="n">detected_text</span><span class="p">)</span>
|
||||||
<span class="k">try</span><span class="p">:</span>
|
<span class="k">return</span> <span class="n">detected_text</span>
|
||||||
<span class="n">detected_text</span> <span class="o">=</span> <span class="n">detect_text</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
|
|
||||||
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
|
||||||
<span class="n">detected_text</span> <span class="o">=</span> <span class="p">[]</span>
|
|
||||||
|
|
||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span> <span class="o">=</span> <span class="p">[</span>
|
|
||||||
<span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">confidence</span><span class="p">)</span>
|
|
||||||
<span class="k">for</span> <span class="n">text</span><span class="p">,</span> <span class="n">confidence</span> <span class="ow">in</span> <span class="n">detected_text</span>
|
|
||||||
<span class="k">if</span> <span class="n">confidence</span> <span class="o">>=</span> <span class="n">confidence_threshold</span>
|
|
||||||
<span class="p">]</span>
|
|
||||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_detected_text</span><span class="p">[(</span><span class="n">path</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="p">)]</span></div>
|
|
||||||
|
|
||||||
<span class="nd">@property</span>
|
<span class="nd">@property</span>
|
||||||
<span class="k">def</span> <span class="nf">_longitude</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
<span class="k">def</span> <span class="nf">_longitude</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||||
@@ -1433,7 +1477,7 @@
|
|||||||
<h3 id="searchlabel">Quick search</h3>
|
<h3 id="searchlabel">Quick search</h3>
|
||||||
<div class="searchformwrapper">
|
<div class="searchformwrapper">
|
||||||
<form class="search" action="../../../search.html" method="get">
|
<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" />
|
<input type="submit" value="Go" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -1455,7 +1499,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.2.0</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>osxphotos.photosdb.photosdb — osxphotos 0.42.66 documentation</title>
|
<title>osxphotos.photosdb.photosdb — osxphotos 0.43.8 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="../../../_static/alabaster.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 data-url_root="../../../" id="documentation_options" src="../../../_static/documentation_options.js"></script>
|
||||||
@@ -45,12 +45,14 @@
|
|||||||
<span class="kn">import</span> <span class="nn">sys</span>
|
<span class="kn">import</span> <span class="nn">sys</span>
|
||||||
<span class="kn">import</span> <span class="nn">tempfile</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</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">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">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">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">bitmath</span>
|
||||||
<span class="kn">import</span> <span class="nn">photoscript</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="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>
|
<span class="n">_DB_TABLE_NAMES</span><span class="p">,</span>
|
||||||
@@ -283,6 +285,10 @@
|
|||||||
<span class="c1"># Dict to hold information on volume names (Photos 5+)</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="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="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">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</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">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||||
|
|
||||||
@@ -363,6 +369,8 @@
|
|||||||
<span class="k">else</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_database5</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="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="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">"""return keywords as dict of keyword, count in reverse sorted order (descending)"""</span>
|
<span class="sd">"""return keywords as dict of keyword, count in reverse sorted order (descending)"""</span>
|
||||||
@@ -823,8 +831,8 @@
|
|||||||
<span class="s2">"creation_date"</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">"creation_date"</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">"start_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
<span class="s2">"start_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||||
<span class="s2">"end_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
<span class="s2">"end_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||||
<span class="s2">"customsortascending"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
<span class="s2">"customsortascending"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||||
<span class="s2">"customsortkey"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
<span class="s2">"customsortkey"</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="p">}</span>
|
||||||
|
|
||||||
<span class="c1"># get details about folders</span>
|
<span class="c1"># get details about folders</span>
|
||||||
@@ -1137,7 +1145,9 @@
|
|||||||
<span class="c1"># get info on special types</span>
|
<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">"specialType"</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">"specialType"</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">"masterModelID"</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">"masterModelID"</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">"pk"</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">"pk"</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">"panorama"</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">"panorama"</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">"slow_mo"</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">"slow_mo"</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">"time_lapse"</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>
|
<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">"time_lapse"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">25</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span> <span class="k">else</span> <span class="kc">False</span>
|
||||||
@@ -1228,6 +1238,9 @@
|
|||||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"import_uuid"</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">"import_uuid"</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">"fok_import_session"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</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">"fok_import_session"</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">"cloudownerhashedpersonid"</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="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="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="k">try</span><span class="p">:</span>
|
||||||
@@ -1956,7 +1969,8 @@
|
|||||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZTRASHEDDATE,</span>
|
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.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">.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">.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"> 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 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 """</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 """</span>
|
||||||
@@ -2006,6 +2020,7 @@
|
|||||||
<span class="c1"># 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported</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"># 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library</span>
|
||||||
<span class="c1"># 42 ZGENERICASSET.Z_PK -- primary key</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="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="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||||
@@ -2191,6 +2206,7 @@
|
|||||||
<span class="n">info</span><span class="p">[</span><span class="s2">"added_date"</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">"added_date"</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">"pk"</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">"pk"</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">"cloudownerhashedpersonid"</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"># 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="c1"># not every photo has an import session so initialize all records now</span>
|
||||||
@@ -2514,6 +2530,10 @@
|
|||||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing comments and likes for shared photos."</span><span class="p">)</span>
|
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing comments and likes for shared photos."</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="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">"Processing moments."</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="c1"># done processing, dump debug data if requested</span>
|
||||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Done processing details from Photos library."</span><span class="p">)</span>
|
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Done processing details from Photos library."</span><span class="p">)</span>
|
||||||
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
<span class="k">if</span> <span class="n">_debug</span><span class="p">():</span>
|
||||||
@@ -2559,6 +2579,109 @@
|
|||||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">"Burst Photos (dbphotos_burst:"</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="s2">"Burst Photos (dbphotos_burst:"</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="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">"""Process data from ZMOMENT table"""</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"><=</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">"Moment info implemented for this database version"</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">"""Process moment info for Photos 5 databases"""</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">"""</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"""</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">"pk"</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">"timezoneOffset"</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">"trashedState"</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">"approximateLatitude"</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">"approximateLongitude"</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">"endDate"</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">"modificationDate"</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">"representativeDate"</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">"startDate"</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">"subtitle"</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">"title"</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">"uuid"</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">"approximateLatitude"</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">"approximateLongitude"</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">"latitude"</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">"longitude"</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">"latitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"approximateLatitude"</span><span class="p">]</span>
|
||||||
|
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"longitude"</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"approximateLongitude"</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">"timezoneOffset"</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">"startDate"</span><span class="p">,</span>
|
||||||
|
<span class="s2">"endDate"</span><span class="p">,</span>
|
||||||
|
<span class="s2">"modificationDate"</span><span class="p">,</span>
|
||||||
|
<span class="s2">"representativeDate"</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">"_timestamp"</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">"_timestamp"</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">"title"</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"title"</span><span class="p">]</span> <span class="ow">or</span> <span class="s2">""</span>
|
||||||
|
<span class="n">moment_info</span><span class="p">[</span><span class="s2">"subtitle"</span><span class="p">]</span> <span class="o">=</span> <span class="n">moment_info</span><span class="p">[</span><span class="s2">"subtitle"</span><span class="p">]</span> <span class="ow">or</span> <span class="s2">""</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">"pk"</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="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">"""recursively build folder/album hierarchy</span>
|
<span class="sd">"""recursively build folder/album hierarchy</span>
|
||||||
<span class="sd"> uuid: uuid of the album/folder being processed</span>
|
<span class="sd"> uuid: uuid of the album/folder being processed</span>
|
||||||
@@ -3319,9 +3442,9 @@
|
|||||||
<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="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">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">""</span><span class="p">)</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">""</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="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">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="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">render_options</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">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">rendered</span><span class="p">:</span>
|
||||||
@@ -3381,12 +3504,45 @@
|
|||||||
<span class="c1"># selection only works if photos selected in main media browser</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="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">""</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">""</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">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="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="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>
|
<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">"""Execute sql statement and return cursor"""</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="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">"""Compute a signature for finding possible duplicates"""</span>
|
<span class="sd">"""Compute a signature for finding possible duplicates"""</span>
|
||||||
<span class="k">return</span> <span class="p">(</span>
|
<span class="k">return</span> <span class="p">(</span>
|
||||||
@@ -3412,7 +3568,11 @@
|
|||||||
<span class="sd">"""Returns number of photos in the database</span>
|
<span class="sd">"""Returns number of photos in the database</span>
|
||||||
<span class="sd"> Includes recently deleted photos and non-selected burst images</span>
|
<span class="sd"> Includes recently deleted photos and non-selected burst images</span>
|
||||||
<span class="sd"> """</span>
|
<span class="sd"> """</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">"_db_connection"</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>
|
<span class="k">def</span> <span class="nf">_get_photos_by_attribute</span><span class="p">(</span><span class="n">photos</span><span class="p">,</span> <span class="n">attribute</span><span class="p">,</span> <span class="n">values</span><span class="p">,</span> <span class="n">ignore_case</span><span class="p">):</span>
|
||||||
@@ -3477,7 +3637,7 @@
|
|||||||
<h3 id="searchlabel">Quick search</h3>
|
<h3 id="searchlabel">Quick search</h3>
|
||||||
<div class="searchformwrapper">
|
<div class="searchformwrapper">
|
||||||
<form class="search" action="../../../search.html" method="get">
|
<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" />
|
<input type="submit" value="Go" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -3499,7 +3659,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.2</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
5
docs/_static/basic.css
vendored
@@ -731,8 +731,9 @@ dl.glossary dt {
|
|||||||
|
|
||||||
.classifier:before {
|
.classifier:before {
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
margin: 0.5em;
|
margin: 0 0.5em;
|
||||||
content: ":";
|
content: ":";
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
abbr, acronym {
|
abbr, acronym {
|
||||||
@@ -819,7 +820,7 @@ div.code-block-caption code {
|
|||||||
|
|
||||||
table.highlighttable td.linenos,
|
table.highlighttable td.linenos,
|
||||||
span.linenos,
|
span.linenos,
|
||||||
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
|
div.highlight span.gp { /* gp: Generic.Prompt */
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: text; /* Safari fallback only */
|
-webkit-user-select: text; /* Safari fallback only */
|
||||||
-webkit-user-select: none; /* Chrome/Safari */
|
-webkit-user-select: none; /* Chrome/Safari */
|
||||||
|
|||||||
2
docs/_static/doctools.js
vendored
@@ -301,12 +301,14 @@ var Documentation = {
|
|||||||
window.location.href = prevHref;
|
window.location.href = prevHref;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
case 39: // right
|
case 39: // right
|
||||||
var nextHref = $('link[rel="next"]').prop('href');
|
var nextHref = $('link[rel="next"]').prop('href');
|
||||||
if (nextHref) {
|
if (nextHref) {
|
||||||
window.location.href = nextHref;
|
window.location.href = nextHref;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
|||||||
var DOCUMENTATION_OPTIONS = {
|
var DOCUMENTATION_OPTIONS = {
|
||||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||||
VERSION: '0.42.69',
|
VERSION: '0.45.8',
|
||||||
LANGUAGE: 'None',
|
LANGUAGE: 'None',
|
||||||
COLLAPSE_INDEX: false,
|
COLLAPSE_INDEX: false,
|
||||||
BUILDER: 'html',
|
BUILDER: 'html',
|
||||||
|
|||||||
13
docs/_static/searchtools.js
vendored
@@ -282,7 +282,10 @@ var Search = {
|
|||||||
complete: function(jqxhr, textstatus) {
|
complete: function(jqxhr, textstatus) {
|
||||||
var data = jqxhr.responseText;
|
var data = jqxhr.responseText;
|
||||||
if (data !== '' && data !== undefined) {
|
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);
|
Search.output.append(listItem);
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
@@ -325,7 +328,9 @@ var Search = {
|
|||||||
var results = [];
|
var results = [];
|
||||||
|
|
||||||
for (var prefix in objects) {
|
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 fullname = (prefix ? prefix + '.' : '') + name;
|
||||||
var fullnameLower = fullname.toLowerCase()
|
var fullnameLower = fullname.toLowerCase()
|
||||||
if (fullnameLower.indexOf(object) > -1) {
|
if (fullnameLower.indexOf(object) > -1) {
|
||||||
@@ -339,7 +344,6 @@ var Search = {
|
|||||||
} else if (parts[parts.length - 1].indexOf(object) > -1) {
|
} else if (parts[parts.length - 1].indexOf(object) > -1) {
|
||||||
score += Scorer.objPartialMatch;
|
score += Scorer.objPartialMatch;
|
||||||
}
|
}
|
||||||
var match = objects[prefix][name];
|
|
||||||
var objname = objnames[match[1]][2];
|
var objname = objnames[match[1]][2];
|
||||||
var title = titles[match[0]];
|
var title = titles[match[0]];
|
||||||
// If more than one term searched for, we require other words to be
|
// If more than one term searched for, we require other words to be
|
||||||
@@ -498,6 +502,9 @@ var Search = {
|
|||||||
*/
|
*/
|
||||||
makeSearchSummary : function(htmlText, keywords, hlwords) {
|
makeSearchSummary : function(htmlText, keywords, hlwords) {
|
||||||
var text = Search.htmlToText(htmlText);
|
var text = Search.htmlToText(htmlText);
|
||||||
|
if (text == "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
var textLower = text.toLowerCase();
|
var textLower = text.toLowerCase();
|
||||||
var start = 0;
|
var start = 0;
|
||||||
$.each(keywords, function() {
|
$.each(keywords, function() {
|
||||||
|
|||||||
1588
docs/cli.html
2331
docs/genindex.html
106
docs/index.html
@@ -4,8 +4,9 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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 osxphotos’s documentation! — osxphotos 0.42.69 documentation</title>
|
|
||||||
|
<title>Welcome to osxphotos’s documentation! — osxphotos 0.45.8 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.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 data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
@@ -31,30 +32,30 @@
|
|||||||
|
|
||||||
<div class="body" role="main">
|
<div class="body" role="main">
|
||||||
|
|
||||||
<div class="section" id="welcome-to-osxphotos-s-documentation">
|
<section id="welcome-to-osxphotos-s-documentation">
|
||||||
<h1>Welcome to osxphotos’s documentation!<a class="headerlink" href="#welcome-to-osxphotos-s-documentation" title="Permalink to this headline">¶</a></h1>
|
<h1>Welcome to osxphotos’s documentation!<a class="headerlink" href="#welcome-to-osxphotos-s-documentation" title="Permalink to this headline">¶</a></h1>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="osxphotos">
|
<section id="osxphotos">
|
||||||
<h1>OSXPhotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
<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>
|
<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 Apple’s Photos.app library on macOS directly from your python code
|
<p>OSXPhotos provides both the ability to interact with and query Apple’s Photos.app library on macOS directly from your python code
|
||||||
as well as a very flexible command line interface (CLI) app for exporting photos.
|
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 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>
|
You can also easily export both the original and edited photos.</p>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="supported-operating-systems">
|
<section id="supported-operating-systems">
|
||||||
<h2>Supported operating systems<a class="headerlink" href="#supported-operating-systems" title="Permalink to this headline">¶</a></h2>
|
<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) through macOS Big Sur (11.3).</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>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.
|
<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>
|
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 >= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
|
<p>Requires python >= <code class="docutils literal notranslate"><span class="pre">3.7</span></code>.</p>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="installation">
|
<section id="installation">
|
||||||
<h2>Installation<a class="headerlink" href="#installation" title="Permalink to this headline">¶</a></h2>
|
<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>
|
<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>
|
<h3>Installation using pipx<a class="headerlink" href="#installation-using-pipx" title="Permalink to this headline">¶</a></h3>
|
||||||
<p>If you aren’t 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>
|
<p>If you aren’t 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">
|
<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>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>
|
<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>
|
</ul>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="installation-using-pip">
|
<section id="installation-using-pip">
|
||||||
<h3>Installation using pip<a class="headerlink" href="#installation-using-pip" title="Permalink to this headline">¶</a></h3>
|
<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>
|
<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>
|
<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>
|
</pre></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="installation-from-git-repository">
|
<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>
|
<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>
|
<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>
|
<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
|
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.
|
<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 aren’t 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>
|
If you aren’t 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>
|
</section>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="command-line-usage">
|
<section id="command-line-usage">
|
||||||
<h2>Command Line Usage<a class="headerlink" href="#command-line-usage" title="Permalink to this headline">¶</a></h2>
|
<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.
|
<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>
|
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>
|
||||||
@@ -127,38 +128,38 @@ Alternatively, you can also run the command line utility like this: <code class=
|
|||||||
</pre></div>
|
</pre></div>
|
||||||
</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"><command_name></span></code></p>
|
<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"><command_name></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>
|
<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>
|
<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><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><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>
|
<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>
|
</section>
|
||||||
<div class="section" id="find-all-photos-with-keyword-kids-and-output-results-to-json-file-named-results-json">
|
<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>
|
<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">>results.json</span></code></p>
|
<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">>results.json</span></code></p>
|
||||||
</div>
|
</section>
|
||||||
<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 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 photo’s 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>
|
<h4>export photos to file structure based on 4-digit year and full name of month of photo’s 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">"{created.year}/{created.month}"</span></code></p>
|
<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">"{created.year}/{created.month}"</span></code></p>
|
||||||
<p>(by default, it will attempt to use the system library)</p>
|
<p>(by default, it will attempt to use the system library)</p>
|
||||||
</div>
|
</section>
|
||||||
<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 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 photo’s 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>
|
<h4>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):<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">"{created.year}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{media_type}"</span></code></p>
|
<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">"{created.year}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{label}"</span> <span class="pre">--keyword-template</span> <span class="pre">"{media_type}"</span></code></p>
|
||||||
</div>
|
</section>
|
||||||
<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 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>
|
<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">"{place.name.country,NoCountry}/{created.year}"</span>  <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">"{created.year}"</span> <span class="pre">--exiftool</span> <span class="pre">--update</span> <span class="pre">--verbose</span></code></p>
|
<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">"{place.name.country,NoCountry}/{created.year}"</span>  <span class="pre">--person-keyword</span> <span class="pre">--album-keyword</span> <span class="pre">--keyword-template</span> <span class="pre">"{created.year}"</span> <span class="pre">--exiftool</span> <span class="pre">--update</span> <span class="pre">--verbose</span></code></p>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="find-all-videos-larger-than-200mb-and-add-them-to-photos-album-big-videos-creating-the-album-if-necessary">
|
<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>
|
<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">"Big</span> <span class="pre">Videos"</span></code></p>
|
<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">"Big</span> <span class="pre">Videos"</span></code></p>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="example-uses-of-the-package">
|
<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>
|
<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">""" Simple usage of the package """</span>
|
<div class="highlight-python notranslate"><div class="highlight"><pre><span></span><span class="sd">""" Simple usage of the package """</span>
|
||||||
<span class="kn">import</span> <span class="nn">osxphotos</span>
|
<span class="kn">import</span> <span class="nn">osxphotos</span>
|
||||||
@@ -274,48 +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>
|
<span class="n">export</span><span class="p">()</span> <span class="c1"># pylint: disable=no-value-for-parameter</span>
|
||||||
</pre></div>
|
</pre></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="package-interface">
|
<section id="package-interface">
|
||||||
<h2>Package Interface<a class="headerlink" href="#package-interface" title="Permalink to this headline">¶</a></h2>
|
<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>
|
<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">
|
<div class="toctree-wrapper compound">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a><ul>
|
<li class="toctree-l1"><a class="reference internal" href="cli.html">osxphotos command line interface (CLI)</a></li>
|
||||||
<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>
|
|
||||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-repl">repl</a></li>
|
|
||||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-tutorial">tutorial</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">osxphotos package</a><ul>
|
<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>
|
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos-module">osxphotos module</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</section>
|
||||||
<div class="section" id="indices-and-tables">
|
<section id="indices-and-tables">
|
||||||
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this headline">¶</a></h1>
|
<h1>Indices and tables<a class="headerlink" href="#indices-and-tables" title="Permalink to this headline">¶</a></h1>
|
||||||
<ul class="simple">
|
<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="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="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>
|
<li><p><a class="reference internal" href="search.html"><span class="std std-ref">Search Page</span></a></p></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -351,7 +333,7 @@ Alternatively, you can also run the command line utility like this: <code class=
|
|||||||
<h3 id="searchlabel">Quick search</h3>
|
<h3 id="searchlabel">Quick search</h3>
|
||||||
<div class="searchformwrapper">
|
<div class="searchformwrapper">
|
||||||
<form class="search" action="search.html" method="get">
|
<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" />
|
<input type="submit" value="Go" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,7 +355,7 @@ Alternatively, you can also run the command line utility like this: <code class=
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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 — osxphotos 0.42.69 documentation</title>
|
|
||||||
|
<title>osxphotos — osxphotos 0.45.8 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.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 data-url_root="./" id="documentation_options" src="_static/documentation_options.js"></script>
|
||||||
@@ -30,11 +31,11 @@
|
|||||||
|
|
||||||
<div class="body" role="main">
|
<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>
|
<h1>osxphotos<a class="headerlink" href="#osxphotos" title="Permalink to this headline">¶</a></h1>
|
||||||
<div class="toctree-wrapper compound">
|
<div class="toctree-wrapper compound">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
<h3 id="searchlabel">Quick search</h3>
|
<h3 id="searchlabel">Quick search</h3>
|
||||||
<div class="searchformwrapper">
|
<div class="searchformwrapper">
|
||||||
<form class="search" action="search.html" method="get">
|
<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" />
|
<input type="submit" value="Go" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,7 +92,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|||||||
BIN
docs/objects.inv
1338
docs/reference.html
@@ -5,7 +5,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Search — osxphotos 0.42.69 documentation</title>
|
<title>Search — osxphotos 0.45.8 documentation</title>
|
||||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||||
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
<link rel="stylesheet" type="text/css" href="_static/alabaster.css" />
|
||||||
|
|
||||||
@@ -38,13 +38,14 @@
|
|||||||
|
|
||||||
<h1 id="search-documentation">Search</h1>
|
<h1 id="search-documentation">Search</h1>
|
||||||
|
|
||||||
<div id="fallback" class="admonition warning">
|
<noscript>
|
||||||
<script>$('#fallback').hide();</script>
|
<div class="admonition warning">
|
||||||
<p>
|
<p>
|
||||||
Please activate JavaScript to enable the search
|
Please activate JavaScript to enable the search
|
||||||
functionality.
|
functionality.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
@@ -54,7 +55,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<form action="" method="get">
|
<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" />
|
<input type="submit" value="search" />
|
||||||
<span id="search-progress" style="padding-left: 10px"></span>
|
<span id="search-progress" style="padding-left: 10px"></span>
|
||||||
</form>
|
</form>
|
||||||
@@ -110,7 +111,7 @@
|
|||||||
©2021, Rhet Turnbull.
|
©2021, Rhet Turnbull.
|
||||||
|
|
||||||
|
|
|
|
||||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.0.2</a>
|
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.3.1</a>
|
||||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ datas = [
|
|||||||
("osxphotos/phototemplate.tx", "osxphotos"),
|
("osxphotos/phototemplate.tx", "osxphotos"),
|
||||||
("osxphotos/phototemplate.md", "osxphotos"),
|
("osxphotos/phototemplate.md", "osxphotos"),
|
||||||
("osxphotos/tutorial.md", "osxphotos"),
|
("osxphotos/tutorial.md", "osxphotos"),
|
||||||
|
("osxphotos/exiftool_filetypes.json", "osxphotos"),
|
||||||
]
|
]
|
||||||
package_imports = [["photoscript", ["photoscript.applescript"]]]
|
package_imports = [["photoscript", ["photoscript.applescript"]]]
|
||||||
for package, files in package_imports:
|
for package, files in package_imports:
|
||||||
|
|||||||
@@ -1,12 +1,45 @@
|
|||||||
from ._constants import AlbumSortOrder
|
from ._constants import AlbumSortOrder
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from .exiftool import ExifTool
|
from .exiftool import ExifTool
|
||||||
from .photoinfo import ExportResults, PhotoInfo
|
from .export_db import ExportDB, ExportDBInMemory, ExportDBNoOp
|
||||||
|
from .fileutil import FileUtil, FileUtilNoOp
|
||||||
|
from .momentinfo import MomentInfo
|
||||||
|
from .personinfo import PersonInfo
|
||||||
|
from .photoexporter import ExportOptions, ExportResults, PhotoExporter
|
||||||
|
from .photoinfo import PhotoInfo
|
||||||
from .photosdb import PhotosDB
|
from .photosdb import PhotosDB
|
||||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||||
from .phototemplate import PhotoTemplate
|
from .phototemplate import PhotoTemplate
|
||||||
|
from .placeinfo import PlaceInfo
|
||||||
from .queryoptions import QueryOptions
|
from .queryoptions import QueryOptions
|
||||||
|
from .scoreinfo import ScoreInfo
|
||||||
|
from .searchinfo import SearchInfo
|
||||||
from .utils import _debug, _get_logger, _set_debug
|
from .utils import _debug, _get_logger, _set_debug
|
||||||
|
|
||||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
__all__ = [
|
||||||
# TODO: Add special albums and magic albums
|
"__version__",
|
||||||
|
"_debug",
|
||||||
|
"_get_logger",
|
||||||
|
"_set_debug",
|
||||||
|
"AlbumSortOrder",
|
||||||
|
"CommentInfo",
|
||||||
|
"ExifTool",
|
||||||
|
"ExportDB",
|
||||||
|
"ExportDBInMemory",
|
||||||
|
"ExportDBNoOp",
|
||||||
|
"ExportOptions",
|
||||||
|
"ExportResults",
|
||||||
|
"FileUtil",
|
||||||
|
"FileUtilNoOp",
|
||||||
|
"LikeInfo",
|
||||||
|
"MomentInfo",
|
||||||
|
"PersonInfo",
|
||||||
|
"PhotoExporter",
|
||||||
|
"PhotoInfo",
|
||||||
|
"PhotosDB",
|
||||||
|
"PhotoTemplate",
|
||||||
|
"PlaceInfo",
|
||||||
|
"QueryOptions",
|
||||||
|
"ScoreInfo",
|
||||||
|
"SearchInfo",
|
||||||
|
]
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ UNICODE_FORMAT = "NFC"
|
|||||||
# Photos 3.0 (10.13.6) == 3301
|
# Photos 3.0 (10.13.6) == 3301
|
||||||
# Photos 4.0 (10.14.5) == 4016
|
# Photos 4.0 (10.14.5) == 4016
|
||||||
# Photos 4.0 (10.14.6) == 4025
|
# Photos 4.0 (10.14.6) == 4025
|
||||||
# Photos 5.0 (10.15.0) == 6000
|
# Photos 5.0 (10.15.0) == 6000 or 5001
|
||||||
_TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
|
_TESTED_DB_VERSIONS = ["6000", "5001", "4025", "4016", "3301", "2622"]
|
||||||
|
|
||||||
# database model versions (applies to Photos 5, Photos 6)
|
# database model versions (applies to Photos 5, Photos 6)
|
||||||
# these come from PLModelVersion key in binary plist in Z_METADATA.Z_PLIST
|
# these come from PLModelVersion key in binary plist in Z_METADATA.Z_PLIST
|
||||||
@@ -30,17 +30,22 @@ _TESTED_DB_VERSIONS = ["6000", "4025", "4016", "3301", "2622"]
|
|||||||
# Photos 6 (10.16.0 Beta) == 14104
|
# Photos 6 (10.16.0 Beta) == 14104
|
||||||
_TEST_MODEL_VERSIONS = ["13537", "13703", "14104"]
|
_TEST_MODEL_VERSIONS = ["13537", "13703", "14104"]
|
||||||
|
|
||||||
|
_PHOTOS_2_VERSION = "2622"
|
||||||
|
|
||||||
# only version 3 - 4 have RKVersion.selfPortrait
|
# only version 3 - 4 have RKVersion.selfPortrait
|
||||||
_PHOTOS_3_VERSION = "3301"
|
_PHOTOS_3_VERSION = "3301"
|
||||||
|
|
||||||
# versions 5.0 and later have a different database structure
|
# versions 5.0 and later have a different database structure
|
||||||
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6
|
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6
|
||||||
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.7 (also Big Sur and Monterey which switch to model version)
|
_PHOTOS_5_VERSION = "5000" # I've seen both 5001 and 6000. 6000 is most common on Catalina and up but there are some version 5001 database in the wild
|
||||||
|
|
||||||
# Ranges for model version by Photos version
|
# Ranges for model version by Photos version
|
||||||
_PHOTOS_5_MODEL_VERSION = [13000, 13999]
|
_PHOTOS_5_MODEL_VERSION = [13000, 13999]
|
||||||
_PHOTOS_6_MODEL_VERSION = [14000, 14999]
|
_PHOTOS_6_MODEL_VERSION = [14000, 14999]
|
||||||
_PHOTOS_7_MODEL_VERSION = [15000, 15999] # Monterey developer preview is 15134
|
_PHOTOS_7_MODEL_VERSION = [
|
||||||
|
15000,
|
||||||
|
15999,
|
||||||
|
] # Monterey developer preview is 15134, 12.1 is 15331
|
||||||
|
|
||||||
# some table names differ between Photos 5 and Photos 6
|
# some table names differ between Photos 5 and Photos 6
|
||||||
_DB_TABLE_NAMES = {
|
_DB_TABLE_NAMES = {
|
||||||
@@ -94,6 +99,10 @@ _TESTED_OS_VERSIONS = [
|
|||||||
("11", "2"),
|
("11", "2"),
|
||||||
("11", "3"),
|
("11", "3"),
|
||||||
("11", "4"),
|
("11", "4"),
|
||||||
|
("11", "5"),
|
||||||
|
("11", "6"),
|
||||||
|
("12", "0"),
|
||||||
|
("12", "1"),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Photos 5 has persons who are empty string if unidentified face
|
# Photos 5 has persons who are empty string if unidentified face
|
||||||
@@ -119,12 +128,20 @@ _XMP_TEMPLATE_NAME_BETA = "xmp_sidecar_beta.mako"
|
|||||||
# Constants used for processing folders and albums
|
# Constants used for processing folders and albums
|
||||||
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
_PHOTOS_5_ALBUM_KIND = 2 # normal user album
|
||||||
_PHOTOS_5_SHARED_ALBUM_KIND = 1505 # shared 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_FOLDER_KIND = 4000 # user folder
|
||||||
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder
|
_PHOTOS_5_ROOT_FOLDER_KIND = 3999 # root folder
|
||||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session
|
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND = 1506 # import session
|
||||||
|
|
||||||
_PHOTOS_4_ALBUM_KIND = 3 # RKAlbum.albumSubclass
|
_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"
|
_PHOTOS_4_ROOT_FOLDER = "LibraryFolder"
|
||||||
|
|
||||||
# EXIF related constants
|
# EXIF related constants
|
||||||
@@ -197,7 +214,8 @@ SEARCH_CATEGORY_PHOTO_NAME = 2056
|
|||||||
|
|
||||||
|
|
||||||
# Max filename length on MacOS
|
# Max filename length on MacOS
|
||||||
MAX_FILENAME_LEN = 255
|
# subtract 6 chars for the lock file extension in form: ".filename.lock"
|
||||||
|
MAX_FILENAME_LEN = 255 - 6
|
||||||
|
|
||||||
# Max directory name length on MacOS
|
# Max directory name length on MacOS
|
||||||
MAX_DIRNAME_LEN = 255
|
MAX_DIRNAME_LEN = 255
|
||||||
@@ -246,6 +264,7 @@ EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
|
|||||||
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
|
OSXPHOTOS_EXPORT_DB = ".osxphotos_export.db"
|
||||||
|
|
||||||
# bit flags for burst images ("burstPickType")
|
# bit flags for burst images ("burstPickType")
|
||||||
|
BURST_PICK_TYPE_NONE = 0b0 # 0: sometimes used for single images with a burst UUID
|
||||||
BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected
|
BURST_NOT_SELECTED = 0b10 # 2: burst image is not selected
|
||||||
BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made
|
BURST_DEFAULT_PICK = 0b100 # 4: burst image is the one Photos picked to be key image before any selections made
|
||||||
BURST_SELECTED = 0b1000 # 8: burst image is selected
|
BURST_SELECTED = 0b1000 # 8: burst image is selected
|
||||||
@@ -275,12 +294,33 @@ POST_COMMAND_CATEGORIES = {
|
|||||||
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
|
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class AlbumSortOrder(Enum):
|
class AlbumSortOrder(Enum):
|
||||||
"""Album Sort Order"""
|
"""Album Sort Order"""
|
||||||
|
|
||||||
UNKNOWN = 0
|
UNKNOWN = 0
|
||||||
MANUAL = 1
|
MANUAL = 1
|
||||||
NEWEST_FIRST = 2
|
NEWEST_FIRST = 2
|
||||||
OLDEST_FIRST = 3
|
OLDEST_FIRST = 3
|
||||||
TITLE = 5
|
TITLE = 5
|
||||||
|
|
||||||
|
|
||||||
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75
|
TEXT_DETECTION_CONFIDENCE_THRESHOLD = 0.75
|
||||||
|
|
||||||
|
# stat sort order for cProfile: https://docs.python.org/3/library/profile.html#pstats.Stats.sort_stats
|
||||||
|
PROFILE_SORT_KEYS = [
|
||||||
|
"calls",
|
||||||
|
"cumulative",
|
||||||
|
"cumtime",
|
||||||
|
"file",
|
||||||
|
"filename",
|
||||||
|
"module",
|
||||||
|
"ncalls",
|
||||||
|
"pcalls",
|
||||||
|
"line",
|
||||||
|
"name",
|
||||||
|
"nfl",
|
||||||
|
"stdname",
|
||||||
|
"time",
|
||||||
|
"tottime",
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
""" version info """
|
""" version info """
|
||||||
|
|
||||||
__version__ = "0.42.70"
|
__version__ = "0.45.8"
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import zlib
|
|||||||
|
|
||||||
from .datetime_utils import datetime_naive_to_utc
|
from .datetime_utils import datetime_naive_to_utc
|
||||||
|
|
||||||
|
__all__ = ["AdjustmentsDecodeError", "AdjustmentsInfo"]
|
||||||
|
|
||||||
|
|
||||||
class AdjustmentsDecodeError(Exception):
|
class AdjustmentsDecodeError(Exception):
|
||||||
"""Could not decode adjustments plist file"""
|
"""Could not decode adjustments plist file"""
|
||||||
@@ -73,37 +75,37 @@ class AdjustmentsInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def plist(self):
|
def plist(self):
|
||||||
"""The actual adjustments plist content as a dict """
|
"""The actual adjustments plist content as a dict"""
|
||||||
return self._plist
|
return self._plist
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
"""The raw adjustments data as a binary blob """
|
"""The raw adjustments data as a binary blob"""
|
||||||
return self._data
|
return self._data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def editor(self):
|
def editor(self):
|
||||||
"""The editor bundle ID for app/plug-in which made the adjustments """
|
"""The editor bundle ID for app/plug-in which made the adjustments"""
|
||||||
return self._editor_bundle_id
|
return self._editor_bundle_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def format_id(self):
|
def format_id(self):
|
||||||
"""The value of the adjustmentFormatIdentifier field in the plist """
|
"""The value of the adjustmentFormatIdentifier field in the plist"""
|
||||||
return self._format_identifier
|
return self._format_identifier
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def base_version(self):
|
def base_version(self):
|
||||||
"""Value of adjustmentBaseVersion field """
|
"""Value of adjustmentBaseVersion field"""
|
||||||
return self._base_version
|
return self._base_version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def format_version(self):
|
def format_version(self):
|
||||||
"""The value of the adjustmentFormatVersion in the plist """
|
"""The value of the adjustmentFormatVersion in the plist"""
|
||||||
return self._format_version
|
return self._format_version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def timestamp(self):
|
def timestamp(self):
|
||||||
"""The time stamp of the adjustment as timezone aware datetime.datetime object or None if no timestamp """
|
"""The time stamp of the adjustment as timezone aware datetime.datetime object or None if no timestamp"""
|
||||||
return self._timestamp
|
return self._timestamp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
|
|
||||||
from ._constants import (
|
from ._constants import (
|
||||||
_PHOTOS_4_ALBUM_KIND,
|
_PHOTOS_4_ALBUM_KIND,
|
||||||
_PHOTOS_4_TOP_LEVEL_ALBUM,
|
_PHOTOS_4_TOP_LEVEL_ALBUMS,
|
||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
_PHOTOS_5_ALBUM_KIND,
|
_PHOTOS_5_ALBUM_KIND,
|
||||||
_PHOTOS_5_FOLDER_KIND,
|
_PHOTOS_5_FOLDER_KIND,
|
||||||
@@ -22,6 +22,16 @@ from ._constants import (
|
|||||||
AlbumSortOrder,
|
AlbumSortOrder,
|
||||||
)
|
)
|
||||||
from .datetime_utils import get_local_tz
|
from .datetime_utils import get_local_tz
|
||||||
|
from .query_builder import get_query
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"sort_list_by_keys",
|
||||||
|
"AlbumInfoBaseClass",
|
||||||
|
"AlbumInfo",
|
||||||
|
"ImportInfo",
|
||||||
|
"ProjectInfo",
|
||||||
|
"FolderInfo",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def sort_list_by_keys(values, sort_keys):
|
def sort_list_by_keys(values, sort_keys):
|
||||||
@@ -131,6 +141,28 @@ class AlbumInfoBaseClass:
|
|||||||
def photos(self):
|
def photos(self):
|
||||||
return []
|
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):
|
def __len__(self):
|
||||||
"""return number of photos contained in album"""
|
"""return number of photos contained in album"""
|
||||||
return len(self.photos)
|
return len(self.photos)
|
||||||
@@ -138,7 +170,6 @@ class AlbumInfoBaseClass:
|
|||||||
|
|
||||||
class AlbumInfo(AlbumInfoBaseClass):
|
class AlbumInfo(AlbumInfoBaseClass):
|
||||||
"""
|
"""
|
||||||
Base class for AlbumInfo, ImportInfo
|
|
||||||
Info about a specific Album, contains all the details about the album
|
Info about a specific Album, contains all the details about the album
|
||||||
including folders, photos, etc.
|
including folders, photos, etc.
|
||||||
"""
|
"""
|
||||||
@@ -208,7 +239,7 @@ class AlbumInfo(AlbumInfoBaseClass):
|
|||||||
parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
|
parent_uuid = self._db._dbalbum_details[self._uuid]["folderUuid"]
|
||||||
self._parent = (
|
self._parent = (
|
||||||
FolderInfo(db=self._db, uuid=parent_uuid)
|
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 None
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -243,18 +274,17 @@ class AlbumInfo(AlbumInfoBaseClass):
|
|||||||
|
|
||||||
def photo_index(self, photo):
|
def photo_index(self, photo):
|
||||||
"""return index of photo in album (based on album sort order)"""
|
"""return index of photo in album (based on album sort order)"""
|
||||||
index = 0
|
for index, p in enumerate(self.photos):
|
||||||
for p in self.photos:
|
|
||||||
if p.uuid == photo.uuid:
|
if p.uuid == photo.uuid:
|
||||||
return index
|
return index
|
||||||
index += 1
|
raise ValueError(
|
||||||
else:
|
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):
|
class ImportInfo(AlbumInfoBaseClass):
|
||||||
|
"""Information about import sessions"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def photos(self):
|
def photos(self):
|
||||||
"""return list of photos contained in import session"""
|
"""return list of photos contained in import session"""
|
||||||
@@ -273,6 +303,15 @@ class ImportInfo(AlbumInfoBaseClass):
|
|||||||
return self._photos
|
return self._photos
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectInfo(AlbumInfo):
|
||||||
|
"""
|
||||||
|
ProjectInfo with info about projects
|
||||||
|
Projects are cards, calendars, slideshows, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
class FolderInfo:
|
class FolderInfo:
|
||||||
"""
|
"""
|
||||||
Info about a specific folder, contains all the details about the folder
|
Info about a specific folder, contains all the details about the folder
|
||||||
@@ -334,7 +373,7 @@ class FolderInfo:
|
|||||||
parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"]
|
parent_uuid = self._db._dbfolder_details[self._uuid]["parentFolderUuid"]
|
||||||
self._parent = (
|
self._parent = (
|
||||||
FolderInfo(db=self._db, uuid=parent_uuid)
|
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 None
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
1991
osxphotos/cli.py
@@ -22,6 +22,17 @@ from .phototemplate import (
|
|||||||
get_template_help,
|
get_template_help,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ExportCommand",
|
||||||
|
"template_help",
|
||||||
|
"tutorial_help",
|
||||||
|
"rich_text",
|
||||||
|
"strip_md_header_and_links",
|
||||||
|
"strip_md_links",
|
||||||
|
"strip_html_comments",
|
||||||
|
"get_tutorial_text",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# TODO: The following help text could probably be done as mako template
|
# TODO: The following help text could probably be done as mako template
|
||||||
class ExportCommand(click.Command):
|
class ExportCommand(click.Command):
|
||||||
@@ -207,7 +218,7 @@ The following attributes may be used with '--xattr-template':
|
|||||||
+ "The following categories are available: "
|
+ "The following categories are available: "
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
templ_tuples = [("Catgory", "Description")]
|
templ_tuples = [("Category", "Description")]
|
||||||
templ_tuples.extend((k, v) for k, v in POST_COMMAND_CATEGORIES.items())
|
templ_tuples.extend((k, v) for k, v in POST_COMMAND_CATEGORIES.items())
|
||||||
formatter.write_dl(templ_tuples)
|
formatter.write_dl(templ_tuples)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
@@ -224,13 +235,13 @@ The following attributes may be used with '--xattr-template':
|
|||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
formatter.write(
|
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("\n\n")
|
||||||
formatter.write_text(
|
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. "
|
+ "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: "
|
"thus renders to: "
|
||||||
)
|
)
|
||||||
formatter.write("\n")
|
formatter.write("\n")
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
""" ConfigOptions class to load/save config settings for osxphotos CLI """
|
""" ConfigOptions class to load/save config settings for osxphotos CLI """
|
||||||
import toml
|
import toml
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"ConfigOptionsException",
|
||||||
|
"ConfigOptionsInvalidError",
|
||||||
|
"ConfigOptionsLoadError",
|
||||||
|
"ConfigOptions",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConfigOptionsException(Exception):
|
class ConfigOptionsException(Exception):
|
||||||
""" Invalid combination of options. """
|
"""Invalid combination of options."""
|
||||||
|
|
||||||
def __init__(self, message):
|
def __init__(self, message):
|
||||||
self.message = message
|
self.message = message
|
||||||
@@ -19,10 +26,10 @@ class ConfigOptionsLoadError(ConfigOptionsException):
|
|||||||
|
|
||||||
|
|
||||||
class ConfigOptions:
|
class ConfigOptions:
|
||||||
""" data class to store and load options for osxphotos commands """
|
"""data class to store and load options for osxphotos commands"""
|
||||||
|
|
||||||
def __init__(self, name, attrs, ignore=None):
|
def __init__(self, name, attrs, ignore=None):
|
||||||
""" init ConfigOptions class
|
"""init ConfigOptions class
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: name for these options, will be used for section heading in TOML file when saving/loading from file
|
name: name for these options, will be used for section heading in TOML file when saving/loading from file
|
||||||
@@ -53,7 +60,7 @@ class ConfigOptions:
|
|||||||
raise KeyError(f"Missing argument: {attr}")
|
raise KeyError(f"Missing argument: {attr}")
|
||||||
|
|
||||||
def validate(self, exclusive=None, inclusive=None, dependent=None, cli=False):
|
def validate(self, exclusive=None, inclusive=None, dependent=None, cli=False):
|
||||||
""" validate combinations of otions
|
"""validate combinations of otions
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
exclusive: list of tuples in form [("option_1", "option_2")...] which are exclusive;
|
exclusive: list of tuples in form [("option_1", "option_2")...] which are exclusive;
|
||||||
@@ -121,7 +128,7 @@ class ConfigOptions:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def write_to_file(self, filename):
|
def write_to_file(self, filename):
|
||||||
""" Write self to TOML file
|
"""Write self to TOML file
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename: full path to TOML file to write; filename will be overwritten if it exists
|
filename: full path to TOML file to write; filename will be overwritten if it exists
|
||||||
@@ -141,7 +148,7 @@ class ConfigOptions:
|
|||||||
toml.dump({self._name: data}, fd)
|
toml.dump({self._name: data}, fd)
|
||||||
|
|
||||||
def load_from_file(self, filename, override=False):
|
def load_from_file(self, filename, override=False):
|
||||||
""" Load options from a TOML file.
|
"""Load options from a TOML file.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filename: full path to TOML file
|
filename: full path to TOML file
|
||||||
|
|||||||
@@ -2,69 +2,71 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
__all__ = ["DateTimeFormatter"]
|
||||||
|
|
||||||
|
|
||||||
class DateTimeFormatter:
|
class DateTimeFormatter:
|
||||||
""" provides property access to formatted datetime.datetime strftime values """
|
"""provides property access to formatted datetime.datetime strftime values"""
|
||||||
|
|
||||||
def __init__(self, dt: datetime.datetime):
|
def __init__(self, dt: datetime.datetime):
|
||||||
self.dt = dt
|
self.dt = dt
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date(self):
|
def date(self):
|
||||||
""" ISO date in form 2020-03-22 """
|
"""ISO date in form 2020-03-22"""
|
||||||
return self.dt.date().isoformat()
|
return self.dt.date().isoformat()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def year(self):
|
def year(self):
|
||||||
""" 4 digit year """
|
"""4 digit year"""
|
||||||
return f"{self.dt.year}"
|
return f"{self.dt.year}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def yy(self):
|
def yy(self):
|
||||||
""" 2 digit year """
|
"""2 digit year"""
|
||||||
return f"{self.dt.strftime('%y')}"
|
return f"{self.dt.strftime('%y')}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mm(self):
|
def mm(self):
|
||||||
""" 2 digit month """
|
"""2 digit month"""
|
||||||
return f"{self.dt.strftime('%m')}"
|
return f"{self.dt.strftime('%m')}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def month(self):
|
def month(self):
|
||||||
""" Month as locale's full name """
|
"""Month as locale's full name"""
|
||||||
return f"{self.dt.strftime('%B')}"
|
return f"{self.dt.strftime('%B')}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mon(self):
|
def mon(self):
|
||||||
""" Month as locale's abbreviated name """
|
"""Month as locale's abbreviated name"""
|
||||||
return f"{self.dt.strftime('%b')}"
|
return f"{self.dt.strftime('%b')}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dd(self):
|
def dd(self):
|
||||||
""" 2-digit day of the month """
|
"""2-digit day of the month"""
|
||||||
return f"{self.dt.strftime('%d')}"
|
return f"{self.dt.strftime('%d')}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dow(self):
|
def dow(self):
|
||||||
""" Day of week as locale's name """
|
"""Day of week as locale's name"""
|
||||||
return f"{self.dt.strftime('%A')}"
|
return f"{self.dt.strftime('%A')}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def doy(self):
|
def doy(self):
|
||||||
""" Julian day of year starting from 001 """
|
"""Julian day of year starting from 001"""
|
||||||
return f"{self.dt.strftime('%j')}"
|
return f"{self.dt.strftime('%j')}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hour(self):
|
def hour(self):
|
||||||
""" 2-digit hour """
|
"""2-digit hour"""
|
||||||
return f"{self.dt.strftime('%H')}"
|
return f"{self.dt.strftime('%H')}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def min(self):
|
def min(self):
|
||||||
""" 2-digit minute """
|
"""2-digit minute"""
|
||||||
return f"{self.dt.strftime('%M')}"
|
return f"{self.dt.strftime('%M')}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sec(self):
|
def sec(self):
|
||||||
""" 2-digit second """
|
"""2-digit second"""
|
||||||
return f"{self.dt.strftime('%S')}"
|
return f"{self.dt.strftime('%S')}"
|
||||||
|
|||||||
@@ -2,9 +2,19 @@
|
|||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"get_local_tz",
|
||||||
|
"datetime_has_tz",
|
||||||
|
"datetime_tz_to_utc",
|
||||||
|
"datetime_remove_tz",
|
||||||
|
"datetime_naive_to_utc",
|
||||||
|
"datetime_naive_to_local",
|
||||||
|
"datetime_utc_to_local",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_local_tz(dt):
|
def get_local_tz(dt):
|
||||||
""" Return local timezone as datetime.timezone tzinfo for dt
|
"""Return local timezone as datetime.timezone tzinfo for dt
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dt: datetime.datetime
|
dt: datetime.datetime
|
||||||
@@ -22,7 +32,7 @@ def get_local_tz(dt):
|
|||||||
|
|
||||||
|
|
||||||
def datetime_has_tz(dt):
|
def datetime_has_tz(dt):
|
||||||
""" Return True if datetime dt has tzinfo else False
|
"""Return True if datetime dt has tzinfo else False
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dt: datetime.datetime
|
dt: datetime.datetime
|
||||||
@@ -41,7 +51,7 @@ def datetime_has_tz(dt):
|
|||||||
|
|
||||||
|
|
||||||
def datetime_tz_to_utc(dt):
|
def datetime_tz_to_utc(dt):
|
||||||
""" Convert datetime.datetime object with timezone to UTC timezone
|
"""Convert datetime.datetime object with timezone to UTC timezone
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dt: datetime.datetime object
|
dt: datetime.datetime object
|
||||||
@@ -64,7 +74,7 @@ def datetime_tz_to_utc(dt):
|
|||||||
|
|
||||||
|
|
||||||
def datetime_remove_tz(dt):
|
def datetime_remove_tz(dt):
|
||||||
""" Remove timezone from a datetime.datetime object
|
"""Remove timezone from a datetime.datetime object
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dt: datetime.datetime object with tzinfo
|
dt: datetime.datetime object with tzinfo
|
||||||
@@ -83,7 +93,7 @@ def datetime_remove_tz(dt):
|
|||||||
|
|
||||||
|
|
||||||
def datetime_naive_to_utc(dt):
|
def datetime_naive_to_utc(dt):
|
||||||
""" Convert naive (timezone unaware) datetime.datetime
|
"""Convert naive (timezone unaware) datetime.datetime
|
||||||
to aware timezone in UTC timezone
|
to aware timezone in UTC timezone
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -111,7 +121,7 @@ def datetime_naive_to_utc(dt):
|
|||||||
|
|
||||||
|
|
||||||
def datetime_naive_to_local(dt):
|
def datetime_naive_to_local(dt):
|
||||||
""" Convert naive (timezone unaware) datetime.datetime
|
"""Convert naive (timezone unaware) datetime.datetime
|
||||||
to aware timezone in local timezone
|
to aware timezone in local timezone
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -139,7 +149,7 @@ def datetime_naive_to_local(dt):
|
|||||||
|
|
||||||
|
|
||||||
def datetime_utc_to_local(dt):
|
def datetime_utc_to_local(dt):
|
||||||
""" Convert datetime.datetime object in UTC timezone to local timezone
|
"""Convert datetime.datetime object in UTC timezone to local timezone
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dt: datetime.datetime object
|
dt: datetime.datetime object
|
||||||
|
|||||||
30
osxphotos/exifinfo.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
""" ExifInfo class to expose EXIF info from the library """
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
__all__ = ["ExifInfo"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ExifInfo:
|
||||||
|
"""EXIF info associated with a photo from the Photos library"""
|
||||||
|
|
||||||
|
flash_fired: bool
|
||||||
|
iso: int
|
||||||
|
metering_mode: int
|
||||||
|
sample_rate: int
|
||||||
|
track_format: int
|
||||||
|
white_balance: int
|
||||||
|
aperture: float
|
||||||
|
bit_rate: float
|
||||||
|
duration: float
|
||||||
|
exposure_bias: float
|
||||||
|
focal_length: float
|
||||||
|
fps: float
|
||||||
|
latitude: float
|
||||||
|
longitude: float
|
||||||
|
shutter_speed: float
|
||||||
|
camera_make: str
|
||||||
|
camera_model: str
|
||||||
|
codec: str
|
||||||
|
lens_model: str
|
||||||
@@ -7,15 +7,27 @@
|
|||||||
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
|
pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """
|
||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
|
import html
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from functools import lru_cache # pylint: disable=syntax-error
|
from functools import lru_cache # pylint: disable=syntax-error
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"escape_str",
|
||||||
|
"exiftool_can_write",
|
||||||
|
"ExifTool",
|
||||||
|
"ExifToolCaching",
|
||||||
|
"get_exiftool_path",
|
||||||
|
"terminate_exiftool",
|
||||||
|
"unescape_str",
|
||||||
|
]
|
||||||
|
|
||||||
# exiftool -stay_open commands outputs this EOF marker after command is run
|
# exiftool -stay_open commands outputs this EOF marker after command is run
|
||||||
EXIFTOOL_STAYOPEN_EOF = "{ready}"
|
EXIFTOOL_STAYOPEN_EOF = "{ready}"
|
||||||
EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
|
EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
|
||||||
@@ -23,17 +35,53 @@ EXIFTOOL_STAYOPEN_EOF_LEN = len(EXIFTOOL_STAYOPEN_EOF)
|
|||||||
# list of exiftool processes to cleanup when exiting or when terminate is called
|
# list of exiftool processes to cleanup when exiting or when terminate is called
|
||||||
EXIFTOOL_PROCESSES = []
|
EXIFTOOL_PROCESSES = []
|
||||||
|
|
||||||
|
# exiftool supported file types, created by utils/exiftool_supported_types.py
|
||||||
|
EXIFTOOL_FILETYPES_JSON = "exiftool_filetypes.json"
|
||||||
|
with (pathlib.Path(__file__).parent / EXIFTOOL_FILETYPES_JSON).open("r") as f:
|
||||||
|
EXIFTOOL_SUPPORTED_FILETYPES = json.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def exiftool_can_write(suffix: str) -> bool:
|
||||||
|
"""Return True if exiftool supports writing to a file with the given suffix, otherwise False"""
|
||||||
|
if not suffix:
|
||||||
|
return False
|
||||||
|
suffix = suffix.lower()
|
||||||
|
if suffix[0] == ".":
|
||||||
|
suffix = suffix[1:]
|
||||||
|
return (
|
||||||
|
suffix in EXIFTOOL_SUPPORTED_FILETYPES
|
||||||
|
and EXIFTOOL_SUPPORTED_FILETYPES[suffix]["write"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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", "
")
|
||||||
|
s = s.replace("\t", "	")
|
||||||
|
s = s.replace("\r", "
")
|
||||||
|
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
|
@atexit.register
|
||||||
def terminate_exiftool():
|
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:
|
for proc in EXIFTOOL_PROCESSES:
|
||||||
proc._stop_proc()
|
proc._stop_proc()
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def get_exiftool_path():
|
def get_exiftool_path():
|
||||||
""" return path of exiftool, cache result """
|
"""return path of exiftool, cache result"""
|
||||||
exiftool_path = shutil.which("exiftool")
|
exiftool_path = shutil.which("exiftool")
|
||||||
if exiftool_path:
|
if exiftool_path:
|
||||||
return exiftool_path.rstrip()
|
return exiftool_path.rstrip()
|
||||||
@@ -49,7 +97,7 @@ class _ExifToolProc:
|
|||||||
Creates a singleton object"""
|
Creates a singleton object"""
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
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:
|
if not hasattr(cls, "instance") or not cls.instance:
|
||||||
cls.instance = super().__new__(cls)
|
cls.instance = super().__new__(cls)
|
||||||
|
|
||||||
@@ -74,7 +122,7 @@ class _ExifToolProc:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def process(self):
|
def process(self):
|
||||||
""" return the exiftool subprocess """
|
"""return the exiftool subprocess"""
|
||||||
if self._process_running:
|
if self._process_running:
|
||||||
return self._process
|
return self._process
|
||||||
else:
|
else:
|
||||||
@@ -83,16 +131,16 @@ class _ExifToolProc:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def pid(self):
|
def pid(self):
|
||||||
""" return process id (PID) of the exiftool process """
|
"""return process id (PID) of the exiftool process"""
|
||||||
return self._process.pid
|
return self._process.pid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def exiftool(self):
|
def exiftool(self):
|
||||||
""" return path to exiftool process """
|
"""return path to exiftool process"""
|
||||||
return self._exiftool
|
return self._exiftool
|
||||||
|
|
||||||
def _start_proc(self):
|
def _start_proc(self):
|
||||||
""" start exiftool in batch mode """
|
"""start exiftool in batch mode"""
|
||||||
|
|
||||||
if self._process_running:
|
if self._process_running:
|
||||||
logging.warning("exiftool already running: {self._process}")
|
logging.warning("exiftool already running: {self._process}")
|
||||||
@@ -110,6 +158,7 @@ class _ExifToolProc:
|
|||||||
"-n", # no print conversion (e.g. print tag values in machine readable format)
|
"-n", # no print conversion (e.g. print tag values in machine readable format)
|
||||||
"-P", # Preserve file modification date/time
|
"-P", # Preserve file modification date/time
|
||||||
"-G", # print group name for each tag
|
"-G", # print group name for each tag
|
||||||
|
"-E", # escape tag values for HTML (allows use of HTML 
 for newlines)
|
||||||
],
|
],
|
||||||
stdin=subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
@@ -120,7 +169,7 @@ class _ExifToolProc:
|
|||||||
EXIFTOOL_PROCESSES.append(self)
|
EXIFTOOL_PROCESSES.append(self)
|
||||||
|
|
||||||
def _stop_proc(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:
|
if not self._process_running:
|
||||||
return
|
return
|
||||||
@@ -143,7 +192,7 @@ class _ExifToolProc:
|
|||||||
|
|
||||||
|
|
||||||
class ExifTool:
|
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):
|
def __init__(self, filepath, exiftool=None, overwrite=True, flags=None):
|
||||||
"""Create ExifTool object
|
"""Create ExifTool object
|
||||||
@@ -189,6 +238,7 @@ class ExifTool:
|
|||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
value = ""
|
value = ""
|
||||||
|
value = escape_str(value)
|
||||||
command = [f"-{tag}={value}"]
|
command = [f"-{tag}={value}"]
|
||||||
if self.overwrite and not self._context_mgr:
|
if self.overwrite and not self._context_mgr:
|
||||||
command.append("-overwrite_original")
|
command.append("-overwrite_original")
|
||||||
@@ -233,6 +283,7 @@ class ExifTool:
|
|||||||
for value in values:
|
for value in values:
|
||||||
if value is None:
|
if value is None:
|
||||||
raise ValueError("Can't add None value to tag")
|
raise ValueError("Can't add None value to tag")
|
||||||
|
value = escape_str(value)
|
||||||
command.append(f"-{tag}+={value}")
|
command.append(f"-{tag}+={value}")
|
||||||
|
|
||||||
if self.overwrite and not self._context_mgr:
|
if self.overwrite and not self._context_mgr:
|
||||||
@@ -315,12 +366,12 @@ class ExifTool:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def pid(self):
|
def pid(self):
|
||||||
""" return process id (PID) of the exiftool process """
|
"""return process id (PID) of the exiftool process"""
|
||||||
return self._process.pid
|
return self._process.pid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self):
|
def version(self):
|
||||||
""" returns exiftool version """
|
"""returns exiftool version"""
|
||||||
ver, _, _ = self.run_commands("-ver", no_file=True)
|
ver, _, _ = self.run_commands("-ver", no_file=True)
|
||||||
return ver.decode("utf-8")
|
return ver.decode("utf-8")
|
||||||
|
|
||||||
@@ -335,6 +386,7 @@ class ExifTool:
|
|||||||
json_str, _, _ = self.run_commands("-json")
|
json_str, _, _ = self.run_commands("-json")
|
||||||
if not json_str:
|
if not json_str:
|
||||||
return dict()
|
return dict()
|
||||||
|
json_str = unescape_str(json_str.decode("utf-8"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
exifdict = json.loads(json_str)
|
exifdict = json.loads(json_str)
|
||||||
@@ -342,7 +394,6 @@ class ExifTool:
|
|||||||
# will fail with some commands, e.g --ext AVI which produces
|
# will fail with some commands, e.g --ext AVI which produces
|
||||||
# 'No file with specified extension' instead of json
|
# 'No file with specified extension' instead of json
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
exifdict = exifdict[0]
|
exifdict = exifdict[0]
|
||||||
if not tag_groups:
|
if not tag_groups:
|
||||||
# strip tag groups
|
# strip tag groups
|
||||||
@@ -358,12 +409,13 @@ class ExifTool:
|
|||||||
return exifdict
|
return exifdict
|
||||||
|
|
||||||
def json(self):
|
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, _, _ = self.run_commands("-json")
|
||||||
|
json = unescape_str(json.decode("utf-8"))
|
||||||
return json
|
return json
|
||||||
|
|
||||||
def _read_exif(self):
|
def _read_exif(self):
|
||||||
""" read exif data from file """
|
"""read exif data from file"""
|
||||||
data = self.asdict()
|
data = self.asdict()
|
||||||
self.data = {k: v for k, v in data.items()}
|
self.data = {k: v for k, v in data.items()}
|
||||||
|
|
||||||
@@ -384,15 +436,15 @@ class ExifTool:
|
|||||||
|
|
||||||
|
|
||||||
class ExifToolCaching(ExifTool):
|
class ExifToolCaching(ExifTool):
|
||||||
""" Basic exiftool interface for reading and writing EXIF tags, with caching.
|
"""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.
|
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 """
|
Creates a singleton cached ExifTool instance"""
|
||||||
|
|
||||||
_singletons = {}
|
_singletons = {}
|
||||||
|
|
||||||
def __new__(cls, filepath, exiftool=None):
|
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:
|
if filepath not in cls._singletons:
|
||||||
cls._singletons[filepath] = _ExifToolCaching(filepath, exiftool=exiftool)
|
cls._singletons[filepath] = _ExifToolCaching(filepath, exiftool=exiftool)
|
||||||
return cls._singletons[filepath]
|
return cls._singletons[filepath]
|
||||||
@@ -448,7 +500,6 @@ class _ExifToolCaching(ExifTool):
|
|||||||
return self._asdict_cache[tag_groups][normalized]
|
return self._asdict_cache[tag_groups][normalized]
|
||||||
|
|
||||||
def flush_cache(self):
|
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._json_cache = None
|
||||||
self._asdict_cache = {}
|
self._asdict_cache = {}
|
||||||
|
|
||||||
|
|||||||
4976
osxphotos/exiftool_filetypes.json
Normal file
@@ -1,5 +1,6 @@
|
|||||||
""" 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 datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -9,12 +10,18 @@ import sys
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from sqlite3 import Error
|
from sqlite3 import Error
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from ._constants import OSXPHOTOS_EXPORT_DB
|
from ._constants import OSXPHOTOS_EXPORT_DB
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
|
from .utils import normalize_fs_path
|
||||||
|
|
||||||
OSXPHOTOS_EXPORTDB_VERSION = "4.0"
|
__all__ = ["ExportDB_ABC", "ExportDBNoOp", "ExportDB", "ExportDBInMemory"]
|
||||||
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {str(datetime.datetime.now())}"
|
|
||||||
|
OSXPHOTOS_EXPORTDB_VERSION = "4.3"
|
||||||
|
OSXPHOTOS_EXPORTDB_VERSION_MIGRATE_FILEPATH = "4.3"
|
||||||
|
|
||||||
|
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
|
||||||
|
|
||||||
|
|
||||||
class ExportDB_ABC(ABC):
|
class ExportDB_ABC(ABC):
|
||||||
@@ -101,15 +108,18 @@ class ExportDB_ABC(ABC):
|
|||||||
self,
|
self,
|
||||||
filename,
|
filename,
|
||||||
uuid,
|
uuid,
|
||||||
orig_stat,
|
orig_stat=None,
|
||||||
exif_stat,
|
exif_stat=None,
|
||||||
converted_stat,
|
converted_stat=None,
|
||||||
edited_stat,
|
edited_stat=None,
|
||||||
info_json,
|
info_json=None,
|
||||||
exif_json,
|
exif_json=None,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_connection(self):
|
||||||
|
pass
|
||||||
|
|
||||||
class ExportDBNoOp(ExportDB_ABC):
|
class ExportDBNoOp(ExportDB_ABC):
|
||||||
"""An ExportDB with NoOp methods"""
|
"""An ExportDB with NoOp methods"""
|
||||||
@@ -180,28 +190,29 @@ class ExportDBNoOp(ExportDB_ABC):
|
|||||||
self,
|
self,
|
||||||
filename,
|
filename,
|
||||||
uuid,
|
uuid,
|
||||||
orig_stat,
|
orig_stat=None,
|
||||||
exif_stat,
|
exif_stat=None,
|
||||||
converted_stat,
|
converted_stat=None,
|
||||||
edited_stat,
|
edited_stat=None,
|
||||||
info_json,
|
info_json=None,
|
||||||
exif_json,
|
exif_json=None,
|
||||||
):
|
):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
pass
|
||||||
|
|
||||||
class ExportDB(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):
|
def __init__(self, dbfile, export_dir):
|
||||||
"""dbfile: path to osxphotos export database file"""
|
"""dbfile: path to osxphotos export database file"""
|
||||||
self._dbfile = dbfile
|
self._dbfile = dbfile
|
||||||
# _path is parent of the database
|
# export_dir is required as all files referenced by get_/set_uuid_for_file will be converted to
|
||||||
# all files referenced by get_/set_uuid_for_file will be converted to
|
# relative paths to this path
|
||||||
# relative paths to this parent _path
|
|
||||||
# this allows the entire export tree to be moved to a new disk/location
|
# this allows the entire export tree to be moved to a new disk/location
|
||||||
# whilst preserving the UUID to filename mapping
|
# whilst preserving the UUID to filename mapping
|
||||||
self._path = pathlib.Path(dbfile).parent
|
self._path = export_dir
|
||||||
self._conn = self._open_export_db(dbfile)
|
self._conn = self._open_export_db(dbfile)
|
||||||
self._insert_run_info()
|
self._insert_run_info()
|
||||||
|
|
||||||
@@ -209,32 +220,33 @@ class ExportDB(ExportDB_ABC):
|
|||||||
"""query database for filename and return UUID
|
"""query database for filename and return UUID
|
||||||
returns None if filename not found in database
|
returns None if filename not found in database
|
||||||
"""
|
"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filepath_normalized = self._normalize_filepath_relative(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
f"SELECT uuid FROM files WHERE filepath_normalized = ?", (filename,)
|
"SELECT uuid FROM files WHERE filepath_normalized = ?",
|
||||||
|
(filepath_normalized,),
|
||||||
)
|
)
|
||||||
results = c.fetchone()
|
results = c.fetchone()
|
||||||
uuid = results[0] if results else None
|
uuid = results[0] if results else None
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
uuid = None
|
uuid = None
|
||||||
|
|
||||||
return uuid
|
return uuid
|
||||||
|
|
||||||
def set_uuid_for_file(self, filename, 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 = str(pathlib.Path(filename).relative_to(self._path))
|
||||||
filename_normalized = filename.lower()
|
filename_normalized = self._normalize_filepath(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
f"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
|
"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
|
||||||
(filename, filename_normalized, uuid),
|
(filename, filename_normalized, uuid),
|
||||||
)
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
@@ -243,11 +255,11 @@ class ExportDB(ExportDB_ABC):
|
|||||||
"""set stat info for filename
|
"""set stat info for filename
|
||||||
filename: filename to set the stat info for
|
filename: filename to set the stat info for
|
||||||
stat: a tuple of length 3: mode, size, mtime"""
|
stat: a tuple of length 3: mode, size, mtime"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
if len(stats) != 3:
|
if len(stats) != 3:
|
||||||
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
||||||
|
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -264,8 +276,8 @@ class ExportDB(ExportDB_ABC):
|
|||||||
"""get stat info for filename
|
"""get stat info for filename
|
||||||
returns: tuple of (mode, size, mtime)
|
returns: tuple of (mode, size, mtime)
|
||||||
"""
|
"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -274,15 +286,14 @@ class ExportDB(ExportDB_ABC):
|
|||||||
)
|
)
|
||||||
results = c.fetchone()
|
results = c.fetchone()
|
||||||
if results:
|
if results:
|
||||||
stats = results[0:3]
|
stats = results[:3]
|
||||||
mtime = int(stats[2]) if stats[2] is not None else None
|
mtime = int(stats[2]) if stats[2] is not None else None
|
||||||
stats = (stats[0], stats[1], mtime)
|
stats = (stats[0], stats[1], mtime)
|
||||||
else:
|
else:
|
||||||
stats = (None, None, None)
|
stats = (None, None, None)
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
stats = (None, None, None)
|
stats = None, None, None
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def set_stat_edited_for_file(self, filename, stats):
|
def set_stat_edited_for_file(self, filename, stats):
|
||||||
@@ -301,11 +312,11 @@ class ExportDB(ExportDB_ABC):
|
|||||||
"""set stat info for filename (after exiftool has updated it)
|
"""set stat info for filename (after exiftool has updated it)
|
||||||
filename: filename to set the stat info for
|
filename: filename to set the stat info for
|
||||||
stat: a tuple of length 3: mode, size, mtime"""
|
stat: a tuple of length 3: mode, size, mtime"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
if len(stats) != 3:
|
if len(stats) != 3:
|
||||||
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
||||||
|
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -322,8 +333,8 @@ class ExportDB(ExportDB_ABC):
|
|||||||
"""get stat info for filename (after exiftool has updated it)
|
"""get stat info for filename (after exiftool has updated it)
|
||||||
returns: tuple of (mode, size, mtime)
|
returns: tuple of (mode, size, mtime)
|
||||||
"""
|
"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -332,15 +343,14 @@ class ExportDB(ExportDB_ABC):
|
|||||||
)
|
)
|
||||||
results = c.fetchone()
|
results = c.fetchone()
|
||||||
if results:
|
if results:
|
||||||
stats = results[0:3]
|
stats = results[:3]
|
||||||
mtime = int(stats[2]) if stats[2] is not None else None
|
mtime = int(stats[2]) if stats[2] is not None else None
|
||||||
stats = (stats[0], stats[1], mtime)
|
stats = (stats[0], stats[1], mtime)
|
||||||
else:
|
else:
|
||||||
stats = (None, None, None)
|
stats = (None, None, None)
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
stats = (None, None, None)
|
stats = None, None, None
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def set_stat_converted_for_file(self, filename, stats):
|
def set_stat_converted_for_file(self, filename, stats):
|
||||||
@@ -357,7 +367,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def get_info_for_uuid(self, uuid):
|
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
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT json_info FROM info WHERE uuid = ?", (uuid,))
|
c.execute("SELECT json_info FROM info WHERE uuid = ?", (uuid,))
|
||||||
@@ -371,7 +381,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def set_info_for_uuid(self, uuid, 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
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -384,8 +394,8 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def get_exifdata_for_file(self, filename):
|
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()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -402,8 +412,8 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def set_exifdata_for_file(self, filename, 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()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -416,8 +426,8 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def get_sidecar_for_file(self, filename):
|
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()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -444,8 +454,8 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def set_sidecar_for_file(self, filename, 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()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -458,7 +468,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def get_previous_uuids(self):
|
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
|
conn = self.get_connection()
|
||||||
previous_uuids = []
|
previous_uuids = []
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
@@ -471,7 +481,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def get_detected_text_for_uuid(self, uuid):
|
def get_detected_text_for_uuid(self, uuid):
|
||||||
"""Get the detected_text for a uuid"""
|
"""Get the detected_text for a uuid"""
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -488,12 +498,15 @@ class ExportDB(ExportDB_ABC):
|
|||||||
|
|
||||||
def set_detected_text_for_uuid(self, uuid, text_json):
|
def set_detected_text_for_uuid(self, uuid, text_json):
|
||||||
"""Set the detected text for uuid"""
|
"""Set the detected text for uuid"""
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
"INSERT OR REPLACE INTO detected_text(uuid, text_data) VALUES (?, ?);",
|
"INSERT OR REPLACE INTO detected_text(uuid, text_data) VALUES (?, ?);",
|
||||||
(uuid, text_json,),
|
(
|
||||||
|
uuid,
|
||||||
|
text_json,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
@@ -503,51 +516,65 @@ class ExportDB(ExportDB_ABC):
|
|||||||
self,
|
self,
|
||||||
filename,
|
filename,
|
||||||
uuid,
|
uuid,
|
||||||
orig_stat,
|
orig_stat=None,
|
||||||
exif_stat,
|
exif_stat=None,
|
||||||
converted_stat,
|
converted_stat=None,
|
||||||
edited_stat,
|
edited_stat=None,
|
||||||
info_json,
|
info_json=None,
|
||||||
exif_json,
|
exif_json=None,
|
||||||
):
|
):
|
||||||
"""sets all the data for file and uuid at once"""
|
"""sets all the data for file and uuid at once; if any value is None, does not set it"""
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path))
|
filename = str(pathlib.Path(filename).relative_to(self._path))
|
||||||
filename_normalized = filename.lower()
|
filename_normalized = self._normalize_filepath(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
# update files table (if needed);
|
||||||
|
# this statement works around fact that there was no unique constraint on files.filepath_normalized
|
||||||
c.execute(
|
c.execute(
|
||||||
f"INSERT OR REPLACE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);",
|
"""INSERT OR IGNORE INTO files(filepath, filepath_normalized, uuid) VALUES (?, ?, ?);""",
|
||||||
(filename, filename_normalized, uuid),
|
(filename, filename_normalized, uuid),
|
||||||
)
|
)
|
||||||
c.execute(
|
|
||||||
"UPDATE files "
|
if orig_stat is not None:
|
||||||
+ "SET orig_mode = ?, orig_size = ?, orig_mtime = ? "
|
c.execute(
|
||||||
+ "WHERE filepath_normalized = ?;",
|
"UPDATE files "
|
||||||
(*orig_stat, filename_normalized),
|
+ "SET orig_mode = ?, orig_size = ?, orig_mtime = ? "
|
||||||
)
|
+ "WHERE filepath_normalized = ?;",
|
||||||
c.execute(
|
(*orig_stat, filename_normalized),
|
||||||
"UPDATE files "
|
)
|
||||||
+ "SET exif_mode = ?, exif_size = ?, exif_mtime = ? "
|
|
||||||
+ "WHERE filepath_normalized = ?;",
|
if exif_stat is not None:
|
||||||
(*exif_stat, filename_normalized),
|
c.execute(
|
||||||
)
|
"UPDATE files "
|
||||||
c.execute(
|
+ "SET exif_mode = ?, exif_size = ?, exif_mtime = ? "
|
||||||
"INSERT OR REPLACE INTO converted(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
|
+ "WHERE filepath_normalized = ?;",
|
||||||
(filename_normalized, *converted_stat),
|
(*exif_stat, filename_normalized),
|
||||||
)
|
)
|
||||||
c.execute(
|
|
||||||
"INSERT OR REPLACE INTO edited(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
|
if converted_stat is not None:
|
||||||
(filename_normalized, *edited_stat),
|
c.execute(
|
||||||
)
|
"INSERT OR REPLACE INTO converted(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
|
||||||
c.execute(
|
(filename_normalized, *converted_stat),
|
||||||
"INSERT OR REPLACE INTO info(uuid, json_info) VALUES (?, ?);",
|
)
|
||||||
(uuid, info_json),
|
|
||||||
)
|
if edited_stat is not None:
|
||||||
c.execute(
|
c.execute(
|
||||||
"INSERT OR REPLACE INTO exifdata(filepath_normalized, json_exifdata) VALUES (?, ?);",
|
"INSERT OR REPLACE INTO edited(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
|
||||||
(filename_normalized, exif_json),
|
(filename_normalized, *edited_stat),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if info_json is not None:
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO info(uuid, json_info) VALUES (?, ?);",
|
||||||
|
(uuid, info_json),
|
||||||
|
)
|
||||||
|
|
||||||
|
if exif_json is not None:
|
||||||
|
c.execute(
|
||||||
|
"INSERT OR REPLACE INTO exifdata(filepath_normalized, json_exifdata) VALUES (?, ?);",
|
||||||
|
(filename_normalized, exif_json),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
@@ -555,16 +582,23 @@ class ExportDB(ExportDB_ABC):
|
|||||||
def close(self):
|
def close(self):
|
||||||
"""close the database connection"""
|
"""close the database connection"""
|
||||||
try:
|
try:
|
||||||
self._conn.close()
|
if self._conn:
|
||||||
|
self._conn.close()
|
||||||
|
self._conn = None
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
|
|
||||||
|
def get_connection(self):
|
||||||
|
if self._conn is None:
|
||||||
|
self._conn = self._open_export_db(self._dbfile)
|
||||||
|
return self._conn
|
||||||
|
|
||||||
def _set_stat_for_file(self, table, filename, stats):
|
def _set_stat_for_file(self, table, filename, stats):
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
if len(stats) != 3:
|
if len(stats) != 3:
|
||||||
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
raise ValueError(f"expected 3 elements for stat, got {len(stats)}")
|
||||||
|
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
f"INSERT OR REPLACE INTO {table}(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
|
f"INSERT OR REPLACE INTO {table}(filepath_normalized, mode, size, mtime) VALUES (?, ?, ?, ?);",
|
||||||
@@ -573,8 +607,8 @@ class ExportDB(ExportDB_ABC):
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def _get_stat_for_file(self, table, filename):
|
def _get_stat_for_file(self, table, filename):
|
||||||
filename = str(pathlib.Path(filename).relative_to(self._path)).lower()
|
filename = self._normalize_filepath_relative(filename)
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
f"SELECT mode, size, mtime FROM {table} WHERE filepath_normalized = ?",
|
f"SELECT mode, size, mtime FROM {table} WHERE filepath_normalized = ?",
|
||||||
@@ -582,7 +616,7 @@ class ExportDB(ExportDB_ABC):
|
|||||||
)
|
)
|
||||||
results = c.fetchone()
|
results = c.fetchone()
|
||||||
if results:
|
if results:
|
||||||
stats = results[0:3]
|
stats = results[:3]
|
||||||
mtime = int(stats[2]) if stats[2] is not None else None
|
mtime = int(stats[2]) if stats[2] is not None else None
|
||||||
stats = (stats[0], stats[1], mtime)
|
stats = (stats[0], stats[1], mtime)
|
||||||
else:
|
else:
|
||||||
@@ -609,10 +643,20 @@ class ExportDB(ExportDB_ABC):
|
|||||||
version_info = self._get_database_version(conn)
|
version_info = self._get_database_version(conn)
|
||||||
if version_info[1] < OSXPHOTOS_EXPORTDB_VERSION:
|
if version_info[1] < OSXPHOTOS_EXPORTDB_VERSION:
|
||||||
self._create_db_tables(conn)
|
self._create_db_tables(conn)
|
||||||
|
if version_info[1] < OSXPHOTOS_EXPORTDB_VERSION_MIGRATE_FILEPATH:
|
||||||
|
self._migrate_normalized_filepath(conn)
|
||||||
self.was_upgraded = (version_info[1], OSXPHOTOS_EXPORTDB_VERSION)
|
self.was_upgraded = (version_info[1], OSXPHOTOS_EXPORTDB_VERSION)
|
||||||
else:
|
else:
|
||||||
self.was_upgraded = ()
|
self.was_upgraded = ()
|
||||||
self.version = OSXPHOTOS_EXPORTDB_VERSION
|
self.version = OSXPHOTOS_EXPORTDB_VERSION
|
||||||
|
|
||||||
|
# turn on performance optimizations
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("PRAGMA journal_mode=WAL;")
|
||||||
|
c.execute("PRAGMA synchronous=NORMAL;")
|
||||||
|
c.execute("PRAGMA cache_size=-100000;")
|
||||||
|
c.execute("PRAGMA temp_store=MEMORY;")
|
||||||
|
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
def _get_db_connection(self, dbfile):
|
def _get_db_connection(self, dbfile):
|
||||||
@@ -658,6 +702,22 @@ class ExportDB(ExportDB_ABC):
|
|||||||
exif_size INTEGER,
|
exif_size INTEGER,
|
||||||
exif_mtime REAL
|
exif_mtime REAL
|
||||||
); """,
|
); """,
|
||||||
|
"sql_files_table_migrate": """ CREATE TABLE IF NOT EXISTS files_migrate (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
filepath TEXT NOT NULL,
|
||||||
|
filepath_normalized TEXT NOT NULL,
|
||||||
|
uuid TEXT,
|
||||||
|
orig_mode INTEGER,
|
||||||
|
orig_size INTEGER,
|
||||||
|
orig_mtime REAL,
|
||||||
|
exif_mode INTEGER,
|
||||||
|
exif_size INTEGER,
|
||||||
|
exif_mtime REAL,
|
||||||
|
UNIQUE(filepath_normalized)
|
||||||
|
); """,
|
||||||
|
"sql_files_migrate": """ INSERT INTO files_migrate SELECT * FROM files;""",
|
||||||
|
"sql_files_drop_tables": """ DROP TABLE files;""",
|
||||||
|
"sql_files_alter": """ ALTER TABLE files_migrate RENAME TO files;""",
|
||||||
"sql_runs_table": """ CREATE TABLE IF NOT EXISTS runs (
|
"sql_runs_table": """ CREATE TABLE IF NOT EXISTS runs (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
datetime TEXT,
|
datetime TEXT,
|
||||||
@@ -737,17 +797,44 @@ class ExportDB(ExportDB_ABC):
|
|||||||
cmd = sys.argv[0]
|
cmd = sys.argv[0]
|
||||||
args = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else ""
|
args = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else ""
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
conn = self._conn
|
conn = self.get_connection()
|
||||||
try:
|
try:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute(
|
c.execute(
|
||||||
f"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)",
|
"INSERT INTO runs (datetime, python_path, script_name, args, cwd) VALUES (?, ?, ?, ?, ?)",
|
||||||
(dt, python_path, cmd, args, cwd),
|
(dt, python_path, cmd, args, cwd),
|
||||||
)
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Error as e:
|
except Error as e:
|
||||||
logging.warning(e)
|
logging.warning(e)
|
||||||
|
|
||||||
|
def _normalize_filepath(self, filepath: Union[str, pathlib.Path]) -> str:
|
||||||
|
"""normalize filepath for unicode, lower case"""
|
||||||
|
return normalize_fs_path(str(filepath)).lower()
|
||||||
|
|
||||||
|
def _normalize_filepath_relative(self, filepath: Union[str, pathlib.Path]) -> str:
|
||||||
|
"""normalize filepath for unicode, relative path (to export dir), lower case"""
|
||||||
|
filepath = str(pathlib.Path(filepath).relative_to(self._path))
|
||||||
|
return normalize_fs_path(str(filepath)).lower()
|
||||||
|
|
||||||
|
def _migrate_normalized_filepath(self, conn):
|
||||||
|
"""Fix all filepath_normalized columns for unicode normalization"""
|
||||||
|
# Prior to database version 4.3, filepath_normalized was not normalized for unicode
|
||||||
|
c = conn.cursor()
|
||||||
|
for table in ["converted", "edited", "exifdata", "files", "sidecar"]:
|
||||||
|
old_values = c.execute(
|
||||||
|
f"SELECT filepath_normalized, id FROM {table}"
|
||||||
|
).fetchall()
|
||||||
|
new_values = [
|
||||||
|
(self._normalize_filepath(filepath_normalized), id_)
|
||||||
|
for filepath_normalized, id_ in old_values
|
||||||
|
]
|
||||||
|
c.executemany(
|
||||||
|
f"UPDATE {table} SET filepath_normalized=? WHERE id=?", new_values
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
|
||||||
class ExportDBInMemory(ExportDB):
|
class ExportDBInMemory(ExportDB):
|
||||||
"""In memory version of ExportDB
|
"""In memory version of ExportDB
|
||||||
@@ -755,14 +842,13 @@ class ExportDBInMemory(ExportDB):
|
|||||||
modifying the on-disk version
|
modifying the on-disk version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, dbfile):
|
def __init__(self, dbfile, export_dir):
|
||||||
self._dbfile = dbfile or f"./{OSXPHOTOS_EXPORT_DB}"
|
self._dbfile = dbfile or f"./{OSXPHOTOS_EXPORT_DB}"
|
||||||
# _path is parent of the database
|
# export_dir is required as all files referenced by get_/set_uuid_for_file will be converted to
|
||||||
# all files referenced by get_/set_uuid_for_file will be converted to
|
# relative paths to this path
|
||||||
# relative paths to this parent _path
|
|
||||||
# this allows the entire export tree to be moved to a new disk/location
|
# this allows the entire export tree to be moved to a new disk/location
|
||||||
# whilst preserving the UUID to filename mapping
|
# whilst preserving the UUID to filename mapping
|
||||||
self._path = pathlib.Path(self._dbfile).parent
|
self._path = export_dir
|
||||||
self._conn = self._open_export_db(self._dbfile)
|
self._conn = self._open_export_db(self._dbfile)
|
||||||
self._insert_run_info()
|
self._insert_run_info()
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
import CoreFoundation
|
import Foundation
|
||||||
|
|
||||||
from .imageconverter import ImageConverter
|
from .imageconverter import ImageConverter
|
||||||
|
|
||||||
|
__all__ = ["FileUtilABC", "FileUtilMacOS", "FileUtil", "FileUtilNoOp"]
|
||||||
|
|
||||||
|
|
||||||
class FileUtilABC(ABC):
|
class FileUtilABC(ABC):
|
||||||
""" Abstract base class for FileUtil """
|
"""Abstract base class for FileUtil"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -67,14 +69,14 @@ class FileUtilABC(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class FileUtilMacOS(FileUtilABC):
|
class FileUtilMacOS(FileUtilABC):
|
||||||
""" Various file utilities """
|
"""Various file utilities"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def hardlink(cls, src, dest):
|
def hardlink(cls, src, dest):
|
||||||
""" Hardlinks a file from src path to dest path
|
"""Hardlinks a file from src path to dest path
|
||||||
src: source path as string
|
src: source path as string
|
||||||
dest: destination path as string
|
dest: destination path as string
|
||||||
Raises exception if linking fails or either path is None """
|
Raises exception if linking fails or either path is None"""
|
||||||
|
|
||||||
if src is None or dest is None:
|
if src is None or dest is None:
|
||||||
raise ValueError("src and dest must not be None", src, dest)
|
raise ValueError("src and dest must not be None", src, dest)
|
||||||
@@ -90,7 +92,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def copy(cls, src, dest):
|
def copy(cls, src, dest):
|
||||||
""" Copies a file from src path to dest path
|
"""Copies a file from src path to dest path
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
src: source path as string; must be a valid file path
|
src: source path as string; must be a valid file path
|
||||||
@@ -114,7 +116,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
if dest.is_dir():
|
if dest.is_dir():
|
||||||
dest /= src.name
|
dest /= src.name
|
||||||
|
|
||||||
filemgr = CoreFoundation.NSFileManager.defaultManager()
|
filemgr = Foundation.NSFileManager.defaultManager()
|
||||||
error = filemgr.copyItemAtPath_toPath_error_(str(src), str(dest), None)
|
error = filemgr.copyItemAtPath_toPath_error_(str(src), str(dest), None)
|
||||||
# error is a tuple of (bool, error_string)
|
# error is a tuple of (bool, error_string)
|
||||||
# error[0] is True if copy succeeded
|
# error[0] is True if copy succeeded
|
||||||
@@ -124,7 +126,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unlink(cls, filepath):
|
def unlink(cls, filepath):
|
||||||
""" unlink filepath; if it's pathlib.Path, use Path.unlink, otherwise use os.unlink """
|
"""unlink filepath; if it's pathlib.Path, use Path.unlink, otherwise use os.unlink"""
|
||||||
if isinstance(filepath, pathlib.Path):
|
if isinstance(filepath, pathlib.Path):
|
||||||
filepath.unlink()
|
filepath.unlink()
|
||||||
else:
|
else:
|
||||||
@@ -132,7 +134,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def rmdir(cls, dirpath):
|
def rmdir(cls, dirpath):
|
||||||
""" remove directory filepath; dirpath must be empty """
|
"""remove directory filepath; dirpath must be empty"""
|
||||||
if isinstance(dirpath, pathlib.Path):
|
if isinstance(dirpath, pathlib.Path):
|
||||||
dirpath.rmdir()
|
dirpath.rmdir()
|
||||||
else:
|
else:
|
||||||
@@ -140,7 +142,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def utime(cls, path, times):
|
def utime(cls, path, times):
|
||||||
""" Set the access and modified time of path. """
|
"""Set the access and modified time of path."""
|
||||||
os.utime(path, times)
|
os.utime(path, times)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -179,27 +181,26 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
s1 = cls._sig(os.stat(f1))
|
s1 = cls._sig(os.stat(f1))
|
||||||
|
|
||||||
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
|
if s1[0] != stat.S_IFREG or s2[0] != stat.S_IFREG:
|
||||||
return False
|
return False
|
||||||
return s1 == s2
|
return s1 == s2
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def file_sig(cls, f1):
|
def file_sig(cls, f1):
|
||||||
""" return os.stat signature for file f1 """
|
"""return os.stat signature for file f1"""
|
||||||
return cls._sig(os.stat(f1))
|
return cls._sig(os.stat(f1))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
|
def convert_to_jpeg(cls, src_file, dest_file, compression_quality=1.0):
|
||||||
""" converts image file src_file to jpeg format as dest_file
|
"""converts image file src_file to jpeg format as dest_file
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
src_file: image file to convert
|
src_file: image file to convert
|
||||||
dest_file: destination path to write converted file to
|
dest_file: destination path to write converted file to
|
||||||
compression quality: JPEG compression quality in range 0.0 <= compression_quality <= 1.0; default 1.0 (best quality)
|
compression quality: JPEG compression quality in range 0.0 <= compression_quality <= 1.0; default 1.0 (best quality)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if success, otherwise False
|
True if success, otherwise False
|
||||||
"""
|
"""
|
||||||
converter = ImageConverter()
|
converter = ImageConverter()
|
||||||
return converter.write_jpeg(
|
return converter.write_jpeg(
|
||||||
@@ -208,7 +209,7 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def rename(cls, src, dest):
|
def rename(cls, src, dest):
|
||||||
""" Copy src to dest
|
"""Copy src to dest
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
src: path to source file
|
src: path to source file
|
||||||
@@ -223,25 +224,25 @@ class FileUtilMacOS(FileUtilABC):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sig(st):
|
def _sig(st):
|
||||||
""" return tuple of (mode, size, mtime) of file based on os.stat
|
"""return tuple of (mode, size, mtime) of file based on os.stat
|
||||||
Args:
|
Args:
|
||||||
st: os.stat signature
|
st: os.stat signature
|
||||||
"""
|
"""
|
||||||
# use int(st.st_mtime) because ditto does not copy fractional portion of mtime
|
# use int(st.st_mtime) because ditto does not copy fractional portion of mtime
|
||||||
return (stat.S_IFMT(st.st_mode), st.st_size, int(st.st_mtime))
|
return (stat.S_IFMT(st.st_mode), st.st_size, int(st.st_mtime))
|
||||||
|
|
||||||
|
|
||||||
class FileUtil(FileUtilMacOS):
|
class FileUtil(FileUtilMacOS):
|
||||||
""" Various file utilities """
|
"""Various file utilities"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class FileUtilNoOp(FileUtil):
|
class FileUtilNoOp(FileUtil):
|
||||||
""" No-Op implementation of FileUtil for testing / dry-run mode
|
"""No-Op implementation of FileUtil for testing / dry-run mode
|
||||||
all methods with exception of cmp, cmp_file_sig and file_cmp are no-op
|
all methods with exception of cmp, cmp_file_sig and file_cmp are no-op
|
||||||
cmp and cmp_file_sig functions as FileUtil methods do
|
cmp and cmp_file_sig functions as FileUtil methods do
|
||||||
file_cmp returns mock data
|
file_cmp returns mock data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -15,27 +15,29 @@ from Foundation import NSDictionary
|
|||||||
# needed to capture system-level stderr
|
# needed to capture system-level stderr
|
||||||
from wurlitzer import pipes
|
from wurlitzer import pipes
|
||||||
|
|
||||||
|
__all__ = ["ImageConversionError", "ImageConverter"]
|
||||||
|
|
||||||
|
|
||||||
class ImageConversionError(Exception):
|
class ImageConversionError(Exception):
|
||||||
"""Base class for exceptions in this module. """
|
"""Base class for exceptions in this module."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ImageConverter:
|
class ImageConverter:
|
||||||
""" Convert images to jpeg. This class is a singleton
|
"""Convert images to jpeg. This class is a singleton
|
||||||
which will re-use the Core Image CIContext to avoid
|
which will re-use the Core Image CIContext to avoid
|
||||||
creating a new context for every conversion. """
|
creating a new context for every conversion."""
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
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:
|
if not hasattr(cls, "instance") or not cls.instance:
|
||||||
cls.instance = super().__new__(cls)
|
cls.instance = super().__new__(cls)
|
||||||
|
|
||||||
return cls.instance
|
return cls.instance
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
""" return existing singleton or create a new one """
|
"""return existing singleton or create a new one"""
|
||||||
|
|
||||||
if hasattr(self, "context"):
|
if hasattr(self, "context"):
|
||||||
return
|
return
|
||||||
@@ -47,13 +49,10 @@ class ImageConverter:
|
|||||||
"workingFormat": Quartz.kCIFormatRGBAh,
|
"workingFormat": Quartz.kCIFormatRGBAh,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
mtldevice = Metal.MTLCreateSystemDefaultDevice()
|
self.context = Quartz.CIContext.contextWithOptions_(context_options)
|
||||||
self.context = Quartz.CIContext.contextWithMTLDevice_options_(
|
|
||||||
mtldevice, context_options
|
|
||||||
)
|
|
||||||
|
|
||||||
def write_jpeg(self, input_path, output_path, compression_quality=1.0):
|
def write_jpeg(self, input_path, output_path, compression_quality=1.0):
|
||||||
""" convert image to jpeg and write image to output_path
|
"""convert image to jpeg and write image to output_path
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input_path: path to input image (e.g. '/path/to/import/file.CR2') as str or pathlib.Path
|
input_path: path to input image (e.g. '/path/to/import/file.CR2') as str or pathlib.Path
|
||||||
@@ -104,8 +103,11 @@ class ImageConverter:
|
|||||||
if input_image is None:
|
if input_image is None:
|
||||||
raise ImageConversionError(f"Could not create CIImage for {input_path}")
|
raise ImageConversionError(f"Could not create CIImage for {input_path}")
|
||||||
|
|
||||||
output_colorspace = input_image.colorSpace() or Quartz.CGColorSpaceCreateWithName(
|
output_colorspace = (
|
||||||
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
input_image.colorSpace()
|
||||||
|
or Quartz.CGColorSpaceCreateWithName(
|
||||||
|
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
output_options = NSDictionary.dictionaryWithDictionary_(
|
output_options = NSDictionary.dictionaryWithDictionary_(
|
||||||
@@ -123,4 +125,3 @@ class ImageConverter:
|
|||||||
raise ImageConversionError(
|
raise ImageConversionError(
|
||||||
f"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
f"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
70
osxphotos/momentinfo.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
__all__ = ["MomentInfo"]
|
||||||
|
"""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
|
||||||
@@ -1,9 +1,20 @@
|
|||||||
""" utility functions for validating/sanitizing path components """
|
""" utility functions for validating/sanitizing path components """
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
import pathvalidate
|
import pathvalidate
|
||||||
|
|
||||||
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
|
from ._constants import MAX_DIRNAME_LEN, MAX_FILENAME_LEN
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"is_valid_filepath",
|
||||||
|
"sanitize_dirname",
|
||||||
|
"sanitize_filename",
|
||||||
|
"sanitize_filepath",
|
||||||
|
"sanitize_filestem_with_count",
|
||||||
|
"sanitize_pathpart",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def sanitize_filepath(filepath):
|
def sanitize_filepath(filepath):
|
||||||
"""sanitize a filepath"""
|
"""sanitize a filepath"""
|
||||||
@@ -45,6 +56,26 @@ def sanitize_filename(filename, replacement=":"):
|
|||||||
return filename
|
return filename
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_filestem_with_count(file_stem: str, file_suffix: str) -> str:
|
||||||
|
"""Sanitize a filestem that may end in (1), (2), etc. to ensure it + file_suffix doesn't exceed MAX_FILENAME_LEN"""
|
||||||
|
filename_len = len(file_stem) + len(file_suffix)
|
||||||
|
if filename_len <= MAX_FILENAME_LEN:
|
||||||
|
return file_stem
|
||||||
|
|
||||||
|
drop = filename_len - MAX_FILENAME_LEN
|
||||||
|
match = re.match(r"(.*)(\(\d+\))$", file_stem)
|
||||||
|
if not match:
|
||||||
|
# filename doesn't end in (1), (2), etc.
|
||||||
|
# truncate filename to MAX_FILENAME_LEN
|
||||||
|
return file_stem[:-drop]
|
||||||
|
|
||||||
|
# filename ends in (1), (2), etc.
|
||||||
|
file_stem = match.group(1)
|
||||||
|
file_count = match.group(2)
|
||||||
|
file_stem = file_stem[:-drop]
|
||||||
|
return f"{file_stem}{file_count}"
|
||||||
|
|
||||||
|
|
||||||
def sanitize_dirname(dirname, replacement=":"):
|
def sanitize_dirname(dirname, replacement=":"):
|
||||||
"""replace any illegal characters in a directory name and truncate directory name if needed
|
"""replace any illegal characters in a directory name and truncate directory name if needed
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import math
|
|||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
|
__all__ = ["PersonInfo", "FaceInfo", "rotate_image_point"]
|
||||||
|
|
||||||
MWG_RS_Area = namedtuple("MWG_RS_Area", ["x", "y", "h", "w"])
|
MWG_RS_Area = namedtuple("MWG_RS_Area", ["x", "y", "h", "w"])
|
||||||
MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
|
MPRI_Reg_Rect = namedtuple("MPRI_Reg_Rect", ["x", "y", "h", "w"])
|
||||||
|
|
||||||
@@ -51,7 +53,7 @@ class PersonInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def photos(self):
|
def photos(self):
|
||||||
""" Returns list of PhotoInfo objects associated with this person """
|
"""Returns list of PhotoInfo objects associated with this person"""
|
||||||
return self._db.photos_by_uuid(self._db._dbfaces_pk[self._pk])
|
return self._db.photos_by_uuid(self._db._dbfaces_pk[self._pk])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -71,7 +73,7 @@ class PersonInfo:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
""" Returns dictionary representation of class instance """
|
"""Returns dictionary representation of class instance"""
|
||||||
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
|
keyphoto = self.keyphoto.uuid if self.keyphoto is not None else None
|
||||||
return {
|
return {
|
||||||
"uuid": self.uuid,
|
"uuid": self.uuid,
|
||||||
@@ -83,7 +85,7 @@ class PersonInfo:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
""" Returns JSON representation of class instance """
|
"""Returns JSON representation of class instance"""
|
||||||
return json.dumps(self.asdict())
|
return json.dumps(self.asdict())
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -141,7 +143,7 @@ class FaceInfo:
|
|||||||
self.manual = face["manual"]
|
self.manual = face["manual"]
|
||||||
self.face_type = face["facetype"]
|
self.face_type = face["facetype"]
|
||||||
self.age_type = face["agetype"]
|
self.age_type = face["agetype"]
|
||||||
self.bald_type = face["baldtype"]
|
# self.bald_type = face["baldtype"]
|
||||||
self.eye_makeup_type = face["eyemakeuptype"]
|
self.eye_makeup_type = face["eyemakeuptype"]
|
||||||
self.eye_state = face["eyestate"]
|
self.eye_state = face["eyestate"]
|
||||||
self.facial_hair_type = face["facialhairtype"]
|
self.facial_hair_type = face["facialhairtype"]
|
||||||
@@ -201,7 +203,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def person_info(self):
|
def person_info(self):
|
||||||
""" PersonInfo instance for person associated with this face """
|
"""PersonInfo instance for person associated with this face"""
|
||||||
try:
|
try:
|
||||||
return self._person
|
return self._person
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -210,7 +212,7 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def photo(self):
|
def photo(self):
|
||||||
""" PhotoInfo instance associated with this face """
|
"""PhotoInfo instance associated with this face"""
|
||||||
try:
|
try:
|
||||||
return self._photo
|
return self._photo
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -292,7 +294,7 @@ class FaceInfo:
|
|||||||
return [(x0, y0), (x1, y1)]
|
return [(x0, y0), (x1, y1)]
|
||||||
|
|
||||||
def roll_pitch_yaw(self):
|
def roll_pitch_yaw(self):
|
||||||
""" Roll, pitch, yaw of face in radians as tuple """
|
"""Roll, pitch, yaw of face in radians as tuple"""
|
||||||
info = self._info
|
info = self._info
|
||||||
roll = 0 if info["roll"] is None else info["roll"]
|
roll = 0 if info["roll"] is None else info["roll"]
|
||||||
pitch = 0 if info["pitch"] is None else info["pitch"]
|
pitch = 0 if info["pitch"] is None else info["pitch"]
|
||||||
@@ -302,19 +304,19 @@ class FaceInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def roll(self):
|
def roll(self):
|
||||||
""" Return roll angle in radians of the face region """
|
"""Return roll angle in radians of the face region"""
|
||||||
roll, _, _ = self.roll_pitch_yaw()
|
roll, _, _ = self.roll_pitch_yaw()
|
||||||
return roll
|
return roll
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pitch(self):
|
def pitch(self):
|
||||||
""" Return pitch angle in radians of the face region """
|
"""Return pitch angle in radians of the face region"""
|
||||||
_, pitch, _ = self.roll_pitch_yaw()
|
_, pitch, _ = self.roll_pitch_yaw()
|
||||||
return pitch
|
return pitch
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def yaw(self):
|
def yaw(self):
|
||||||
""" Return yaw angle in radians of the face region """
|
"""Return yaw angle in radians of the face region"""
|
||||||
_, _, yaw = self.roll_pitch_yaw()
|
_, _, yaw = self.roll_pitch_yaw()
|
||||||
return yaw
|
return yaw
|
||||||
|
|
||||||
@@ -402,7 +404,7 @@ class FaceInfo:
|
|||||||
return (int(xr), int(yr))
|
return (int(xr), int(yr))
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
""" Returns dict representation of class instance """
|
"""Returns dict representation of class instance"""
|
||||||
roll, pitch, yaw = self.roll_pitch_yaw()
|
roll, pitch, yaw = self.roll_pitch_yaw()
|
||||||
return {
|
return {
|
||||||
"_pk": self._pk,
|
"_pk": self._pk,
|
||||||
@@ -438,7 +440,7 @@ class FaceInfo:
|
|||||||
"manual": self.manual,
|
"manual": self.manual,
|
||||||
"face_type": self.face_type,
|
"face_type": self.face_type,
|
||||||
"age_type": self.age_type,
|
"age_type": self.age_type,
|
||||||
"bald_type": self.bald_type,
|
# "bald_type": self.bald_type,
|
||||||
"eye_makeup_type": self.eye_makeup_type,
|
"eye_makeup_type": self.eye_makeup_type,
|
||||||
"eye_state": self.eye_state,
|
"eye_state": self.eye_state,
|
||||||
"facial_hair_type": self.facial_hair_type,
|
"facial_hair_type": self.facial_hair_type,
|
||||||
@@ -451,7 +453,7 @@ class FaceInfo:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def json(self):
|
def json(self):
|
||||||
""" Return JSON representation of FaceInfo instance """
|
"""Return JSON representation of FaceInfo instance"""
|
||||||
return json.dumps(self.asdict())
|
return json.dumps(self.asdict())
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
2020
osxphotos/photoexporter.py
Normal file
@@ -14,15 +14,20 @@ from datetime import timedelta, timezone
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
from osxmetadata import OSXMetaData
|
||||||
|
|
||||||
from .._constants import (
|
from ._constants import (
|
||||||
_MOVIE_TYPE,
|
_MOVIE_TYPE,
|
||||||
_PHOTO_TYPE,
|
_PHOTO_TYPE,
|
||||||
_PHOTOS_4_ALBUM_KIND,
|
_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_ROOT_FOLDER,
|
||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
_PHOTOS_5_ALBUM_KIND,
|
_PHOTOS_5_ALBUM_KIND,
|
||||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||||
|
_PHOTOS_5_PROJECT_ALBUM_KIND,
|
||||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||||
_PHOTOS_5_VERSION,
|
_PHOTOS_5_VERSION,
|
||||||
@@ -30,16 +35,28 @@ from .._constants import (
|
|||||||
BURST_KEY,
|
BURST_KEY,
|
||||||
BURST_NOT_SELECTED,
|
BURST_NOT_SELECTED,
|
||||||
BURST_SELECTED,
|
BURST_SELECTED,
|
||||||
|
SIDECAR_EXIFTOOL,
|
||||||
|
SIDECAR_JSON,
|
||||||
|
SIDECAR_XMP,
|
||||||
TEXT_DETECTION_CONFIDENCE_THRESHOLD,
|
TEXT_DETECTION_CONFIDENCE_THRESHOLD,
|
||||||
)
|
)
|
||||||
from ..adjustmentsinfo import AdjustmentsInfo
|
from .adjustmentsinfo import AdjustmentsInfo
|
||||||
from ..albuminfo import AlbumInfo, ImportInfo
|
from .albuminfo import AlbumInfo, ImportInfo, ProjectInfo
|
||||||
from ..personinfo import FaceInfo, PersonInfo
|
from .exifinfo import ExifInfo
|
||||||
from ..phototemplate import PhotoTemplate, RenderOptions
|
from .exiftool import ExifToolCaching, get_exiftool_path
|
||||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
from .momentinfo import MomentInfo
|
||||||
from ..text_detection import detect_text
|
from .personinfo import FaceInfo, PersonInfo
|
||||||
from ..uti import get_preferred_uti_extension, get_uti_for_extension
|
from .photoexporter import ExportOptions, PhotoExporter
|
||||||
from ..utils import _debug, _get_resource_loc, findfiles
|
from .phototemplate import PhotoTemplate, RenderOptions
|
||||||
|
from .placeinfo import PlaceInfo4, PlaceInfo5
|
||||||
|
from .query_builder import get_query
|
||||||
|
from .scoreinfo import ScoreInfo
|
||||||
|
from .searchinfo import SearchInfo
|
||||||
|
from .text_detection import detect_text
|
||||||
|
from .uti import get_preferred_uti_extension, get_uti_for_extension
|
||||||
|
from .utils import _debug, _get_resource_loc, list_directory
|
||||||
|
|
||||||
|
__all__ = ["PhotoInfo", "PhotoInfoNone"]
|
||||||
|
|
||||||
|
|
||||||
class PhotoInfo:
|
class PhotoInfo:
|
||||||
@@ -48,42 +65,12 @@ class PhotoInfo:
|
|||||||
including keywords, persons, albums, uuid, path, etc.
|
including keywords, persons, albums, uuid, path, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# import additional methods
|
|
||||||
from ._photoinfo_comments import comments, likes
|
|
||||||
from ._photoinfo_exifinfo import ExifInfo, exif_info
|
|
||||||
from ._photoinfo_exiftool import exiftool
|
|
||||||
from ._photoinfo_export import (
|
|
||||||
ExportResults,
|
|
||||||
_exiftool_dict,
|
|
||||||
_exiftool_json_sidecar,
|
|
||||||
_export_photo,
|
|
||||||
_export_photo_with_photos_export,
|
|
||||||
_get_exif_keywords,
|
|
||||||
_get_exif_persons,
|
|
||||||
_write_exif_data,
|
|
||||||
_write_sidecar,
|
|
||||||
_xmp_sidecar,
|
|
||||||
export,
|
|
||||||
export2,
|
|
||||||
)
|
|
||||||
from ._photoinfo_scoreinfo import ScoreInfo, score
|
|
||||||
from ._photoinfo_searchinfo import (
|
|
||||||
SearchInfo,
|
|
||||||
labels,
|
|
||||||
labels_normalized,
|
|
||||||
search_info,
|
|
||||||
search_info_normalized,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, db=None, uuid=None, info=None):
|
def __init__(self, db=None, uuid=None, info=None):
|
||||||
self._uuid = uuid
|
self._uuid = uuid
|
||||||
self._info = info
|
self._info = info
|
||||||
self._db = db
|
self._db = db
|
||||||
self._verbose = self._db._verbose
|
self._verbose = self._db._verbose
|
||||||
|
|
||||||
# TODO: remove this once refactor of PhotoExporter is done
|
|
||||||
self._render_options = RenderOptions()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self):
|
def filename(self):
|
||||||
"""filename of the picture"""
|
"""filename of the picture"""
|
||||||
@@ -382,7 +369,7 @@ class PhotoInfo:
|
|||||||
# In Photos 5, raw is in same folder as original but with _4.ext
|
# In Photos 5, raw is in same folder as original but with _4.ext
|
||||||
# Unless "Copy Items to the Photos Library" is not checked
|
# Unless "Copy Items to the Photos Library" is not checked
|
||||||
# then RAW image is not renamed but has same name is jpeg buth with raw extension
|
# then RAW image is not renamed but has same name is jpeg buth with raw extension
|
||||||
# Current implementation uses findfiles to find images with the correct raw UTI extension
|
# Current implementation finds images with the correct raw UTI extension
|
||||||
# in same folder as the original and with same stem as original in form: original_stem*.raw_ext
|
# in same folder as the original and with same stem as original in form: original_stem*.raw_ext
|
||||||
# TODO: I don't like this -- would prefer a more deterministic approach but until I have more
|
# TODO: I don't like this -- would prefer a more deterministic approach but until I have more
|
||||||
# data on how Photos stores and retrieves RAW images, this seems to be working
|
# data on how Photos stores and retrieves RAW images, this seems to be working
|
||||||
@@ -418,8 +405,7 @@ class PhotoInfo:
|
|||||||
# raw files have same name as original but with _4.raw_ext appended
|
# raw files have same name as original but with _4.raw_ext appended
|
||||||
# I believe the _4 maps to PHAssetResourceTypeAlternatePhoto = 4
|
# I believe the _4 maps to PHAssetResourceTypeAlternatePhoto = 4
|
||||||
# see: https://developer.apple.com/documentation/photokit/phassetresourcetype/phassetresourcetypealternatephoto?language=objc
|
# see: https://developer.apple.com/documentation/photokit/phassetresourcetype/phassetresourcetypealternatephoto?language=objc
|
||||||
glob_str = f"{filestem}_4*"
|
raw_file = list_directory(filepath, startswith=f"{filestem}_4")
|
||||||
raw_file = findfiles(glob_str, filepath)
|
|
||||||
if not raw_file:
|
if not raw_file:
|
||||||
photopath = None
|
photopath = None
|
||||||
else:
|
else:
|
||||||
@@ -492,6 +478,18 @@ class PhotoInfo:
|
|||||||
self._faceinfo = []
|
self._faceinfo = []
|
||||||
return 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
|
@property
|
||||||
def albums(self):
|
def albums(self):
|
||||||
"""list of albums picture is contained in"""
|
"""list of albums picture is contained in"""
|
||||||
@@ -555,6 +553,18 @@ class PhotoInfo:
|
|||||||
)
|
)
|
||||||
return self._import_info
|
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
|
@property
|
||||||
def keywords(self):
|
def keywords(self):
|
||||||
"""list of keywords for picture"""
|
"""list of keywords for picture"""
|
||||||
@@ -563,7 +573,12 @@ class PhotoInfo:
|
|||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
"""name / title of picture"""
|
"""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
|
@property
|
||||||
def uuid(self):
|
def uuid(self):
|
||||||
@@ -714,8 +729,10 @@ class PhotoInfo:
|
|||||||
self._uti_original = self.uti
|
self._uti_original = self.uti
|
||||||
elif self._db._photos_ver >= 7:
|
elif self._db._photos_ver >= 7:
|
||||||
# Monterey+
|
# Monterey+
|
||||||
self._uti_original = get_uti_for_extension(
|
# there are some cases with UTI_original is None (photo imported with no extension) so fallback to UTI and hope it's right
|
||||||
pathlib.Path(self.original_filename).suffix
|
self._uti_original = (
|
||||||
|
get_uti_for_extension(pathlib.Path(self.original_filename).suffix)
|
||||||
|
or self.uti
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._uti_original = self._info["UTI_original"]
|
self._uti_original = self._info["UTI_original"]
|
||||||
@@ -836,7 +853,7 @@ class PhotoInfo:
|
|||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
if self.live_photo and not self.ismissing:
|
if self.live_photo and not self.ismissing:
|
||||||
live_model_id = self._info["live_model_id"]
|
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}")
|
logging.debug(f"missing live_model_id: {self._uuid}")
|
||||||
photopath = None
|
photopath = None
|
||||||
else:
|
else:
|
||||||
@@ -857,28 +874,20 @@ class PhotoInfo:
|
|||||||
# photos 4 has "isOnDisk" column we could check
|
# photos 4 has "isOnDisk" column we could check
|
||||||
# or could do the actual check with "isfile"
|
# or could do the actual check with "isfile"
|
||||||
# TODO: should this be a warning or debug?
|
# 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
|
photopath = None
|
||||||
else:
|
else:
|
||||||
photopath = None
|
photopath = None
|
||||||
else:
|
elif self.live_photo and self.path and not self.ismissing:
|
||||||
# Photos 5
|
filename = pathlib.Path(self.path)
|
||||||
if self.live_photo and not self.ismissing:
|
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
|
||||||
filename = pathlib.Path(self.path)
|
photopath = str(photopath)
|
||||||
photopath = filename.parent.joinpath(f"{filename.stem}_3.mov")
|
if not os.path.isfile(photopath):
|
||||||
photopath = str(photopath)
|
# In testing, I've seen occasional missing movie for live photo
|
||||||
if not os.path.isfile(photopath):
|
# these appear to be valid -- e.g. video component not yet downloaded from iCloud
|
||||||
# In testing, I've seen occasional missing movie for live photo
|
# TODO: should this be a warning or debug?
|
||||||
# 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:
|
|
||||||
photopath = None
|
photopath = None
|
||||||
|
else:
|
||||||
|
photopath = None
|
||||||
|
|
||||||
return photopath
|
return photopath
|
||||||
|
|
||||||
@@ -1022,7 +1031,7 @@ class PhotoInfo:
|
|||||||
@property
|
@property
|
||||||
def israw(self):
|
def israw(self):
|
||||||
"""returns True if photo is a raw image. For images with an associated RAW+JPEG pair, see has_raw"""
|
"""returns True if photo is a raw image. For images with an associated RAW+JPEG pair, see has_raw"""
|
||||||
return "raw-image" in self.uti_original
|
return "raw-image" in self.uti_original if self.uti_original else False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def raw_original(self):
|
def raw_original(self):
|
||||||
@@ -1048,15 +1057,15 @@ class PhotoInfo:
|
|||||||
return self._info["orientation"]
|
return self._info["orientation"]
|
||||||
|
|
||||||
# For Photos 5+, try to get the adjusted orientation
|
# For Photos 5+, try to get the adjusted orientation
|
||||||
if self.hasadjustments:
|
if not 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:
|
|
||||||
return self._info["orientation"]
|
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
|
@property
|
||||||
def original_height(self):
|
def original_height(self):
|
||||||
"""returns height of the original photo version in pixels"""
|
"""returns height of the original photo version in pixels"""
|
||||||
@@ -1092,6 +1101,317 @@ class PhotoInfo:
|
|||||||
logging.warning(f"Did not find signature for {self.uuid} in _db_signatures")
|
logging.warning(f"Did not find signature for {self.uuid} in _db_signatures")
|
||||||
return duplicates
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def score(self):
|
||||||
|
"""Computed score information for a photo
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ScoreInfo instance
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
logging.debug(f"score not implemented for this database version")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._scoreinfo # pylint: disable=access-member-before-definition
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
scores = self._db._db_scoreinfo_uuid[self.uuid]
|
||||||
|
self._scoreinfo = ScoreInfo(
|
||||||
|
overall=scores["overall_aesthetic"],
|
||||||
|
curation=scores["curation"],
|
||||||
|
promotion=scores["promotion"],
|
||||||
|
highlight_visibility=scores["highlight_visibility"],
|
||||||
|
behavioral=scores["behavioral"],
|
||||||
|
failure=scores["failure"],
|
||||||
|
harmonious_color=scores["harmonious_color"],
|
||||||
|
immersiveness=scores["immersiveness"],
|
||||||
|
interaction=scores["interaction"],
|
||||||
|
interesting_subject=scores["interesting_subject"],
|
||||||
|
intrusive_object_presence=scores["intrusive_object_presence"],
|
||||||
|
lively_color=scores["lively_color"],
|
||||||
|
low_light=scores["low_light"],
|
||||||
|
noise=scores["noise"],
|
||||||
|
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
|
||||||
|
pleasant_composition=scores["pleasant_composition"],
|
||||||
|
pleasant_lighting=scores["pleasant_lighting"],
|
||||||
|
pleasant_pattern=scores["pleasant_pattern"],
|
||||||
|
pleasant_perspective=scores["pleasant_perspective"],
|
||||||
|
pleasant_post_processing=scores["pleasant_post_processing"],
|
||||||
|
pleasant_reflection=scores["pleasant_reflection"],
|
||||||
|
pleasant_symmetry=scores["pleasant_symmetry"],
|
||||||
|
sharply_focused_subject=scores["sharply_focused_subject"],
|
||||||
|
tastefully_blurred=scores["tastefully_blurred"],
|
||||||
|
well_chosen_subject=scores["well_chosen_subject"],
|
||||||
|
well_framed_subject=scores["well_framed_subject"],
|
||||||
|
well_timed_shot=scores["well_timed_shot"],
|
||||||
|
)
|
||||||
|
return self._scoreinfo
|
||||||
|
except KeyError:
|
||||||
|
self._scoreinfo = ScoreInfo(
|
||||||
|
overall=0.0,
|
||||||
|
curation=0.0,
|
||||||
|
promotion=0.0,
|
||||||
|
highlight_visibility=0.0,
|
||||||
|
behavioral=0.0,
|
||||||
|
failure=0.0,
|
||||||
|
harmonious_color=0.0,
|
||||||
|
immersiveness=0.0,
|
||||||
|
interaction=0.0,
|
||||||
|
interesting_subject=0.0,
|
||||||
|
intrusive_object_presence=0.0,
|
||||||
|
lively_color=0.0,
|
||||||
|
low_light=0.0,
|
||||||
|
noise=0.0,
|
||||||
|
pleasant_camera_tilt=0.0,
|
||||||
|
pleasant_composition=0.0,
|
||||||
|
pleasant_lighting=0.0,
|
||||||
|
pleasant_pattern=0.0,
|
||||||
|
pleasant_perspective=0.0,
|
||||||
|
pleasant_post_processing=0.0,
|
||||||
|
pleasant_reflection=0.0,
|
||||||
|
pleasant_symmetry=0.0,
|
||||||
|
sharply_focused_subject=0.0,
|
||||||
|
tastefully_blurred=0.0,
|
||||||
|
well_chosen_subject=0.0,
|
||||||
|
well_framed_subject=0.0,
|
||||||
|
well_timed_shot=0.0,
|
||||||
|
)
|
||||||
|
return self._scoreinfo
|
||||||
|
|
||||||
|
@property
|
||||||
|
def search_info(self):
|
||||||
|
"""returns SearchInfo object for photo
|
||||||
|
only valid on Photos 5, on older libraries, returns None
|
||||||
|
"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# memoize SearchInfo object
|
||||||
|
try:
|
||||||
|
return self._search_info
|
||||||
|
except AttributeError:
|
||||||
|
self._search_info = SearchInfo(self)
|
||||||
|
return self._search_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def search_info_normalized(self):
|
||||||
|
"""returns SearchInfo object for photo that produces normalized results
|
||||||
|
only valid on Photos 5, on older libraries, returns None
|
||||||
|
"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# memoize SearchInfo object
|
||||||
|
try:
|
||||||
|
return self._search_info_normalized
|
||||||
|
except AttributeError:
|
||||||
|
self._search_info_normalized = SearchInfo(self, normalized=True)
|
||||||
|
return self._search_info_normalized
|
||||||
|
|
||||||
|
@property
|
||||||
|
def labels(self):
|
||||||
|
"""returns list of labels applied to photo by Photos image categorization
|
||||||
|
only valid on Photos 5, on older libraries returns empty list
|
||||||
|
"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self.search_info.labels
|
||||||
|
|
||||||
|
@property
|
||||||
|
def labels_normalized(self):
|
||||||
|
"""returns normalized list of labels applied to photo by Photos image categorization
|
||||||
|
only valid on Photos 5, on older libraries returns empty list
|
||||||
|
"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self.search_info_normalized.labels
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comments(self):
|
||||||
|
"""Returns list of Comment objects for any comments on the photo (sorted by date)"""
|
||||||
|
try:
|
||||||
|
return self._db._db_comments_uuid[self.uuid]["comments"]
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def likes(self):
|
||||||
|
"""Returns list of Like objects for any likes on the photo (sorted by date)"""
|
||||||
|
try:
|
||||||
|
return self._db._db_comments_uuid[self.uuid]["likes"]
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exif_info(self):
|
||||||
|
"""Returns an ExifInfo object with the EXIF data for photo
|
||||||
|
Note: the returned EXIF data is the data Photos stores in the database on import;
|
||||||
|
ExifInfo does not provide access to the EXIF info in the actual image file
|
||||||
|
Some or all of the fields may be None
|
||||||
|
Only valid for Photos 5; on earlier database returns None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
logging.debug(f"exif_info not implemented for this database version")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
exif = self._db._db_exifinfo_uuid[self.uuid]
|
||||||
|
exif_info = ExifInfo(
|
||||||
|
iso=exif["ZISO"],
|
||||||
|
flash_fired=True if exif["ZFLASHFIRED"] == 1 else False,
|
||||||
|
metering_mode=exif["ZMETERINGMODE"],
|
||||||
|
sample_rate=exif["ZSAMPLERATE"],
|
||||||
|
track_format=exif["ZTRACKFORMAT"],
|
||||||
|
white_balance=exif["ZWHITEBALANCE"],
|
||||||
|
aperture=exif["ZAPERTURE"],
|
||||||
|
bit_rate=exif["ZBITRATE"],
|
||||||
|
duration=exif["ZDURATION"],
|
||||||
|
exposure_bias=exif["ZEXPOSUREBIAS"],
|
||||||
|
focal_length=exif["ZFOCALLENGTH"],
|
||||||
|
fps=exif["ZFPS"],
|
||||||
|
latitude=exif["ZLATITUDE"],
|
||||||
|
longitude=exif["ZLONGITUDE"],
|
||||||
|
shutter_speed=exif["ZSHUTTERSPEED"],
|
||||||
|
camera_make=exif["ZCAMERAMAKE"],
|
||||||
|
camera_model=exif["ZCAMERAMODEL"],
|
||||||
|
codec=exif["ZCODEC"],
|
||||||
|
lens_model=exif["ZLENSMODEL"],
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
logging.debug(f"Could not find exif record for uuid {self.uuid}")
|
||||||
|
exif_info = ExifInfo(
|
||||||
|
iso=None,
|
||||||
|
flash_fired=None,
|
||||||
|
metering_mode=None,
|
||||||
|
sample_rate=None,
|
||||||
|
track_format=None,
|
||||||
|
white_balance=None,
|
||||||
|
aperture=None,
|
||||||
|
bit_rate=None,
|
||||||
|
duration=None,
|
||||||
|
exposure_bias=None,
|
||||||
|
focal_length=None,
|
||||||
|
fps=None,
|
||||||
|
latitude=None,
|
||||||
|
longitude=None,
|
||||||
|
shutter_speed=None,
|
||||||
|
camera_make=None,
|
||||||
|
camera_model=None,
|
||||||
|
codec=None,
|
||||||
|
lens_model=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return exif_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exiftool(self):
|
||||||
|
"""Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
|
||||||
|
Requires that exiftool (https://exiftool.org/) be installed
|
||||||
|
If exiftool not installed, logs warning and returns None
|
||||||
|
If photo path is missing, returns None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# return the memoized instance if it exists
|
||||||
|
return self._exiftool
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
exiftool_path = self._db._exiftool_path or get_exiftool_path()
|
||||||
|
if self.path is not None and os.path.isfile(self.path):
|
||||||
|
exiftool = ExifToolCaching(self.path, exiftool=exiftool_path)
|
||||||
|
else:
|
||||||
|
exiftool = None
|
||||||
|
except FileNotFoundError:
|
||||||
|
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
||||||
|
exiftool = None
|
||||||
|
logging.warning(
|
||||||
|
"exiftool not in path; download and install from https://exiftool.org/"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._exiftool = exiftool
|
||||||
|
return self._exiftool
|
||||||
|
|
||||||
|
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"""
|
||||||
|
return self._info["longitude"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _latitude(self):
|
||||||
|
"""Returns latitude, in degrees"""
|
||||||
|
return self._info["latitude"]
|
||||||
|
|
||||||
def render_template(
|
def render_template(
|
||||||
self, template_str: str, options: Optional[RenderOptions] = None
|
self, template_str: str, options: Optional[RenderOptions] = None
|
||||||
):
|
):
|
||||||
@@ -1108,79 +1428,154 @@ class PhotoInfo:
|
|||||||
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
|
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
|
||||||
return template.render(template_str, options)
|
return template.render(template_str, options)
|
||||||
|
|
||||||
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
|
def export(
|
||||||
"""Detects text in photo and returns lists of results as (detected text, confidence)
|
self,
|
||||||
|
dest,
|
||||||
|
filename=None,
|
||||||
|
edited=False,
|
||||||
|
live_photo=False,
|
||||||
|
raw_photo=False,
|
||||||
|
export_as_hardlink=False,
|
||||||
|
overwrite=False,
|
||||||
|
increment=True,
|
||||||
|
sidecar_json=False,
|
||||||
|
sidecar_exiftool=False,
|
||||||
|
sidecar_xmp=False,
|
||||||
|
use_photos_export=False,
|
||||||
|
timeout=120,
|
||||||
|
exiftool=False,
|
||||||
|
use_albums_as_keywords=False,
|
||||||
|
use_persons_as_keywords=False,
|
||||||
|
keyword_template=None,
|
||||||
|
description_template=None,
|
||||||
|
render_options: Optional[RenderOptions] = None,
|
||||||
|
):
|
||||||
|
"""export photo
|
||||||
|
dest: must be valid destination path (or exception raised)
|
||||||
|
filename: (optional): name of exported picture; if not provided, will use current filename
|
||||||
|
**NOTE**: if provided, user must ensure file extension (suffix) is correct.
|
||||||
|
For example, if photo is .CR2 file, edited image may be .jpeg.
|
||||||
|
If you provide an extension different than what the actual file is,
|
||||||
|
export will print a warning but will export the photo using the
|
||||||
|
incorrect file extension (unless use_photos_export is true, in which case export will
|
||||||
|
use the extension provided by Photos upon export; in this case, an incorrect extension is
|
||||||
|
silently ignored).
|
||||||
|
e.g. to get the extension of the edited photo,
|
||||||
|
reference PhotoInfo.path_edited
|
||||||
|
edited: (boolean, default=False); if True will export the edited version of the photo, otherwise exports the original version
|
||||||
|
(or raise exception if no edited version)
|
||||||
|
live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
|
||||||
|
raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
|
||||||
|
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
|
||||||
|
overwrite: (boolean, default=False); if True will overwrite files if they already exist
|
||||||
|
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
|
||||||
|
if overwrite=False and increment=False, export will fail if destination file already exists
|
||||||
|
sidecar_json: if set will write a json sidecar with data in format readable by exiftool
|
||||||
|
sidecar filename will be dest/filename.json; includes exiftool tag group names (e.g. `exiftool -G -j`)
|
||||||
|
sidecar_exiftool: if set will write a json sidecar with data in format readable by exiftool
|
||||||
|
sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. `exiftool -j`)
|
||||||
|
sidecar_xmp: if set will write an XMP sidecar with IPTC data
|
||||||
|
sidecar filename will be dest/filename.xmp
|
||||||
|
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
|
||||||
|
timeout: (int, default=120) timeout in seconds used with use_photos_export
|
||||||
|
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
|
||||||
|
returns list of full paths to the exported files
|
||||||
|
use_albums_as_keywords: (boolean, default = False); if True, will include album names in keywords
|
||||||
|
when exporting metadata with exiftool or sidecar
|
||||||
|
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
|
||||||
|
when exporting metadata with exiftool or sidecar
|
||||||
|
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
|
||||||
|
description_template: string; optional template string that will be rendered for use as photo description
|
||||||
|
render_options: an optional osxphotos.phototemplate.RenderOptions instance with options to pass to template renderer
|
||||||
|
|
||||||
confidence_threshold: float between 0.0 and 1.0. If text detection confidence is below this threshold,
|
Returns: list of photos exported
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
path = (
|
|
||||||
self.path_edited if self.hasadjustments and self.path_edited else self.path
|
exporter = PhotoExporter(self)
|
||||||
|
sidecar = 0
|
||||||
|
if sidecar_json:
|
||||||
|
sidecar |= SIDECAR_JSON
|
||||||
|
if sidecar_exiftool:
|
||||||
|
sidecar |= SIDECAR_EXIFTOOL
|
||||||
|
if sidecar_xmp:
|
||||||
|
sidecar |= SIDECAR_XMP
|
||||||
|
|
||||||
|
if not filename:
|
||||||
|
if not edited:
|
||||||
|
filename = self.original_filename
|
||||||
|
else:
|
||||||
|
original_name = pathlib.Path(self.original_filename)
|
||||||
|
if self.path_edited:
|
||||||
|
ext = pathlib.Path(self.path_edited).suffix
|
||||||
|
else:
|
||||||
|
uti = self.uti_edited if edited and self.uti_edited else self.uti
|
||||||
|
ext = get_preferred_uti_extension(uti)
|
||||||
|
ext = "." + ext
|
||||||
|
filename = original_name.stem + "_edited" + ext
|
||||||
|
|
||||||
|
options = ExportOptions(
|
||||||
|
description_template=description_template,
|
||||||
|
edited=edited,
|
||||||
|
exiftool=exiftool,
|
||||||
|
export_as_hardlink=export_as_hardlink,
|
||||||
|
increment=increment,
|
||||||
|
keyword_template=keyword_template,
|
||||||
|
live_photo=live_photo,
|
||||||
|
overwrite=overwrite,
|
||||||
|
raw_photo=raw_photo,
|
||||||
|
render_options=render_options,
|
||||||
|
sidecar=sidecar,
|
||||||
|
timeout=timeout,
|
||||||
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
|
use_photos_export=use_photos_export,
|
||||||
)
|
)
|
||||||
path = path or self.path_derivatives[0] if self.path_derivatives else None
|
|
||||||
if not path:
|
|
||||||
return []
|
|
||||||
|
|
||||||
try:
|
results = exporter.export(dest, filename=filename, options=options)
|
||||||
return self._detected_text[(path, confidence_threshold)]
|
return results.exported
|
||||||
except (AttributeError, KeyError) as e:
|
|
||||||
if isinstance(e, AttributeError):
|
|
||||||
self._detected_text = {}
|
|
||||||
|
|
||||||
try:
|
def _get_album_uuids(self, project=False):
|
||||||
detected_text = detect_text(path)
|
|
||||||
except Exception as e:
|
|
||||||
detected_text = []
|
|
||||||
|
|
||||||
self._detected_text[(path, confidence_threshold)] = [
|
|
||||||
(text, confidence)
|
|
||||||
for text, confidence in detected_text
|
|
||||||
if confidence >= confidence_threshold
|
|
||||||
]
|
|
||||||
return self._detected_text[(path, confidence_threshold)]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _longitude(self):
|
|
||||||
"""Returns longitude, in degrees"""
|
|
||||||
return self._info["longitude"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _latitude(self):
|
|
||||||
"""Returns latitude, in degrees"""
|
|
||||||
return self._info["latitude"]
|
|
||||||
|
|
||||||
def _get_album_uuids(self):
|
|
||||||
"""Return list of album UUIDs this photo is found in
|
"""Return list of album UUIDs this photo is found in
|
||||||
|
|
||||||
Filters out albums in the trash and any special album types
|
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
|
Returns: list of album UUIDs
|
||||||
"""
|
"""
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
version4 = True
|
|
||||||
album_kind = [_PHOTOS_4_ALBUM_KIND]
|
album_kind = [_PHOTOS_4_ALBUM_KIND]
|
||||||
else:
|
album_type = (
|
||||||
version4 = False
|
[_PHOTOS_4_ALBUM_TYPE_PROJECT, _PHOTOS_4_ALBUM_TYPE_SLIDESHOW]
|
||||||
album_kind = [_PHOTOS_5_SHARED_ALBUM_KIND, _PHOTOS_5_ALBUM_KIND]
|
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 = []
|
album_list = []
|
||||||
for album in self._info["albums"]:
|
for album in self._info["albums"]:
|
||||||
detail = self._db._dbalbum_details[album]
|
detail = self._db._dbalbum_details[album]
|
||||||
if (
|
if detail["kind"] in album_kind and not detail["intrash"]:
|
||||||
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)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
album_list.append(album)
|
album_list.append(album)
|
||||||
return album_list
|
return album_list
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
"""
|
|
||||||
PhotoInfo class
|
|
||||||
Represents a single photo in the Photos library and provides access to the photo's attributes
|
|
||||||
PhotosDB.photos() returns a list of PhotoInfo objects
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ._photoinfo_exifinfo import ExifInfo
|
|
||||||
from ._photoinfo_export import ExportResults
|
|
||||||
from ._photoinfo_scoreinfo import ScoreInfo
|
|
||||||
from .photoinfo import PhotoInfo, PhotoInfoNone
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
""" PhotoInfo methods to expose comments and likes for shared photos """
|
|
||||||
|
|
||||||
@property
|
|
||||||
def comments(self):
|
|
||||||
""" Returns list of Comment objects for any comments on the photo (sorted by date) """
|
|
||||||
try:
|
|
||||||
return self._db._db_comments_uuid[self.uuid]["comments"]
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def likes(self):
|
|
||||||
""" Returns list of Like objects for any likes on the photo (sorted by date) """
|
|
||||||
try:
|
|
||||||
return self._db._db_comments_uuid[self.uuid]["likes"]
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
""" PhotoInfo methods to expose EXIF info from the library """
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from .._constants import _PHOTOS_4_VERSION
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ExifInfo:
|
|
||||||
""" EXIF info associated with a photo from the Photos library """
|
|
||||||
|
|
||||||
flash_fired: bool
|
|
||||||
iso: int
|
|
||||||
metering_mode: int
|
|
||||||
sample_rate: int
|
|
||||||
track_format: int
|
|
||||||
white_balance: int
|
|
||||||
aperture: float
|
|
||||||
bit_rate: float
|
|
||||||
duration: float
|
|
||||||
exposure_bias: float
|
|
||||||
focal_length: float
|
|
||||||
fps: float
|
|
||||||
latitude: float
|
|
||||||
longitude: float
|
|
||||||
shutter_speed: float
|
|
||||||
camera_make: str
|
|
||||||
camera_model: str
|
|
||||||
codec: str
|
|
||||||
lens_model: str
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exif_info(self):
|
|
||||||
""" Returns an ExifInfo object with the EXIF data for photo
|
|
||||||
Note: the returned EXIF data is the data Photos stores in the database on import;
|
|
||||||
ExifInfo does not provide access to the EXIF info in the actual image file
|
|
||||||
Some or all of the fields may be None
|
|
||||||
Only valid for Photos 5; on earlier database returns None
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
logging.debug(f"exif_info not implemented for this database version")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
exif = self._db._db_exifinfo_uuid[self.uuid]
|
|
||||||
exif_info = ExifInfo(
|
|
||||||
iso=exif["ZISO"],
|
|
||||||
flash_fired=True if exif["ZFLASHFIRED"] == 1 else False,
|
|
||||||
metering_mode=exif["ZMETERINGMODE"],
|
|
||||||
sample_rate=exif["ZSAMPLERATE"],
|
|
||||||
track_format=exif["ZTRACKFORMAT"],
|
|
||||||
white_balance=exif["ZWHITEBALANCE"],
|
|
||||||
aperture=exif["ZAPERTURE"],
|
|
||||||
bit_rate=exif["ZBITRATE"],
|
|
||||||
duration=exif["ZDURATION"],
|
|
||||||
exposure_bias=exif["ZEXPOSUREBIAS"],
|
|
||||||
focal_length=exif["ZFOCALLENGTH"],
|
|
||||||
fps=exif["ZFPS"],
|
|
||||||
latitude=exif["ZLATITUDE"],
|
|
||||||
longitude=exif["ZLONGITUDE"],
|
|
||||||
shutter_speed=exif["ZSHUTTERSPEED"],
|
|
||||||
camera_make=exif["ZCAMERAMAKE"],
|
|
||||||
camera_model=exif["ZCAMERAMODEL"],
|
|
||||||
codec=exif["ZCODEC"],
|
|
||||||
lens_model=exif["ZLENSMODEL"],
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
logging.debug(f"Could not find exif record for uuid {self.uuid}")
|
|
||||||
exif_info = ExifInfo(
|
|
||||||
iso=None,
|
|
||||||
flash_fired=None,
|
|
||||||
metering_mode=None,
|
|
||||||
sample_rate=None,
|
|
||||||
track_format=None,
|
|
||||||
white_balance=None,
|
|
||||||
aperture=None,
|
|
||||||
bit_rate=None,
|
|
||||||
duration=None,
|
|
||||||
exposure_bias=None,
|
|
||||||
focal_length=None,
|
|
||||||
fps=None,
|
|
||||||
latitude=None,
|
|
||||||
longitude=None,
|
|
||||||
shutter_speed=None,
|
|
||||||
camera_make=None,
|
|
||||||
camera_model=None,
|
|
||||||
codec=None,
|
|
||||||
lens_model=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
return exif_info
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
""" Implementation for PhotoInfo.exiftool property which returns ExifTool object for a photo """
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ..exiftool import ExifToolCaching, get_exiftool_path
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exiftool(self):
|
|
||||||
""" Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
|
|
||||||
Requires that exiftool (https://exiftool.org/) be installed
|
|
||||||
If exiftool not installed, logs warning and returns None
|
|
||||||
If photo path is missing, returns None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# return the memoized instance if it exists
|
|
||||||
return self._exiftool
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
exiftool_path = self._db._exiftool_path or get_exiftool_path()
|
|
||||||
if self.path is not None and os.path.isfile(self.path):
|
|
||||||
exiftool = ExifToolCaching(self.path, exiftool=exiftool_path)
|
|
||||||
else:
|
|
||||||
exiftool = None
|
|
||||||
except FileNotFoundError:
|
|
||||||
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
|
||||||
exiftool = None
|
|
||||||
logging.warning(
|
|
||||||
f"exiftool not in path; download and install from https://exiftool.org/"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._exiftool = exiftool
|
|
||||||
return self._exiftool
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
""" PhotoInfo methods to expose computed score info from the library """
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from .._constants import _PHOTOS_4_VERSION
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ScoreInfo:
|
|
||||||
""" Computed photo score info associated with a photo from the Photos library """
|
|
||||||
|
|
||||||
overall: float
|
|
||||||
curation: float
|
|
||||||
promotion: float
|
|
||||||
highlight_visibility: float
|
|
||||||
behavioral: float
|
|
||||||
failure: float
|
|
||||||
harmonious_color: float
|
|
||||||
immersiveness: float
|
|
||||||
interaction: float
|
|
||||||
interesting_subject: float
|
|
||||||
intrusive_object_presence: float
|
|
||||||
lively_color: float
|
|
||||||
low_light: float
|
|
||||||
noise: float
|
|
||||||
pleasant_camera_tilt: float
|
|
||||||
pleasant_composition: float
|
|
||||||
pleasant_lighting: float
|
|
||||||
pleasant_pattern: float
|
|
||||||
pleasant_perspective: float
|
|
||||||
pleasant_post_processing: float
|
|
||||||
pleasant_reflection: float
|
|
||||||
pleasant_symmetry: float
|
|
||||||
sharply_focused_subject: float
|
|
||||||
tastefully_blurred: float
|
|
||||||
well_chosen_subject: float
|
|
||||||
well_framed_subject: float
|
|
||||||
well_timed_shot: float
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def score(self):
|
|
||||||
""" Computed score information for a photo
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ScoreInfo instance
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
logging.debug(f"score not implemented for this database version")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self._scoreinfo # pylint: disable=access-member-before-definition
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
scores = self._db._db_scoreinfo_uuid[self.uuid]
|
|
||||||
self._scoreinfo = ScoreInfo(
|
|
||||||
overall=scores["overall_aesthetic"],
|
|
||||||
curation=scores["curation"],
|
|
||||||
promotion=scores["promotion"],
|
|
||||||
highlight_visibility=scores["highlight_visibility"],
|
|
||||||
behavioral=scores["behavioral"],
|
|
||||||
failure=scores["failure"],
|
|
||||||
harmonious_color=scores["harmonious_color"],
|
|
||||||
immersiveness=scores["immersiveness"],
|
|
||||||
interaction=scores["interaction"],
|
|
||||||
interesting_subject=scores["interesting_subject"],
|
|
||||||
intrusive_object_presence=scores["intrusive_object_presence"],
|
|
||||||
lively_color=scores["lively_color"],
|
|
||||||
low_light=scores["low_light"],
|
|
||||||
noise=scores["noise"],
|
|
||||||
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
|
|
||||||
pleasant_composition=scores["pleasant_composition"],
|
|
||||||
pleasant_lighting=scores["pleasant_lighting"],
|
|
||||||
pleasant_pattern=scores["pleasant_pattern"],
|
|
||||||
pleasant_perspective=scores["pleasant_perspective"],
|
|
||||||
pleasant_post_processing=scores["pleasant_post_processing"],
|
|
||||||
pleasant_reflection=scores["pleasant_reflection"],
|
|
||||||
pleasant_symmetry=scores["pleasant_symmetry"],
|
|
||||||
sharply_focused_subject=scores["sharply_focused_subject"],
|
|
||||||
tastefully_blurred=scores["tastefully_blurred"],
|
|
||||||
well_chosen_subject=scores["well_chosen_subject"],
|
|
||||||
well_framed_subject=scores["well_framed_subject"],
|
|
||||||
well_timed_shot=scores["well_timed_shot"],
|
|
||||||
)
|
|
||||||
return self._scoreinfo
|
|
||||||
except KeyError:
|
|
||||||
self._scoreinfo = ScoreInfo(
|
|
||||||
overall=0.0,
|
|
||||||
curation=0.0,
|
|
||||||
promotion=0.0,
|
|
||||||
highlight_visibility=0.0,
|
|
||||||
behavioral=0.0,
|
|
||||||
failure=0.0,
|
|
||||||
harmonious_color=0.0,
|
|
||||||
immersiveness=0.0,
|
|
||||||
interaction=0.0,
|
|
||||||
interesting_subject=0.0,
|
|
||||||
intrusive_object_presence=0.0,
|
|
||||||
lively_color=0.0,
|
|
||||||
low_light=0.0,
|
|
||||||
noise=0.0,
|
|
||||||
pleasant_camera_tilt=0.0,
|
|
||||||
pleasant_composition=0.0,
|
|
||||||
pleasant_lighting=0.0,
|
|
||||||
pleasant_pattern=0.0,
|
|
||||||
pleasant_perspective=0.0,
|
|
||||||
pleasant_post_processing=0.0,
|
|
||||||
pleasant_reflection=0.0,
|
|
||||||
pleasant_symmetry=0.0,
|
|
||||||
sharply_focused_subject=0.0,
|
|
||||||
tastefully_blurred=0.0,
|
|
||||||
well_chosen_subject=0.0,
|
|
||||||
well_framed_subject=0.0,
|
|
||||||
well_timed_shot=0.0,
|
|
||||||
)
|
|
||||||
return self._scoreinfo
|
|
||||||
@@ -6,8 +6,6 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# NOTES:
|
# 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
|
# - There are several techniques used for handling PhotoKit's various
|
||||||
# asynchronous calls used in this code: event loop+notification, threading
|
# asynchronous calls used in this code: event loop+notification, threading
|
||||||
# event, while loop. I've experimented with each to find the one that works.
|
# event, while loop. I've experimented with each to find the one that works.
|
||||||
@@ -32,11 +30,34 @@ import Photos
|
|||||||
import Quartz
|
import Quartz
|
||||||
from Foundation import NSNotificationCenter, NSObject
|
from Foundation import NSNotificationCenter, NSObject
|
||||||
from PyObjCTools import AppHelper
|
from PyObjCTools import AppHelper
|
||||||
|
from wurlitzer import pipes
|
||||||
|
|
||||||
from .fileutil import FileUtil
|
from .fileutil import FileUtil
|
||||||
from .uti import get_preferred_uti_extension
|
from .uti import get_preferred_uti_extension
|
||||||
from .utils import _get_os_version, increment_filename
|
from .utils import _get_os_version, increment_filename
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"NSURL_to_path",
|
||||||
|
"path_to_NSURL",
|
||||||
|
"check_photokit_authorization",
|
||||||
|
"request_photokit_authorization",
|
||||||
|
"PhotoKitError",
|
||||||
|
"PhotoKitFetchFailed",
|
||||||
|
"PhotoKitAuthError",
|
||||||
|
"PhotoKitExportError",
|
||||||
|
"PhotoKitMediaTypeError",
|
||||||
|
"ImageData",
|
||||||
|
"AVAssetData",
|
||||||
|
"PHAssetResourceData",
|
||||||
|
"PhotoKitNotificationDelegate",
|
||||||
|
"PhotoAsset",
|
||||||
|
"SlowMoVideoExporter",
|
||||||
|
"VideoAsset",
|
||||||
|
"LivePhotoRequest",
|
||||||
|
"LivePhotoAsset",
|
||||||
|
"PhotoLibrary",
|
||||||
|
]
|
||||||
|
|
||||||
# NOTE: This requires user have granted access to the terminal (e.g. Terminal.app or iTerm)
|
# NOTE: This requires user have granted access to the terminal (e.g. Terminal.app or iTerm)
|
||||||
# to access Photos. This should happen automatically the first time it's called. I've
|
# to access Photos. This should happen automatically the first time it's called. I've
|
||||||
# not figured out how to get the call to requestAuthorization_ to actually work in the case
|
# not figured out how to get the call to requestAuthorization_ to actually work in the case
|
||||||
@@ -200,16 +221,6 @@ class PHAssetResourceData:
|
|||||||
self.data = b""
|
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):
|
class PhotoKitNotificationDelegate(NSObject):
|
||||||
"""Handles notifications from NotificationCenter;
|
"""Handles notifications from NotificationCenter;
|
||||||
used with asynchronous PhotoKit requests to stop event loop when complete
|
used with asynchronous PhotoKit requests to stop event loop when complete
|
||||||
@@ -487,6 +498,7 @@ class PhotoAsset:
|
|||||||
version=PHOTOS_VERSION_CURRENT,
|
version=PHOTOS_VERSION_CURRENT,
|
||||||
overwrite=False,
|
overwrite=False,
|
||||||
raw=False,
|
raw=False,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Export image to path
|
"""Export image to path
|
||||||
|
|
||||||
@@ -496,6 +508,7 @@ class PhotoAsset:
|
|||||||
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
|
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
|
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
|
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:
|
Returns:
|
||||||
List of path to exported image(s)
|
List of path to exported image(s)
|
||||||
@@ -504,73 +517,75 @@ class PhotoAsset:
|
|||||||
ValueError if dest is not a valid directory
|
ValueError if dest is not a valid directory
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# if self.live:
|
|
||||||
# raise NotImplementedError("Live photos not implemented yet")
|
|
||||||
|
|
||||||
with objc.autorelease_pool():
|
with objc.autorelease_pool():
|
||||||
filename = (
|
with pipes() as (out, err):
|
||||||
pathlib.Path(filename)
|
filename = (
|
||||||
if filename
|
pathlib.Path(filename)
|
||||||
else pathlib.Path(self.original_filename)
|
if filename
|
||||||
)
|
else pathlib.Path(self.original_filename)
|
||||||
|
)
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
dest = pathlib.Path(dest)
|
||||||
if not dest.is_dir():
|
if not dest.is_dir():
|
||||||
raise ValueError("dest must be a valid directory: {dest}")
|
raise ValueError("dest must be a valid directory: {dest}")
|
||||||
|
|
||||||
output_file = None
|
output_file = None
|
||||||
if self.isphoto:
|
if self.isphoto:
|
||||||
# will hold exported image data and needs to be cleaned up at end
|
# will hold exported image data and needs to be cleaned up at end
|
||||||
imagedata = None
|
imagedata = None
|
||||||
if raw:
|
if raw:
|
||||||
# export the raw component
|
# export the raw component
|
||||||
resources = self._resources()
|
resources = self._resources()
|
||||||
for resource in resources:
|
for resource in resources:
|
||||||
if resource.type() == Photos.PHAssetResourceTypeAlternatePhoto:
|
if (
|
||||||
data = self._request_resource_data(resource)
|
resource.type()
|
||||||
ext = pathlib.Path(self.raw_filename).suffix[1:]
|
== Photos.PHAssetResourceTypeAlternatePhoto
|
||||||
break
|
):
|
||||||
|
data = self._request_resource_data(resource)
|
||||||
|
suffix = pathlib.Path(self.raw_filename).suffix
|
||||||
|
ext = suffix[1:] if suffix else ""
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise PhotoKitExportError(
|
||||||
|
"Could not get image data for RAW photo"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise PhotoKitExportError(
|
# TODO: if user has selected use RAW as original, this returns the RAW
|
||||||
"Could not get image data for RAW photo"
|
# can get the jpeg with resource.type() == Photos.PHAssetResourceTypePhoto
|
||||||
)
|
imagedata = self._request_image_data(version=version)
|
||||||
else:
|
if not imagedata.image_data:
|
||||||
# TODO: if user has selected use RAW as original, this returns the RAW
|
raise PhotoKitExportError("Could not get image data")
|
||||||
# can get the jpeg with resource.type() == Photos.PHAssetResourceTypePhoto
|
ext = get_preferred_uti_extension(imagedata.uti)
|
||||||
imagedata = self._request_image_data(version=version)
|
data = imagedata.image_data
|
||||||
if not imagedata.image_data:
|
|
||||||
raise PhotoKitExportError("Could not get image data")
|
|
||||||
ext = get_preferred_uti_extension(imagedata.uti)
|
|
||||||
data = imagedata.image_data
|
|
||||||
|
|
||||||
output_file = dest / f"{filename.stem}.{ext}"
|
output_file = dest / f"{filename.stem}.{ext}"
|
||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
output_file = pathlib.Path(increment_filename(output_file))
|
output_file = pathlib.Path(increment_filename(output_file))
|
||||||
|
|
||||||
with open(output_file, "wb") as fd:
|
with open(output_file, "wb") as fd:
|
||||||
fd.write(data)
|
fd.write(data)
|
||||||
|
|
||||||
if imagedata:
|
if imagedata:
|
||||||
del imagedata
|
del imagedata
|
||||||
elif self.ismovie:
|
elif self.ismovie:
|
||||||
videodata = self._request_video_data(version=version)
|
videodata = self._request_video_data(version=version)
|
||||||
if videodata.asset is None:
|
if videodata.asset is None:
|
||||||
raise PhotoKitExportError("Could not get video for asset")
|
raise PhotoKitExportError("Could not get video for asset")
|
||||||
|
|
||||||
url = videodata.asset.URL()
|
url = videodata.asset.URL()
|
||||||
path = pathlib.Path(NSURL_to_path(url))
|
path = pathlib.Path(NSURL_to_path(url))
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
raise FileNotFoundError("Could not get path to video file")
|
raise FileNotFoundError("Could not get path to video file")
|
||||||
ext = path.suffix
|
ext = path.suffix
|
||||||
output_file = dest / f"{filename.stem}{ext}"
|
output_file = dest / f"{filename.stem}{ext}"
|
||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
output_file = pathlib.Path(increment_filename(output_file))
|
output_file = pathlib.Path(increment_filename(output_file))
|
||||||
|
|
||||||
FileUtil.copy(path, output_file)
|
FileUtil.copy(path, output_file)
|
||||||
|
|
||||||
return [str(output_file)]
|
return [str(output_file)]
|
||||||
|
|
||||||
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL):
|
||||||
"""Request image data and metadata for self._phasset
|
"""Request image data and metadata for self._phasset
|
||||||
@@ -615,9 +630,7 @@ class PhotoAsset:
|
|||||||
|
|
||||||
nonlocal requestdata
|
nonlocal requestdata
|
||||||
|
|
||||||
options = {}
|
options = {Quartz.kCGImageSourceShouldCache: Foundation.kCFBooleanFalse}
|
||||||
# pylint: disable=no-member
|
|
||||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
|
||||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||||
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
requestdata.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||||
imgSrc, 0, options
|
imgSrc, 0, options
|
||||||
@@ -701,9 +714,7 @@ class PhotoAsset:
|
|||||||
|
|
||||||
nonlocal data
|
nonlocal data
|
||||||
|
|
||||||
options = {}
|
options = {Quartz.kCGImageSourceShouldCache: Foundation.kCFBooleanFalse}
|
||||||
# pylint: disable=no-member
|
|
||||||
options[Quartz.kCGImageSourceShouldCache] = Foundation.kCFBooleanFalse
|
|
||||||
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
imgSrc = Quartz.CGImageSourceCreateWithData(imageData, options)
|
||||||
data.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
data.metadata = Quartz.CGImageSourceCopyPropertiesAtIndex(
|
||||||
imgSrc, 0, options
|
imgSrc, 0, options
|
||||||
@@ -789,7 +800,6 @@ class SlowMoVideoExporter(NSObject):
|
|||||||
self.url = None
|
self.url = None
|
||||||
self.done = None
|
self.done = None
|
||||||
self.nc = None
|
self.nc = None
|
||||||
# super(NSObject, self).dealloc()
|
|
||||||
|
|
||||||
|
|
||||||
class VideoAsset(PhotoAsset):
|
class VideoAsset(PhotoAsset):
|
||||||
@@ -801,7 +811,12 @@ class VideoAsset(PhotoAsset):
|
|||||||
# https://developer.apple.com/documentation/photokit/phimagemanager/1616981-requestexportsessionforvideo?language=objc
|
# https://developer.apple.com/documentation/photokit/phimagemanager/1616981-requestexportsessionforvideo?language=objc
|
||||||
# above 10.15 only
|
# above 10.15 only
|
||||||
def export(
|
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
|
"""Export video to path
|
||||||
|
|
||||||
@@ -810,6 +825,7 @@ class VideoAsset(PhotoAsset):
|
|||||||
filename: str, optional name of exported file; if not provided, defaults to asset's original filename
|
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)
|
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
|
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:
|
Returns:
|
||||||
List of path to exported image(s)
|
List of path to exported image(s)
|
||||||
@@ -819,42 +835,46 @@ class VideoAsset(PhotoAsset):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with objc.autorelease_pool():
|
with objc.autorelease_pool():
|
||||||
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
with pipes() as (out, err):
|
||||||
return [
|
if self.slow_mo and version == PHOTOS_VERSION_CURRENT:
|
||||||
self._export_slow_mo(
|
return [
|
||||||
dest, filename=filename, version=version, overwrite=overwrite
|
self._export_slow_mo(
|
||||||
)
|
dest,
|
||||||
]
|
filename=filename,
|
||||||
|
version=version,
|
||||||
|
overwrite=overwrite,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
filename = (
|
filename = (
|
||||||
pathlib.Path(filename)
|
pathlib.Path(filename)
|
||||||
if filename
|
if filename
|
||||||
else pathlib.Path(self.original_filename)
|
else pathlib.Path(self.original_filename)
|
||||||
)
|
)
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
dest = pathlib.Path(dest)
|
||||||
if not dest.is_dir():
|
if not dest.is_dir():
|
||||||
raise ValueError("dest must be a valid directory: {dest}")
|
raise ValueError("dest must be a valid directory: {dest}")
|
||||||
|
|
||||||
output_file = None
|
output_file = None
|
||||||
videodata = self._request_video_data(version=version)
|
videodata = self._request_video_data(version=version)
|
||||||
if videodata.asset is None:
|
if videodata.asset is None:
|
||||||
raise PhotoKitExportError("Could not get video for asset")
|
raise PhotoKitExportError("Could not get video for asset")
|
||||||
|
|
||||||
url = videodata.asset.URL()
|
url = videodata.asset.URL()
|
||||||
path = pathlib.Path(NSURL_to_path(url))
|
path = pathlib.Path(NSURL_to_path(url))
|
||||||
del videodata
|
del videodata
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
raise FileNotFoundError("Could not get path to video file")
|
raise FileNotFoundError("Could not get path to video file")
|
||||||
ext = path.suffix
|
ext = path.suffix
|
||||||
output_file = dest / f"{filename.stem}{ext}"
|
output_file = dest / f"{filename.stem}{ext}"
|
||||||
|
|
||||||
if not overwrite:
|
if not overwrite:
|
||||||
output_file = pathlib.Path(increment_filename(output_file))
|
output_file = pathlib.Path(increment_filename(output_file))
|
||||||
|
|
||||||
FileUtil.copy(path, output_file)
|
FileUtil.copy(path, output_file)
|
||||||
|
|
||||||
return [str(output_file)]
|
return [str(output_file)]
|
||||||
|
|
||||||
def _export_slow_mo(
|
def _export_slow_mo(
|
||||||
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
|
||||||
@@ -1043,6 +1063,7 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
overwrite=False,
|
overwrite=False,
|
||||||
photo=True,
|
photo=True,
|
||||||
video=True,
|
video=True,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Export image to path
|
"""Export image to path
|
||||||
|
|
||||||
@@ -1053,6 +1074,7 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
overwrite: bool, if True, overwrites destination file if it already exists; default is False
|
overwrite: bool, if True, overwrites destination file if it already exists; default is False
|
||||||
photo: bool, if True, export photo component of live photo
|
photo: bool, if True, export photo component of live photo
|
||||||
video: bool, if True, export live video 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:
|
Returns:
|
||||||
list of [path to exported image and/or video]
|
list of [path to exported image and/or video]
|
||||||
@@ -1063,132 +1085,69 @@ class LivePhotoAsset(PhotoAsset):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
with objc.autorelease_pool():
|
with objc.autorelease_pool():
|
||||||
filename = (
|
with pipes() as (out, err):
|
||||||
pathlib.Path(filename)
|
filename = (
|
||||||
if filename
|
pathlib.Path(filename)
|
||||||
else pathlib.Path(self.original_filename)
|
if filename
|
||||||
)
|
else pathlib.Path(self.original_filename)
|
||||||
|
|
||||||
dest = pathlib.Path(dest)
|
|
||||||
if not dest.is_dir():
|
|
||||||
raise ValueError("dest must be a valid directory: {dest}")
|
|
||||||
|
|
||||||
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
|
||||||
self._manager, self.phasset
|
|
||||||
)
|
|
||||||
resources = request.requestLivePhotoResources(version=version)
|
|
||||||
|
|
||||||
video_resource = None
|
|
||||||
photo_resource = None
|
|
||||||
for resource in resources:
|
|
||||||
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
|
||||||
video_resource = resource
|
|
||||||
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
|
||||||
photo_resource = resource
|
|
||||||
|
|
||||||
if not video_resource or not photo_resource:
|
|
||||||
raise PhotoKitExportError(
|
|
||||||
"Did not find photo/video resources for live photo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
photo_ext = get_preferred_uti_extension(
|
dest = pathlib.Path(dest)
|
||||||
photo_resource.uniformTypeIdentifier()
|
if not dest.is_dir():
|
||||||
)
|
raise ValueError("dest must be a valid directory: {dest}")
|
||||||
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
|
||||||
video_ext = get_preferred_uti_extension(
|
|
||||||
video_resource.uniformTypeIdentifier()
|
|
||||||
)
|
|
||||||
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
|
||||||
|
|
||||||
if not overwrite:
|
request = LivePhotoRequest.alloc().initWithManager_Asset_(
|
||||||
photo_output_file = pathlib.Path(increment_filename(photo_output_file))
|
self._manager, self.phasset
|
||||||
video_output_file = pathlib.Path(increment_filename(video_output_file))
|
)
|
||||||
|
resources = request.requestLivePhotoResources(version=version)
|
||||||
|
|
||||||
# def handler(error):
|
video_resource = None
|
||||||
# if error:
|
photo_resource = None
|
||||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
for resource in resources:
|
||||||
|
if resource.type() == Photos.PHAssetResourceTypePairedVideo:
|
||||||
|
video_resource = resource
|
||||||
|
elif resource.type() == Photos.PHAssetMediaTypeImage:
|
||||||
|
photo_resource = resource
|
||||||
|
|
||||||
# resource_manager = Photos.PHAssetResourceManager.defaultManager()
|
if not video_resource or not photo_resource:
|
||||||
# options = Photos.PHAssetResourceRequestOptions.alloc().init()
|
raise PhotoKitExportError(
|
||||||
# options.setNetworkAccessAllowed_(True)
|
"Did not find photo/video resources for live photo"
|
||||||
# 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_ext = get_preferred_uti_extension(
|
||||||
# photo_output_url = path_to_NSURL(photo_output_file)
|
photo_resource.uniformTypeIdentifier()
|
||||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
)
|
||||||
# photo_resource, photo_output_url, options, handler
|
photo_output_file = dest / f"{filename.stem}.{photo_ext}"
|
||||||
# )
|
video_ext = get_preferred_uti_extension(
|
||||||
# exported.append(str(photo_output_file))
|
video_resource.uniformTypeIdentifier()
|
||||||
|
)
|
||||||
|
video_output_file = dest / f"{filename.stem}.{video_ext}"
|
||||||
|
|
||||||
# if video:
|
if not overwrite:
|
||||||
# video_output_url = path_to_NSURL(video_output_file)
|
photo_output_file = pathlib.Path(
|
||||||
# resource_manager.writeDataForAssetResource_toFile_options_completionHandler_(
|
increment_filename(photo_output_file)
|
||||||
# video_resource, video_output_url, options, handler
|
)
|
||||||
# )
|
video_output_file = pathlib.Path(
|
||||||
# exported.append(str(video_output_file))
|
increment_filename(video_output_file)
|
||||||
|
)
|
||||||
|
|
||||||
# def completion_handler(error):
|
exported = []
|
||||||
# if error:
|
if photo:
|
||||||
# raise PhotoKitExportError(f"writeDataForAssetResource error: {error}")
|
data = self._request_resource_data(photo_resource)
|
||||||
|
# image_data = self.request_image_data(version=version)
|
||||||
|
with open(photo_output_file, "wb") as fd:
|
||||||
|
fd.write(data)
|
||||||
|
exported.append(str(photo_output_file))
|
||||||
|
del data
|
||||||
|
if video:
|
||||||
|
data = self._request_resource_data(video_resource)
|
||||||
|
with open(video_output_file, "wb") as fd:
|
||||||
|
fd.write(data)
|
||||||
|
exported.append(str(video_output_file))
|
||||||
|
del data
|
||||||
|
|
||||||
# would be nice to be able to usewriteDataForAssetResource_toFile_options_completionHandler_
|
request.dealloc()
|
||||||
# but it sets quarantine flags that cause issues so instead, request the data and write the files directly
|
return exported
|
||||||
|
|
||||||
exported = []
|
|
||||||
if photo:
|
|
||||||
data = self._request_resource_data(photo_resource)
|
|
||||||
# image_data = self.request_image_data(version=version)
|
|
||||||
with open(photo_output_file, "wb") as fd:
|
|
||||||
fd.write(data)
|
|
||||||
exported.append(str(photo_output_file))
|
|
||||||
del data
|
|
||||||
if video:
|
|
||||||
data = self._request_resource_data(video_resource)
|
|
||||||
with open(video_output_file, "wb") as fd:
|
|
||||||
fd.write(data)
|
|
||||||
exported.append(str(video_output_file))
|
|
||||||
del data
|
|
||||||
|
|
||||||
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:
|
class PhotoLibrary:
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from more_itertools import chunked
|
|||||||
from .photoinfo import PhotoInfo
|
from .photoinfo import PhotoInfo
|
||||||
from .utils import noop
|
from .utils import noop
|
||||||
|
|
||||||
|
__all__ = ["PhotosAlbum"]
|
||||||
|
|
||||||
|
|
||||||
class PhotosAlbum:
|
class PhotosAlbum:
|
||||||
def __init__(self, name: str, verbose: Optional[callable] = None):
|
def __init__(self, name: str, verbose: Optional[callable] = None):
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ from ..utils import _open_sql_file, normalize_unicode
|
|||||||
|
|
||||||
|
|
||||||
def _process_comments(self):
|
def _process_comments(self):
|
||||||
""" load the comments and likes data from the database
|
"""load the comments and likes data from the database
|
||||||
this is a PhotosDB method that should be imported in
|
this is a PhotosDB method that should be imported in
|
||||||
the PhotosDB class definition in photosdb.py
|
the PhotosDB class definition in photosdb.py
|
||||||
"""
|
"""
|
||||||
self._db_hashed_person_id = {}
|
self._db_hashed_person_id = {}
|
||||||
self._db_comments_uuid = {}
|
self._db_comments_uuid = {}
|
||||||
@@ -24,7 +24,7 @@ def _process_comments(self):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CommentInfo:
|
class CommentInfo:
|
||||||
""" Class for shared photo comments """
|
"""Class for shared photo comments"""
|
||||||
|
|
||||||
datetime: datetime.datetime
|
datetime: datetime.datetime
|
||||||
user: str
|
user: str
|
||||||
@@ -37,7 +37,7 @@ class CommentInfo:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LikeInfo:
|
class LikeInfo:
|
||||||
""" Class for shared photo likes """
|
"""Class for shared photo likes"""
|
||||||
|
|
||||||
datetime: datetime.datetime
|
datetime: datetime.datetime
|
||||||
user: str
|
user: str
|
||||||
@@ -50,16 +50,16 @@ class LikeInfo:
|
|||||||
# The following methods do not get imported into PhotosDB
|
# The following methods do not get imported into PhotosDB
|
||||||
# but will get called by _process_comments
|
# but will get called by _process_comments
|
||||||
def _process_comments_4(photosdb):
|
def _process_comments_4(photosdb):
|
||||||
""" process comments and likes info for Photos <= 4
|
"""process comments and likes info for Photos <= 4
|
||||||
photosdb: PhotosDB instance """
|
photosdb: PhotosDB instance"""
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f"Not implemented for database version {photosdb._db_version}."
|
f"Not implemented for database version {photosdb._db_version}."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _process_comments_5(photosdb):
|
def _process_comments_5(photosdb):
|
||||||
""" process comments and likes info for Photos >= 5
|
"""process comments and likes info for Photos >= 5
|
||||||
photosdb: PhotosDB instance """
|
photosdb: PhotosDB instance"""
|
||||||
|
|
||||||
db = photosdb._tmp_db
|
db = photosdb._tmp_db
|
||||||
|
|
||||||
@@ -70,12 +70,24 @@ def _process_comments_5(photosdb):
|
|||||||
results = conn.execute(
|
results = conn.execute(
|
||||||
"""
|
"""
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
ZINVITEEHASHEDPERSONID,
|
ZINVITEEHASHEDPERSONID AS HASHEDPERSONID,
|
||||||
ZINVITEEFIRSTNAME,
|
ZINVITEEFIRSTNAME AS FIRSTNAME,
|
||||||
ZINVITEELASTNAME,
|
ZINVITEELASTNAME AS LASTNAME,
|
||||||
ZINVITEEFULLNAME
|
ZINVITEEFULLNAME AS FULLNAME
|
||||||
FROM
|
FROM ZCLOUDSHAREDALBUMINVITATIONRECORD
|
||||||
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))
|
db_comments["comments"].append(CommentInfo(dt, user_name, ismine, text))
|
||||||
|
|
||||||
# sort results
|
# 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"]:
|
if photosdb._db_comments_uuid[uuid]["likes"]:
|
||||||
photosdb._db_comments_uuid[uuid]["likes"].sort(key=lambda x: x.datetime)
|
photosdb._db_comments_uuid[uuid]["likes"].sort(key=lambda x: x.datetime)
|
||||||
if photosdb._db_comments_uuid[uuid]["comments"]:
|
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()
|
conn.close()
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ from .._constants import _DB_TABLE_NAMES, _PHOTOS_4_VERSION
|
|||||||
from ..utils import _db_is_locked, _debug, _open_sql_file
|
from ..utils import _db_is_locked, _debug, _open_sql_file
|
||||||
from .photosdb_utils import get_db_version
|
from .photosdb_utils import get_db_version
|
||||||
|
|
||||||
|
|
||||||
def _process_exifinfo(self):
|
def _process_exifinfo(self):
|
||||||
""" load the exif data from the database
|
"""load the exif data from the database
|
||||||
this is a PhotosDB method that should be imported in
|
this is a PhotosDB method that should be imported in
|
||||||
the PhotosDB class definition in photosdb.py
|
the PhotosDB class definition in photosdb.py
|
||||||
"""
|
"""
|
||||||
if self._db_version <= _PHOTOS_4_VERSION:
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
_process_exifinfo_4(self)
|
_process_exifinfo_4(self)
|
||||||
@@ -23,15 +24,15 @@ def _process_exifinfo(self):
|
|||||||
|
|
||||||
|
|
||||||
def _process_exifinfo_4(photosdb):
|
def _process_exifinfo_4(photosdb):
|
||||||
""" process exif info for Photos <= 4
|
"""process exif info for Photos <= 4
|
||||||
photosdb: PhotosDB instance """
|
photosdb: PhotosDB instance"""
|
||||||
photosdb._db_exifinfo_uuid = {}
|
photosdb._db_exifinfo_uuid = {}
|
||||||
raise NotImplementedError(f"search info not implemented for this database version")
|
raise NotImplementedError(f"search info not implemented for this database version")
|
||||||
|
|
||||||
|
|
||||||
def _process_exifinfo_5(photosdb):
|
def _process_exifinfo_5(photosdb):
|
||||||
""" process exif info for Photos >= 5
|
"""process exif info for Photos >= 5
|
||||||
photosdb: PhotosDB instance """
|
photosdb: PhotosDB instance"""
|
||||||
|
|
||||||
db = photosdb._tmp_db
|
db = photosdb._tmp_db
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ from .photosdb_utils import get_db_version
|
|||||||
|
|
||||||
|
|
||||||
def _process_faceinfo(self):
|
def _process_faceinfo(self):
|
||||||
""" Process face information
|
"""Process face information"""
|
||||||
"""
|
|
||||||
|
|
||||||
self._db_faceinfo_pk = {}
|
self._db_faceinfo_pk = {}
|
||||||
self._db_faceinfo_uuid = {}
|
self._db_faceinfo_uuid = {}
|
||||||
@@ -36,7 +35,7 @@ def _process_faceinfo(self):
|
|||||||
|
|
||||||
|
|
||||||
def _process_faceinfo_4(photosdb):
|
def _process_faceinfo_4(photosdb):
|
||||||
""" Process face information for Photos 4 databases
|
"""Process face information for Photos 4 databases
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
photosdb: an OSXPhotosDB instance
|
photosdb: an OSXPhotosDB instance
|
||||||
@@ -146,7 +145,6 @@ def _process_faceinfo_4(photosdb):
|
|||||||
|
|
||||||
# Photos 5 only
|
# Photos 5 only
|
||||||
face["agetype"] = None
|
face["agetype"] = None
|
||||||
face["baldtype"] = None
|
|
||||||
face["eyemakeuptype"] = None
|
face["eyemakeuptype"] = None
|
||||||
face["eyestate"] = None
|
face["eyestate"] = None
|
||||||
face["facialhairtype"] = None
|
face["facialhairtype"] = None
|
||||||
@@ -173,7 +171,7 @@ def _process_faceinfo_4(photosdb):
|
|||||||
|
|
||||||
|
|
||||||
def _process_faceinfo_5(photosdb):
|
def _process_faceinfo_5(photosdb):
|
||||||
""" Process face information for Photos 5 databases
|
"""Process face information for Photos 5 databases
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
photosdb: an OSXPhotosDB instance
|
photosdb: an OSXPhotosDB instance
|
||||||
@@ -194,7 +192,7 @@ def _process_faceinfo_5(photosdb):
|
|||||||
ZDETECTEDFACE.ZPERSON,
|
ZDETECTEDFACE.ZPERSON,
|
||||||
ZPERSON.ZFULLNAME,
|
ZPERSON.ZFULLNAME,
|
||||||
ZDETECTEDFACE.ZAGETYPE,
|
ZDETECTEDFACE.ZAGETYPE,
|
||||||
ZDETECTEDFACE.ZBALDTYPE,
|
NULL, -- ZDETECTEDFACE.ZBALDTYPE (Removed in Monterey)
|
||||||
ZDETECTEDFACE.ZEYEMAKEUPTYPE,
|
ZDETECTEDFACE.ZEYEMAKEUPTYPE,
|
||||||
ZDETECTEDFACE.ZEYESSTATE,
|
ZDETECTEDFACE.ZEYESSTATE,
|
||||||
ZDETECTEDFACE.ZFACIALHAIRTYPE,
|
ZDETECTEDFACE.ZFACIALHAIRTYPE,
|
||||||
@@ -239,7 +237,7 @@ def _process_faceinfo_5(photosdb):
|
|||||||
# 3 ZDETECTEDFACE.ZPERSON,
|
# 3 ZDETECTEDFACE.ZPERSON,
|
||||||
# 4 ZPERSON.ZFULLNAME,
|
# 4 ZPERSON.ZFULLNAME,
|
||||||
# 5 ZDETECTEDFACE.ZAGETYPE,
|
# 5 ZDETECTEDFACE.ZAGETYPE,
|
||||||
# 6 ZDETECTEDFACE.ZBALDTYPE,
|
# 6 ZDETECTEDFACE.ZBALDTYPE, (Not available on Monterey)
|
||||||
# 7 ZDETECTEDFACE.ZEYEMAKEUPTYPE,
|
# 7 ZDETECTEDFACE.ZEYEMAKEUPTYPE,
|
||||||
# 8 ZDETECTEDFACE.ZEYESSTATE,
|
# 8 ZDETECTEDFACE.ZEYESSTATE,
|
||||||
# 9 ZDETECTEDFACE.ZFACIALHAIRTYPE,
|
# 9 ZDETECTEDFACE.ZFACIALHAIRTYPE,
|
||||||
@@ -284,7 +282,6 @@ def _process_faceinfo_5(photosdb):
|
|||||||
face["person"] = person_pk
|
face["person"] = person_pk
|
||||||
face["fullname"] = normalize_unicode(row[4])
|
face["fullname"] = normalize_unicode(row[4])
|
||||||
face["agetype"] = row[5]
|
face["agetype"] = row[5]
|
||||||
face["baldtype"] = row[6]
|
|
||||||
face["eyemakeuptype"] = row[7]
|
face["eyemakeuptype"] = row[7]
|
||||||
face["eyestate"] = row[8]
|
face["eyestate"] = row[8]
|
||||||
face["facialhairtype"] = row[9]
|
face["facialhairtype"] = row[9]
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ from .photosdb_utils import get_db_version
|
|||||||
|
|
||||||
|
|
||||||
def _process_scoreinfo(self):
|
def _process_scoreinfo(self):
|
||||||
""" Process computed photo scores
|
"""Process computed photo scores
|
||||||
Note: Only works on Photos version == 5.0
|
Note: Only works on Photos version == 5.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# _db_scoreinfo_uuid is dict in form {uuid: {score values}}
|
# _db_scoreinfo_uuid is dict in form {uuid: {score values}}
|
||||||
@@ -38,7 +38,7 @@ def _process_scoreinfo(self):
|
|||||||
|
|
||||||
|
|
||||||
def _process_scoreinfo_5(photosdb):
|
def _process_scoreinfo_5(photosdb):
|
||||||
""" Process computed photo scores for Photos 5 databases
|
"""Process computed photo scores for Photos 5 databases
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
photosdb: an OSXPhotosDB instance
|
photosdb: an OSXPhotosDB instance
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ from ..utils import _db_is_locked, _debug, _open_sql_file, normalize_unicode
|
|||||||
|
|
||||||
|
|
||||||
def _process_searchinfo(self):
|
def _process_searchinfo(self):
|
||||||
""" load machine learning/search term label info from a Photos library
|
"""load machine learning/search term label info from a Photos library
|
||||||
db_connection: a connection to the SQLite database file containing the
|
db_connection: a connection to the SQLite database file containing the
|
||||||
search terms. In Photos 5, this is called psi.sqlite
|
search terms. In Photos 5, this is called psi.sqlite
|
||||||
Note: Only works on Photos version == 5.0 """
|
Note: Only works on Photos version == 5.0"""
|
||||||
|
|
||||||
# _db_searchinfo_uuid is dict in form {uuid : [list of associated search info records]
|
# _db_searchinfo_uuid is dict in form {uuid : [list of associated search info records]
|
||||||
self._db_searchinfo_uuid = _db_searchinfo_uuid = {}
|
self._db_searchinfo_uuid = _db_searchinfo_uuid = {}
|
||||||
@@ -155,7 +155,7 @@ def _process_searchinfo(self):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def labels(self):
|
def labels(self):
|
||||||
""" return list of all search info labels found in the library """
|
"""return list of all search info labels found in the library"""
|
||||||
if self._db_version <= _PHOTOS_4_VERSION:
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
logging.warning(f"SearchInfo not implemented for this library version")
|
logging.warning(f"SearchInfo not implemented for this library version")
|
||||||
return []
|
return []
|
||||||
@@ -165,7 +165,7 @@ def labels(self):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def labels_normalized(self):
|
def labels_normalized(self):
|
||||||
""" return list of all normalized search info labels found in the library """
|
"""return list of all normalized search info labels found in the library"""
|
||||||
if self._db_version <= _PHOTOS_4_VERSION:
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
logging.warning(f"SearchInfo not implemented for this library version")
|
logging.warning(f"SearchInfo not implemented for this library version")
|
||||||
return []
|
return []
|
||||||
@@ -175,7 +175,7 @@ def labels_normalized(self):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def labels_as_dict(self):
|
def labels_as_dict(self):
|
||||||
""" return labels as dict of label: count in reverse sorted order (descending) """
|
"""return labels as dict of label: count in reverse sorted order (descending)"""
|
||||||
if self._db_version <= _PHOTOS_4_VERSION:
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
logging.warning(f"SearchInfo not implemented for this library version")
|
logging.warning(f"SearchInfo not implemented for this library version")
|
||||||
return dict()
|
return dict()
|
||||||
@@ -187,7 +187,7 @@ def labels_as_dict(self):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def labels_normalized_as_dict(self):
|
def labels_normalized_as_dict(self):
|
||||||
""" return normalized labels as dict of label: count in reverse sorted order (descending) """
|
"""return normalized labels as dict of label: count in reverse sorted order (descending)"""
|
||||||
if self._db_version <= _PHOTOS_4_VERSION:
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
logging.warning(f"SearchInfo not implemented for this library version")
|
logging.warning(f"SearchInfo not implemented for this library version")
|
||||||
return dict()
|
return dict()
|
||||||
@@ -201,8 +201,8 @@ def labels_normalized_as_dict(self):
|
|||||||
|
|
||||||
@lru_cache(maxsize=128)
|
@lru_cache(maxsize=128)
|
||||||
def ints_to_uuid(uuid_0, uuid_1):
|
def ints_to_uuid(uuid_0, uuid_1):
|
||||||
""" convert two signed ints into a UUID strings
|
"""convert two signed ints into a UUID strings
|
||||||
uuid_0, uuid_1: the two int components of an RFC 4122 UUID """
|
uuid_0, uuid_1: the two int components of an RFC 4122 UUID"""
|
||||||
|
|
||||||
# assumes uuid imported as uuidlib (to avoid namespace conflict with other uses of uuid)
|
# assumes uuid imported as uuidlib (to avoid namespace conflict with other uses of uuid)
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from collections.abc import Iterable
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
import bitmath
|
import bitmath
|
||||||
import photoscript
|
import photoscript
|
||||||
|
from rich import print
|
||||||
|
|
||||||
from .._constants import (
|
from .._constants import (
|
||||||
_DB_TABLE_NAMES,
|
_DB_TABLE_NAMES,
|
||||||
@@ -25,22 +27,28 @@ from .._constants import (
|
|||||||
_PHOTO_TYPE,
|
_PHOTO_TYPE,
|
||||||
_PHOTOS_3_VERSION,
|
_PHOTOS_3_VERSION,
|
||||||
_PHOTOS_4_ALBUM_KIND,
|
_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_ROOT_FOLDER,
|
||||||
_PHOTOS_4_TOP_LEVEL_ALBUM,
|
_PHOTOS_4_TOP_LEVEL_ALBUMS,
|
||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
_PHOTOS_5_ALBUM_KIND,
|
_PHOTOS_5_ALBUM_KIND,
|
||||||
_PHOTOS_5_FOLDER_KIND,
|
_PHOTOS_5_FOLDER_KIND,
|
||||||
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND,
|
||||||
|
_PHOTOS_5_PROJECT_ALBUM_KIND,
|
||||||
_PHOTOS_5_ROOT_FOLDER_KIND,
|
_PHOTOS_5_ROOT_FOLDER_KIND,
|
||||||
_PHOTOS_5_SHARED_ALBUM_KIND,
|
_PHOTOS_5_SHARED_ALBUM_KIND,
|
||||||
|
_PHOTOS_5_VERSION,
|
||||||
_TESTED_OS_VERSIONS,
|
_TESTED_OS_VERSIONS,
|
||||||
_UNKNOWN_PERSON,
|
_UNKNOWN_PERSON,
|
||||||
BURST_KEY,
|
BURST_KEY,
|
||||||
|
BURST_PICK_TYPE_NONE,
|
||||||
BURST_SELECTED,
|
BURST_SELECTED,
|
||||||
TIME_DELTA,
|
TIME_DELTA,
|
||||||
)
|
)
|
||||||
from .._version import __version__
|
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 ..datetime_utils import datetime_has_tz, datetime_naive_to_local
|
||||||
from ..fileutil import FileUtil
|
from ..fileutil import FileUtil
|
||||||
from ..personinfo import PersonInfo
|
from ..personinfo import PersonInfo
|
||||||
@@ -59,6 +67,8 @@ from ..utils import (
|
|||||||
)
|
)
|
||||||
from .photosdb_utils import get_db_model_version, get_db_version
|
from .photosdb_utils import get_db_model_version, get_db_version
|
||||||
|
|
||||||
|
__all__ = ["PhotosDB"]
|
||||||
|
|
||||||
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
# TODO: Add test for imageTimeZoneOffsetSeconds = None
|
||||||
# TODO: Add test for __str__
|
# TODO: Add test for __str__
|
||||||
# TODO: Add special albums and magic albums
|
# TODO: Add special albums and magic albums
|
||||||
@@ -250,6 +260,10 @@ class PhotosDB:
|
|||||||
# Dict to hold information on volume names (Photos 5+)
|
# Dict to hold information on volume names (Photos 5+)
|
||||||
self._db_filesystem_volumes = {}
|
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():
|
if _debug():
|
||||||
logging.debug(f"dbfile = {dbfile}")
|
logging.debug(f"dbfile = {dbfile}")
|
||||||
|
|
||||||
@@ -330,6 +344,8 @@ class PhotosDB:
|
|||||||
else:
|
else:
|
||||||
self._process_database5()
|
self._process_database5()
|
||||||
|
|
||||||
|
self._db_connection, _ = self.get_db_connection()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keywords_as_dict(self):
|
def keywords_as_dict(self):
|
||||||
"""return keywords as dict of keyword, count in reverse sorted order (descending)"""
|
"""return keywords as dict of keyword, count in reverse sorted order (descending)"""
|
||||||
@@ -421,7 +437,7 @@ class PhotosDB:
|
|||||||
for folder, detail in self._dbfolder_details.items()
|
for folder, detail in self._dbfolder_details.items()
|
||||||
if not detail["intrash"]
|
if not detail["intrash"]
|
||||||
and not detail["isMagic"]
|
and not detail["isMagic"]
|
||||||
and detail["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
|
and detail["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
folders = [
|
folders = [
|
||||||
@@ -442,7 +458,7 @@ class PhotosDB:
|
|||||||
for folder in self._dbfolder_details.values()
|
for folder in self._dbfolder_details.values()
|
||||||
if not folder["intrash"]
|
if not folder["intrash"]
|
||||||
and not folder["isMagic"]
|
and not folder["isMagic"]
|
||||||
and folder["parentFolderUuid"] == _PHOTOS_4_TOP_LEVEL_ALBUM
|
and folder["parentFolderUuid"] in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
folder_names = [
|
folder_names = [
|
||||||
@@ -521,6 +537,18 @@ class PhotosDB:
|
|||||||
]
|
]
|
||||||
return self._import_info
|
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
|
@property
|
||||||
def db_version(self):
|
def db_version(self):
|
||||||
"""return the database version as stored in LiGlobals table"""
|
"""return the database version as stored in LiGlobals table"""
|
||||||
@@ -632,14 +660,18 @@ class PhotosDB:
|
|||||||
|
|
||||||
for person in c:
|
for person in c:
|
||||||
pk = person[0]
|
pk = person[0]
|
||||||
fullname = person[2] if person[2] is not None else _UNKNOWN_PERSON
|
fullname = (
|
||||||
|
normalize_unicode(person[2])
|
||||||
|
if person[2] is not None
|
||||||
|
else _UNKNOWN_PERSON
|
||||||
|
)
|
||||||
self._dbpersons_pk[pk] = {
|
self._dbpersons_pk[pk] = {
|
||||||
"pk": pk,
|
"pk": pk,
|
||||||
"uuid": person[1],
|
"uuid": person[1],
|
||||||
"fullname": fullname,
|
"fullname": fullname,
|
||||||
"facecount": person[3],
|
"facecount": person[3],
|
||||||
"keyface": person[5],
|
"keyface": person[5],
|
||||||
"displayname": person[4],
|
"displayname": normalize_unicode(person[4]),
|
||||||
"photo_uuid": None,
|
"photo_uuid": None,
|
||||||
"keyface_uuid": None,
|
"keyface_uuid": None,
|
||||||
}
|
}
|
||||||
@@ -706,13 +738,6 @@ class PhotosDB:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
self._dbfaces_pk[pk] = [uuid]
|
self._dbfaces_pk[pk] = [uuid]
|
||||||
|
|
||||||
if _debug():
|
|
||||||
logging.debug(f"Finished walking through persons")
|
|
||||||
logging.debug(pformat(self._dbpersons_pk))
|
|
||||||
logging.debug(pformat(self._dbpersons_fullname))
|
|
||||||
logging.debug(pformat(self._dbfaces_pk))
|
|
||||||
logging.debug(pformat(self._dbfaces_uuid))
|
|
||||||
|
|
||||||
# Get info on albums
|
# Get info on albums
|
||||||
verbose("Processing albums.")
|
verbose("Processing albums.")
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -790,8 +815,8 @@ class PhotosDB:
|
|||||||
"creation_date": album[8],
|
"creation_date": album[8],
|
||||||
"start_date": None, # Photos 5 only
|
"start_date": None, # Photos 5 only
|
||||||
"end_date": None, # Photos 5 only
|
"end_date": None, # Photos 5 only
|
||||||
"customsortascending": None, # Photos 5 only
|
"customsortascending": None, # Photos 5 only
|
||||||
"customsortkey": None, # Photos 5 only
|
"customsortkey": None, # Photos 5 only
|
||||||
}
|
}
|
||||||
|
|
||||||
# get details about folders
|
# get details about folders
|
||||||
@@ -840,24 +865,15 @@ class PhotosDB:
|
|||||||
# build folder hierarchy
|
# build folder hierarchy
|
||||||
for album, details in self._dbalbum_details.items():
|
for album, details in self._dbalbum_details.items():
|
||||||
parent_folder = details["folderUuid"]
|
parent_folder = details["folderUuid"]
|
||||||
if details[
|
if (
|
||||||
"albumSubclass"
|
details["albumSubclass"] == _PHOTOS_4_ALBUM_KIND
|
||||||
] == _PHOTOS_4_ALBUM_KIND and parent_folder not in [
|
and parent_folder not in _PHOTOS_4_TOP_LEVEL_ALBUMS
|
||||||
_PHOTOS_4_TOP_LEVEL_ALBUM
|
):
|
||||||
]:
|
|
||||||
folder_hierarchy = self._build_album_folder_hierarchy_4(parent_folder)
|
folder_hierarchy = self._build_album_folder_hierarchy_4(parent_folder)
|
||||||
self._dbalbum_folders[album] = folder_hierarchy
|
self._dbalbum_folders[album] = folder_hierarchy
|
||||||
else:
|
else:
|
||||||
self._dbalbum_folders[album] = {}
|
self._dbalbum_folders[album] = {}
|
||||||
|
|
||||||
if _debug():
|
|
||||||
logging.debug(f"Finished walking through albums")
|
|
||||||
logging.debug(pformat(self._dbalbums_album))
|
|
||||||
logging.debug(pformat(self._dbalbums_uuid))
|
|
||||||
logging.debug(pformat(self._dbalbum_details))
|
|
||||||
logging.debug(pformat(self._dbalbum_folders))
|
|
||||||
logging.debug(pformat(self._dbfolder_details))
|
|
||||||
|
|
||||||
# Get info on keywords
|
# Get info on keywords
|
||||||
verbose("Processing keywords.")
|
verbose("Processing keywords.")
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -873,13 +889,16 @@ class PhotosDB:
|
|||||||
RKMaster.uuid = RKVersion.masterUuid
|
RKMaster.uuid = RKVersion.masterUuid
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
for keyword in c:
|
for keyword_title, keyword_uuid, _ in c:
|
||||||
if not keyword[1] in self._dbkeywords_uuid:
|
keyword_title = normalize_unicode(keyword_title)
|
||||||
self._dbkeywords_uuid[keyword[1]] = []
|
try:
|
||||||
if not keyword[0] in self._dbkeywords_keyword:
|
self._dbkeywords_uuid[keyword_uuid].append(keyword_title)
|
||||||
self._dbkeywords_keyword[keyword[0]] = []
|
except KeyError:
|
||||||
self._dbkeywords_uuid[keyword[1]].append(keyword[0])
|
self._dbkeywords_uuid[keyword_uuid] = [keyword_title]
|
||||||
self._dbkeywords_keyword[keyword[0]].append(keyword[1])
|
try:
|
||||||
|
self._dbkeywords_keyword[keyword_title].append(keyword_uuid)
|
||||||
|
except KeyError:
|
||||||
|
self._dbkeywords_keyword[keyword_title] = [keyword_uuid]
|
||||||
|
|
||||||
# Get info on disk volumes
|
# Get info on disk volumes
|
||||||
c.execute("select RKVolume.modelId, RKVolume.name from RKVolume")
|
c.execute("select RKVolume.modelId, RKVolume.name from RKVolume")
|
||||||
@@ -1001,13 +1020,11 @@ class PhotosDB:
|
|||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = row[0]
|
uuid = row[0]
|
||||||
if _debug():
|
|
||||||
logging.debug(f"uuid = '{uuid}, master = '{row[2]}")
|
|
||||||
self._dbphotos[uuid] = {}
|
self._dbphotos[uuid] = {}
|
||||||
self._dbphotos[uuid]["_uuid"] = uuid # stored here for easier debugging
|
self._dbphotos[uuid]["_uuid"] = uuid # stored here for easier debugging
|
||||||
self._dbphotos[uuid]["modelID"] = row[1]
|
self._dbphotos[uuid]["modelID"] = row[1]
|
||||||
self._dbphotos[uuid]["masterUuid"] = row[2]
|
self._dbphotos[uuid]["masterUuid"] = row[2]
|
||||||
self._dbphotos[uuid]["filename"] = row[3]
|
self._dbphotos[uuid]["filename"] = normalize_unicode(row[3])
|
||||||
|
|
||||||
# There are sometimes negative values for lastmodifieddate in the database
|
# There are sometimes negative values for lastmodifieddate in the database
|
||||||
# I don't know what these mean but they will raise exception in datetime if
|
# I don't know what these mean but they will raise exception in datetime if
|
||||||
@@ -1104,7 +1121,9 @@ class PhotosDB:
|
|||||||
# get info on special types
|
# get info on special types
|
||||||
self._dbphotos[uuid]["specialType"] = row[25]
|
self._dbphotos[uuid]["specialType"] = row[25]
|
||||||
self._dbphotos[uuid]["masterModelID"] = row[26]
|
self._dbphotos[uuid]["masterModelID"] = row[26]
|
||||||
self._dbphotos[uuid]["pk"] = row[26] # same as masterModelID, to match Photos 5
|
self._dbphotos[uuid]["pk"] = row[
|
||||||
|
26
|
||||||
|
] # same as masterModelID, to match Photos 5
|
||||||
self._dbphotos[uuid]["panorama"] = True if row[25] == 1 else False
|
self._dbphotos[uuid]["panorama"] = True if row[25] == 1 else False
|
||||||
self._dbphotos[uuid]["slow_mo"] = True if row[25] == 2 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
|
self._dbphotos[uuid]["time_lapse"] = True if row[25] == 3 else False
|
||||||
@@ -1195,6 +1214,9 @@ class PhotosDB:
|
|||||||
self._dbphotos[uuid]["import_uuid"] = row[44]
|
self._dbphotos[uuid]["import_uuid"] = row[44]
|
||||||
self._dbphotos[uuid]["fok_import_session"] = None
|
self._dbphotos[uuid]["fok_import_session"] = None
|
||||||
|
|
||||||
|
# photos 5+ only, for shared photos
|
||||||
|
self._dbphotos[uuid]["cloudownerhashedpersonid"] = None
|
||||||
|
|
||||||
# compute signatures for finding possible duplicates
|
# compute signatures for finding possible duplicates
|
||||||
signature = self._duplicate_signature(uuid)
|
signature = self._duplicate_signature(uuid)
|
||||||
try:
|
try:
|
||||||
@@ -1241,13 +1263,13 @@ class PhotosDB:
|
|||||||
info["volumeId"] = row[1]
|
info["volumeId"] = row[1]
|
||||||
info["imagePath"] = row[2]
|
info["imagePath"] = row[2]
|
||||||
info["isMissing"] = row[3]
|
info["isMissing"] = row[3]
|
||||||
info["originalFilename"] = row[4]
|
info["originalFilename"] = normalize_unicode(row[4])
|
||||||
info["UTI"] = row[5]
|
info["UTI"] = row[5]
|
||||||
info["modelID"] = row[6]
|
info["modelID"] = row[6]
|
||||||
info["fileSize"] = row[7]
|
info["fileSize"] = row[7]
|
||||||
info["isTrulyRAW"] = row[8]
|
info["isTrulyRAW"] = row[8]
|
||||||
info["alternateMasterUuid"] = row[9]
|
info["alternateMasterUuid"] = row[9]
|
||||||
info["filename"] = row[10]
|
info["filename"] = normalize_unicode(row[10])
|
||||||
self._dbphotos_master[uuid] = info
|
self._dbphotos_master[uuid] = info
|
||||||
|
|
||||||
# get details needed to find path of the edited photos
|
# get details needed to find path of the edited photos
|
||||||
@@ -1519,39 +1541,6 @@ class PhotosDB:
|
|||||||
|
|
||||||
# done processing, dump debug data if requested
|
# done processing, dump debug data if requested
|
||||||
verbose("Done processing details from Photos library.")
|
verbose("Done processing details from Photos library.")
|
||||||
if _debug():
|
|
||||||
logging.debug("Faces (_dbfaces_uuid):")
|
|
||||||
logging.debug(pformat(self._dbfaces_uuid))
|
|
||||||
|
|
||||||
logging.debug("Persons (_dbpersons_pk):")
|
|
||||||
logging.debug(pformat(self._dbpersons_pk))
|
|
||||||
|
|
||||||
logging.debug("Keywords by uuid (_dbkeywords_uuid):")
|
|
||||||
logging.debug(pformat(self._dbkeywords_uuid))
|
|
||||||
|
|
||||||
logging.debug("Keywords by keyword (_dbkeywords_keywords):")
|
|
||||||
logging.debug(pformat(self._dbkeywords_keyword))
|
|
||||||
|
|
||||||
logging.debug("Albums by uuid (_dbalbums_uuid):")
|
|
||||||
logging.debug(pformat(self._dbalbums_uuid))
|
|
||||||
|
|
||||||
logging.debug("Albums by album (_dbalbums_albums):")
|
|
||||||
logging.debug(pformat(self._dbalbums_album))
|
|
||||||
|
|
||||||
logging.debug("Album details (_dbalbum_details):")
|
|
||||||
logging.debug(pformat(self._dbalbum_details))
|
|
||||||
|
|
||||||
logging.debug("Album titles (_dbalbum_titles):")
|
|
||||||
logging.debug(pformat(self._dbalbum_titles))
|
|
||||||
|
|
||||||
logging.debug("Volumes (_dbvolumes):")
|
|
||||||
logging.debug(pformat(self._dbvolumes))
|
|
||||||
|
|
||||||
logging.debug("Photos (_dbphotos):")
|
|
||||||
logging.debug(pformat(self._dbphotos))
|
|
||||||
|
|
||||||
logging.debug("Burst Photos (dbphotos_burst:")
|
|
||||||
logging.debug(pformat(self._dbphotos_burst))
|
|
||||||
|
|
||||||
def _build_album_folder_hierarchy_4(self, uuid, folders=None):
|
def _build_album_folder_hierarchy_4(self, uuid, folders=None):
|
||||||
"""recursively build folder/album hierarchy
|
"""recursively build folder/album hierarchy
|
||||||
@@ -1569,7 +1558,7 @@ class PhotosDB:
|
|||||||
if parent_uuid is None:
|
if parent_uuid is None:
|
||||||
return folders
|
return folders
|
||||||
|
|
||||||
if parent_uuid == _PHOTOS_4_TOP_LEVEL_ALBUM:
|
if parent_uuid in _PHOTOS_4_TOP_LEVEL_ALBUMS:
|
||||||
if not folders:
|
if not folders:
|
||||||
# this is a top-level folder with no sub-folders
|
# this is a top-level folder with no sub-folders
|
||||||
folders = {uuid: None}
|
folders = {uuid: None}
|
||||||
@@ -1642,7 +1631,7 @@ class PhotosDB:
|
|||||||
for person in c:
|
for person in c:
|
||||||
pk = person[0]
|
pk = person[0]
|
||||||
fullname = (
|
fullname = (
|
||||||
person[2]
|
normalize_unicode(person[2])
|
||||||
if (person[2] != "" and person[2] is not None)
|
if (person[2] != "" and person[2] is not None)
|
||||||
else _UNKNOWN_PERSON
|
else _UNKNOWN_PERSON
|
||||||
)
|
)
|
||||||
@@ -1652,7 +1641,7 @@ class PhotosDB:
|
|||||||
"fullname": fullname,
|
"fullname": fullname,
|
||||||
"facecount": person[3],
|
"facecount": person[3],
|
||||||
"keyface": person[4],
|
"keyface": person[4],
|
||||||
"displayname": person[5],
|
"displayname": normalize_unicode(person[5]),
|
||||||
"photo_uuid": None,
|
"photo_uuid": None,
|
||||||
"keyface_uuid": None,
|
"keyface_uuid": None,
|
||||||
}
|
}
|
||||||
@@ -1716,13 +1705,6 @@ class PhotosDB:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
self._dbfaces_pk[pk] = [uuid]
|
self._dbfaces_pk[pk] = [uuid]
|
||||||
|
|
||||||
if _debug():
|
|
||||||
logging.debug(f"Finished walking through persons")
|
|
||||||
logging.debug(pformat(self._dbpersons_pk))
|
|
||||||
logging.debug(pformat(self._dbpersons_fullname))
|
|
||||||
logging.debug(pformat(self._dbfaces_pk))
|
|
||||||
logging.debug(pformat(self._dbfaces_uuid))
|
|
||||||
|
|
||||||
# get details about albums
|
# get details about albums
|
||||||
verbose("Processing albums.")
|
verbose("Processing albums.")
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -1839,13 +1821,6 @@ class PhotosDB:
|
|||||||
# shared albums can't be in folders
|
# shared albums can't be in folders
|
||||||
self._dbalbum_folders[album] = []
|
self._dbalbum_folders[album] = []
|
||||||
|
|
||||||
if _debug():
|
|
||||||
logging.debug(f"Finished walking through albums")
|
|
||||||
logging.debug(pformat(self._dbalbums_album))
|
|
||||||
logging.debug(pformat(self._dbalbums_uuid))
|
|
||||||
logging.debug(pformat(self._dbalbum_details))
|
|
||||||
logging.debug(pformat(self._dbalbum_folders))
|
|
||||||
|
|
||||||
# get details on keywords
|
# get details on keywords
|
||||||
verbose("Processing keywords.")
|
verbose("Processing keywords.")
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -1855,29 +1830,22 @@ class PhotosDB:
|
|||||||
JOIN Z_1KEYWORDS ON Z_1KEYWORDS.Z_1ASSETATTRIBUTES = ZADDITIONALASSETATTRIBUTES.Z_PK
|
JOIN Z_1KEYWORDS ON Z_1KEYWORDS.Z_1ASSETATTRIBUTES = ZADDITIONALASSETATTRIBUTES.Z_PK
|
||||||
JOIN ZKEYWORD ON ZKEYWORD.Z_PK = {keyword_join} """
|
JOIN ZKEYWORD ON ZKEYWORD.Z_PK = {keyword_join} """
|
||||||
)
|
)
|
||||||
for keyword in c:
|
for keyword_title, keyword_uuid in c:
|
||||||
keyword_title = normalize_unicode(keyword[0])
|
keyword_title = normalize_unicode(keyword_title)
|
||||||
if not keyword[1] in self._dbkeywords_uuid:
|
try:
|
||||||
self._dbkeywords_uuid[keyword[1]] = []
|
self._dbkeywords_uuid[keyword_uuid].append(keyword_title)
|
||||||
if not keyword_title in self._dbkeywords_keyword:
|
except KeyError:
|
||||||
self._dbkeywords_keyword[keyword_title] = []
|
self._dbkeywords_uuid[keyword_uuid] = [keyword_title]
|
||||||
self._dbkeywords_uuid[keyword[1]].append(keyword[0])
|
try:
|
||||||
self._dbkeywords_keyword[keyword_title].append(keyword[1])
|
self._dbkeywords_keyword[keyword_title].append(keyword_uuid)
|
||||||
|
except KeyError:
|
||||||
if _debug():
|
self._dbkeywords_keyword[keyword_title] = [keyword_uuid]
|
||||||
logging.debug(f"Finished walking through keywords")
|
|
||||||
logging.debug(pformat(self._dbkeywords_keyword))
|
|
||||||
logging.debug(pformat(self._dbkeywords_uuid))
|
|
||||||
|
|
||||||
# get details on disk volumes
|
# get details on disk volumes
|
||||||
c.execute("SELECT ZUUID, ZNAME from ZFILESYSTEMVOLUME")
|
c.execute("SELECT ZUUID, ZNAME from ZFILESYSTEMVOLUME")
|
||||||
for vol in c:
|
for vol in c:
|
||||||
self._dbvolumes[vol[0]] = vol[1]
|
self._dbvolumes[vol[0]] = vol[1]
|
||||||
|
|
||||||
if _debug():
|
|
||||||
logging.debug(f"Finished walking through volumes")
|
|
||||||
logging.debug(self._dbvolumes)
|
|
||||||
|
|
||||||
# get details about photos
|
# get details about photos
|
||||||
verbose("Processing photo details.")
|
verbose("Processing photo details.")
|
||||||
c.execute(
|
c.execute(
|
||||||
@@ -1923,7 +1891,8 @@ class PhotosDB:
|
|||||||
{asset_table}.ZTRASHEDDATE,
|
{asset_table}.ZTRASHEDDATE,
|
||||||
{asset_table}.ZSAVEDASSETTYPE,
|
{asset_table}.ZSAVEDASSETTYPE,
|
||||||
{asset_table}.ZADDEDDATE,
|
{asset_table}.ZADDEDDATE,
|
||||||
{asset_table}.Z_PK
|
{asset_table}.Z_PK,
|
||||||
|
{asset_table}.ZCLOUDOWNERHASHEDPERSONID
|
||||||
FROM {asset_table}
|
FROM {asset_table}
|
||||||
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
|
||||||
ORDER BY {asset_table}.ZUUID """
|
ORDER BY {asset_table}.ZUUID """
|
||||||
@@ -1973,6 +1942,7 @@ class PhotosDB:
|
|||||||
# 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported
|
# 40 ZGENERICASSET.ZSAVEDASSETTYPE -- how item imported
|
||||||
# 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library
|
# 41 ZGENERICASSET.ZADDEDDATE -- date item added to the library
|
||||||
# 42 ZGENERICASSET.Z_PK -- primary key
|
# 42 ZGENERICASSET.Z_PK -- primary key
|
||||||
|
# 43 ZGENERICASSET.ZCLOUDOWNERHASHEDPERSONID -- used to look up owner name (for shared photos)
|
||||||
|
|
||||||
for row in c:
|
for row in c:
|
||||||
uuid = row[0]
|
uuid = row[0]
|
||||||
@@ -2009,8 +1979,8 @@ class PhotosDB:
|
|||||||
|
|
||||||
info["hidden"] = row[9]
|
info["hidden"] = row[9]
|
||||||
info["favorite"] = row[10]
|
info["favorite"] = row[10]
|
||||||
info["originalFilename"] = row[3]
|
info["originalFilename"] = normalize_unicode(row[3])
|
||||||
info["filename"] = row[12]
|
info["filename"] = normalize_unicode(row[12])
|
||||||
info["directory"] = row[11]
|
info["directory"] = row[11]
|
||||||
|
|
||||||
# set latitude and longitude
|
# set latitude and longitude
|
||||||
@@ -2158,6 +2128,7 @@ class PhotosDB:
|
|||||||
info["added_date"] = datetime(1970, 1, 1)
|
info["added_date"] = datetime(1970, 1, 1)
|
||||||
|
|
||||||
info["pk"] = row[42]
|
info["pk"] = row[42]
|
||||||
|
info["cloudownerhashedpersonid"] = row[43]
|
||||||
|
|
||||||
# initialize import session info which will be filled in later
|
# initialize import session info which will be filled in later
|
||||||
# not every photo has an import session so initialize all records now
|
# not every photo has an import session so initialize all records now
|
||||||
@@ -2481,50 +2452,115 @@ class PhotosDB:
|
|||||||
verbose("Processing comments and likes for shared photos.")
|
verbose("Processing comments and likes for shared photos.")
|
||||||
self._process_comments()
|
self._process_comments()
|
||||||
|
|
||||||
|
# process moments
|
||||||
|
verbose("Processing moments.")
|
||||||
|
self._process_moments()
|
||||||
|
|
||||||
# done processing, dump debug data if requested
|
# done processing, dump debug data if requested
|
||||||
verbose("Done processing details from Photos library.")
|
verbose("Done processing details from Photos library.")
|
||||||
if _debug():
|
|
||||||
logging.debug("Faces (_dbfaces_uuid):")
|
|
||||||
logging.debug(pformat(self._dbfaces_uuid))
|
|
||||||
|
|
||||||
logging.debug("Persons (_dbpersons_pk):")
|
def _process_moments(self):
|
||||||
logging.debug(pformat(self._dbpersons_pk))
|
"""Process data from ZMOMENT table"""
|
||||||
|
# _db_moment_pk is dict in form {pk: {moment info}} by ZMOMENT.Z_PK
|
||||||
|
|
||||||
logging.debug("Keywords by uuid (_dbkeywords_uuid):")
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
logging.debug(pformat(self._dbkeywords_uuid))
|
raise NotImplementedError(
|
||||||
|
f"Moment info implemented for this database version"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._process_moment_5()
|
||||||
|
|
||||||
logging.debug("Keywords by keyword (_dbkeywords_keywords):")
|
def _process_moment_5(self):
|
||||||
logging.debug(pformat(self._dbkeywords_keyword))
|
"""Process moment info for Photos 5 databases"""
|
||||||
|
|
||||||
logging.debug("Albums by uuid (_dbalbums_uuid):")
|
self._db_moment_pk = {}
|
||||||
logging.debug(pformat(self._dbalbums_uuid))
|
|
||||||
|
|
||||||
logging.debug("Albums by album (_dbalbums_albums):")
|
results = self.execute(
|
||||||
logging.debug(pformat(self._dbalbums_album))
|
f"""
|
||||||
|
SELECT
|
||||||
|
Z_PK,
|
||||||
|
ZTIMEZONEOFFSET,
|
||||||
|
ZTRASHEDSTATE,
|
||||||
|
ZAPPROXIMATELATITUDE,
|
||||||
|
ZAPPROXIMATELONGITUDE,
|
||||||
|
ZENDDATE,
|
||||||
|
ZMODIFICATIONDATE,
|
||||||
|
ZREPRESENTATIVEDATE,
|
||||||
|
ZSTARTDATE,
|
||||||
|
ZSUBTITLE,
|
||||||
|
ZTITLE,
|
||||||
|
ZUUID
|
||||||
|
FROM ZMOMENT"""
|
||||||
|
)
|
||||||
|
|
||||||
logging.debug("Album details (_dbalbum_details):")
|
# results
|
||||||
logging.debug(pformat(self._dbalbum_details))
|
# 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
|
||||||
|
|
||||||
logging.debug("Album titles (_dbalbum_titles):")
|
for row in results:
|
||||||
logging.debug(pformat(self._dbalbum_titles))
|
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"] = normalize_unicode(row[9])
|
||||||
|
moment_info["title"] = normalize_unicode(row[10])
|
||||||
|
moment_info["uuid"] = row[11]
|
||||||
|
|
||||||
logging.debug("Album folders (_dbalbum_folders):")
|
# if both lat/lon == -180, then it means location undefined
|
||||||
logging.debug(pformat(self._dbalbum_folders))
|
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"]
|
||||||
|
|
||||||
logging.debug("Album parent folders (_dbalbum_parent_folders):")
|
# process date stamps
|
||||||
logging.debug(pformat(self._dbalbum_parent_folders))
|
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)
|
||||||
|
|
||||||
logging.debug("Albums pk (_dbalbums_pk):")
|
# process title/subtitle
|
||||||
logging.debug(pformat(self._dbalbums_pk))
|
moment_info["title"] = moment_info["title"] or ""
|
||||||
|
moment_info["subtitle"] = moment_info["subtitle"] or ""
|
||||||
|
|
||||||
logging.debug("Volumes (_dbvolumes):")
|
self._db_moment_pk[moment_info["pk"]] = moment_info
|
||||||
logging.debug(pformat(self._dbvolumes))
|
|
||||||
|
|
||||||
logging.debug("Photos (_dbphotos):")
|
|
||||||
logging.debug(pformat(self._dbphotos))
|
|
||||||
|
|
||||||
logging.debug("Burst Photos (dbphotos_burst:")
|
|
||||||
logging.debug(pformat(self._dbphotos_burst))
|
|
||||||
|
|
||||||
def _build_album_folder_hierarchy_5(self, uuid, folders=None):
|
def _build_album_folder_hierarchy_5(self, uuid, folders=None):
|
||||||
"""recursively build folder/album hierarchy
|
"""recursively build folder/album hierarchy
|
||||||
@@ -2702,7 +2738,7 @@ class PhotosDB:
|
|||||||
hierarchy = _recurse_folder_hierarchy(folders)
|
hierarchy = _recurse_folder_hierarchy(folders)
|
||||||
return hierarchy
|
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
|
"""Return list of album UUIDs found in photos database
|
||||||
|
|
||||||
Filters out albums in the trash and any special album types
|
Filters out albums in the trash and any special album types
|
||||||
@@ -2710,20 +2746,21 @@ class PhotosDB:
|
|||||||
Args:
|
Args:
|
||||||
shared: boolean; if True, returns shared albums, else normal albums
|
shared: boolean; if True, returns shared albums, else normal albums
|
||||||
import_session: boolean, if True, returns import session albums, else normal or shared 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
|
Note: flags (shared, import_session) are mutually exclusive
|
||||||
|
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: raised if mutually exclusive flags passed
|
ValueError: raised if mutually exclusive flags passed
|
||||||
|
|
||||||
Returns: list of album UUIDs
|
Returns: list of album UUIDs
|
||||||
"""
|
"""
|
||||||
if shared and import_session:
|
if sum(bool(x) for x in [shared, import_session, project]) > 1:
|
||||||
raise ValueError(
|
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:
|
if self._db_version <= _PHOTOS_4_VERSION:
|
||||||
version4 = True
|
|
||||||
if shared:
|
if shared:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
f"Shared albums not implemented for Photos library version {self._db_version}"
|
f"Shared albums not implemented for Photos library version {self._db_version}"
|
||||||
@@ -2734,16 +2771,44 @@ class PhotosDB:
|
|||||||
f"Import sessions not implemented for Photos library version {self._db_version}"
|
f"Import sessions not implemented for Photos library version {self._db_version}"
|
||||||
)
|
)
|
||||||
return [] # not implemented for _PHOTOS_4_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
|
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:
|
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 = []
|
album_list = []
|
||||||
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
|
# look through _dbalbum_details because _dbalbums_album won't have empty albums it
|
||||||
@@ -2755,13 +2820,6 @@ class PhotosDB:
|
|||||||
(shared and detail["cloudownerhashedpersonid"] is not None)
|
(shared and detail["cloudownerhashedpersonid"] is not None)
|
||||||
or (not shared and detail["cloudownerhashedpersonid"] is 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)
|
album_list.append(album)
|
||||||
return album_list
|
return album_list
|
||||||
@@ -2864,6 +2922,7 @@ class PhotosDB:
|
|||||||
if keywords:
|
if keywords:
|
||||||
keyword_set = set()
|
keyword_set = set()
|
||||||
for keyword in keywords:
|
for keyword in keywords:
|
||||||
|
keyword = normalize_unicode(keyword)
|
||||||
if keyword in self._dbkeywords_keyword:
|
if keyword in self._dbkeywords_keyword:
|
||||||
keyword_set.update(self._dbkeywords_keyword[keyword])
|
keyword_set.update(self._dbkeywords_keyword[keyword])
|
||||||
photos_sets.append(keyword_set)
|
photos_sets.append(keyword_set)
|
||||||
@@ -2871,6 +2930,7 @@ class PhotosDB:
|
|||||||
if persons:
|
if persons:
|
||||||
person_set = set()
|
person_set = set()
|
||||||
for person in persons:
|
for person in persons:
|
||||||
|
person = normalize_unicode(person)
|
||||||
if person in self._dbpersons_fullname:
|
if person in self._dbpersons_fullname:
|
||||||
for pk in self._dbpersons_fullname[person]:
|
for pk in self._dbpersons_fullname[person]:
|
||||||
try:
|
try:
|
||||||
@@ -2902,6 +2962,7 @@ class PhotosDB:
|
|||||||
if self._dbphotos[p]["burst"] and not (
|
if self._dbphotos[p]["burst"] and not (
|
||||||
self._dbphotos[p]["burstPickType"] & BURST_SELECTED
|
self._dbphotos[p]["burstPickType"] & BURST_SELECTED
|
||||||
or self._dbphotos[p]["burstPickType"] & BURST_KEY
|
or self._dbphotos[p]["burstPickType"] & BURST_KEY
|
||||||
|
or self._dbphotos[p]["burstPickType"] == BURST_PICK_TYPE_NONE
|
||||||
):
|
):
|
||||||
# not a key/selected burst photo, don't include in returned results
|
# not a key/selected burst photo, don't include in returned results
|
||||||
continue
|
continue
|
||||||
@@ -2912,8 +2973,6 @@ class PhotosDB:
|
|||||||
):
|
):
|
||||||
info = PhotoInfo(db=self, uuid=p, info=self._dbphotos[p])
|
info = PhotoInfo(db=self, uuid=p, info=self._dbphotos[p])
|
||||||
photoinfo.append(info)
|
photoinfo.append(info)
|
||||||
if _debug:
|
|
||||||
logging.debug(f"photoinfo: {pformat(photoinfo)}")
|
|
||||||
|
|
||||||
return photoinfo
|
return photoinfo
|
||||||
|
|
||||||
@@ -3250,23 +3309,35 @@ class PhotosDB:
|
|||||||
# case-insensitive
|
# case-insensitive
|
||||||
for n in name:
|
for n in name:
|
||||||
n = n.lower()
|
n = n.lower()
|
||||||
photo_list.extend(
|
if self._db_version >= _PHOTOS_5_VERSION:
|
||||||
[
|
# search only original_filename (#594)
|
||||||
p
|
photo_list.extend(
|
||||||
for p in photos
|
[p for p in photos if n in p.original_filename.lower()]
|
||||||
if n in p.filename.lower()
|
)
|
||||||
or n in p.original_filename.lower()
|
else:
|
||||||
]
|
photo_list.extend(
|
||||||
)
|
[
|
||||||
|
p
|
||||||
|
for p in photos
|
||||||
|
if n in p.filename.lower()
|
||||||
|
or n in p.original_filename.lower()
|
||||||
|
]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
for n in name:
|
for n in name:
|
||||||
photo_list.extend(
|
if self._db_version >= _PHOTOS_5_VERSION:
|
||||||
[
|
# search only original_filename (#594)
|
||||||
p
|
photo_list.extend(
|
||||||
for p in photos
|
[p for p in photos if n in p.original_filename]
|
||||||
if n in p.filename or n in p.original_filename
|
)
|
||||||
]
|
else:
|
||||||
)
|
photo_list.extend(
|
||||||
|
[
|
||||||
|
p
|
||||||
|
for p in photos
|
||||||
|
if n in p.filename or n in p.original_filename
|
||||||
|
]
|
||||||
|
)
|
||||||
photos = photo_list
|
photos = photo_list
|
||||||
|
|
||||||
if options.min_size:
|
if options.min_size:
|
||||||
@@ -3286,9 +3357,9 @@ class PhotosDB:
|
|||||||
if options.regex:
|
if options.regex:
|
||||||
flags = re.IGNORECASE if options.ignore_case else 0
|
flags = re.IGNORECASE if options.ignore_case else 0
|
||||||
render_options = RenderOptions(none_str="")
|
render_options = RenderOptions(none_str="")
|
||||||
|
photo_list = []
|
||||||
for regex, template in options.regex:
|
for regex, template in options.regex:
|
||||||
regex = re.compile(regex, flags)
|
regex = re.compile(regex, flags)
|
||||||
photo_list = []
|
|
||||||
for p in photos:
|
for p in photos:
|
||||||
rendered, _ = p.render_template(template, render_options)
|
rendered, _ = p.render_template(template, render_options)
|
||||||
for value in rendered:
|
for value in rendered:
|
||||||
@@ -3348,12 +3419,45 @@ class PhotosDB:
|
|||||||
# selection only works if photos selected in main media browser
|
# selection only works if photos selected in main media browser
|
||||||
photos = []
|
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:
|
if options.function:
|
||||||
for function in options.function:
|
for function in options.function:
|
||||||
photos = function[0](photos)
|
photos = function[0](photos)
|
||||||
|
|
||||||
return 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):
|
def _duplicate_signature(self, uuid):
|
||||||
"""Compute a signature for finding possible duplicates"""
|
"""Compute a signature for finding possible duplicates"""
|
||||||
return (
|
return (
|
||||||
@@ -3381,6 +3485,10 @@ class PhotosDB:
|
|||||||
"""
|
"""
|
||||||
return len(self._dbphotos)
|
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):
|
def _get_photos_by_attribute(photos, attribute, values, ignore_case):
|
||||||
"""Search for photos based on values being in PhotoInfo.attribute
|
"""Search for photos based on values being in PhotoInfo.attribute
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
""" utility functions used by PhotosDB """
|
""" utility functions used by PhotosDB """
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import pathlib
|
||||||
import plistlib
|
import plistlib
|
||||||
|
|
||||||
from .._constants import (
|
from .._constants import (
|
||||||
|
_PHOTOS_2_VERSION,
|
||||||
|
_PHOTOS_3_VERSION,
|
||||||
|
_PHOTOS_4_VERSION,
|
||||||
_PHOTOS_5_MODEL_VERSION,
|
_PHOTOS_5_MODEL_VERSION,
|
||||||
|
_PHOTOS_5_VERSION,
|
||||||
_PHOTOS_6_MODEL_VERSION,
|
_PHOTOS_6_MODEL_VERSION,
|
||||||
_PHOTOS_7_MODEL_VERSION,
|
_PHOTOS_7_MODEL_VERSION,
|
||||||
_TESTED_DB_VERSIONS,
|
_TESTED_DB_VERSIONS,
|
||||||
)
|
)
|
||||||
from ..utils import _open_sql_file
|
from ..utils import _open_sql_file
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"get_db_version",
|
||||||
|
"get_model_version",
|
||||||
|
"get_db_model_version",
|
||||||
|
"UnknownLibraryVersion",
|
||||||
|
"get_photos_library_version",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_db_version(db_file):
|
def get_db_version(db_file):
|
||||||
""" Gets the Photos DB version from LiGlobals table
|
"""Gets the Photos DB version from LiGlobals table
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db_file: path to photos.db database file containing LiGlobals table
|
db_file: path to photos.db database file containing LiGlobals table
|
||||||
@@ -40,7 +53,7 @@ def get_db_version(db_file):
|
|||||||
|
|
||||||
|
|
||||||
def get_model_version(db_file):
|
def get_model_version(db_file):
|
||||||
""" Returns the database model version from Z_METADATA
|
"""Returns the database model version from Z_METADATA
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db_file: path to Photos.sqlite database file containing Z_METADATA table
|
db_file: path to Photos.sqlite database file containing Z_METADATA table
|
||||||
@@ -63,7 +76,7 @@ def get_model_version(db_file):
|
|||||||
|
|
||||||
|
|
||||||
def get_db_model_version(db_file):
|
def get_db_model_version(db_file):
|
||||||
""" Returns Photos version based on model version found in db_file
|
"""Returns Photos version based on model version found in db_file
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db_file: path to Photos.sqlite file
|
db_file: path to Photos.sqlite file
|
||||||
@@ -83,3 +96,31 @@ def get_db_model_version(db_file):
|
|||||||
logging.warning(f"Unknown model version: {model_ver}")
|
logging.warning(f"Unknown model version: {model_ver}")
|
||||||
# cross our fingers and try latest version
|
# cross our fingers and try latest version
|
||||||
return 7
|
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
|
||||||
|
|
||||||
|
# assume it's a Photos 5+ library, get the model version to determine which version
|
||||||
|
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}")
|
||||||
|
|||||||
@@ -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.
|
In its simplest form, a template statement has the form: `"{template_field}"`, for example `"{title}"` which would resolve to the title of the photo.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" Custom template system for osxphotos, implements osxphotos template language (OTL) """
|
""" Custom template system for osxphotos, implements metadata template language (MTL) """
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
@@ -17,17 +17,25 @@ from ._constants import _UNKNOWN_PERSON, TEXT_DETECTION_CONFIDENCE_THRESHOLD
|
|||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from .datetime_formatter import DateTimeFormatter
|
from .datetime_formatter import DateTimeFormatter
|
||||||
from .exiftool import ExifToolCaching
|
from .exiftool import ExifToolCaching
|
||||||
from .export_db import ExportDB_ABC, ExportDBInMemory
|
|
||||||
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
|
||||||
from .text_detection import detect_text
|
from .text_detection import detect_text
|
||||||
from .utils import expand_and_validate_filepath, load_function
|
from .utils import expand_and_validate_filepath, load_function
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"RenderOptions",
|
||||||
|
"PhotoTemplateParser",
|
||||||
|
"PhotoTemplate",
|
||||||
|
"parse_default_kv",
|
||||||
|
"get_template_help",
|
||||||
|
"format_str_value",
|
||||||
|
]
|
||||||
|
|
||||||
# TODO: a lot of values are passed from function to function like path_sep--make these all class properties
|
# TODO: a lot of values are passed from function to function like path_sep--make these all class properties
|
||||||
|
|
||||||
# ensure locale set to user's locale
|
# ensure locale set to user's locale
|
||||||
locale.setlocale(locale.LC_ALL, "")
|
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 """
|
"""TextX metamodel for osxphotos template language """
|
||||||
|
|
||||||
@@ -181,6 +189,9 @@ TEMPLATE_SUBSTITUTIONS_PATHLIB = {
|
|||||||
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
||||||
"{album}": "Album(s) photo is contained in",
|
"{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",
|
"{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",
|
"{keyword}": "Keyword(s) assigned to photo",
|
||||||
"{person}": "Person(s) / face(s) in a photo",
|
"{person}": "Person(s) / face(s) in a photo",
|
||||||
"{label}": "Image categorization label associated with a photo (Photos 5+ only). "
|
"{label}": "Image categorization label associated with a photo (Photos 5+ only). "
|
||||||
@@ -206,8 +217,10 @@ TEMPLATE_SUBSTITUTIONS_MULTI_VALUED = {
|
|||||||
+ "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. "
|
+ "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}'; "
|
+ "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}. "
|
+ 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.",
|
+ "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.",
|
"{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. "
|
"{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. "
|
+ "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. "
|
+ "The function will be passed the PhotoInfo object for the photo. "
|
||||||
@@ -286,7 +299,6 @@ class RenderOptions:
|
|||||||
dest_path: set to the destination path of the photo (for use by {function} template), only valid with --filename
|
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
|
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
|
quote: quote path templates for execution in the shell
|
||||||
exportdb: ExportDB object
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
none_str: str = "_"
|
none_str: str = "_"
|
||||||
@@ -301,7 +313,6 @@ class RenderOptions:
|
|||||||
dest_path: Optional[str] = None
|
dest_path: Optional[str] = None
|
||||||
filepath: Optional[str] = None
|
filepath: Optional[str] = None
|
||||||
quote: bool = False
|
quote: bool = False
|
||||||
exportdb: Optional[ExportDB_ABC] = None
|
|
||||||
|
|
||||||
|
|
||||||
class PhotoTemplateParser:
|
class PhotoTemplateParser:
|
||||||
@@ -322,7 +333,7 @@ class PhotoTemplateParser:
|
|||||||
if hasattr(self, "metamodel"):
|
if hasattr(self, "metamodel"):
|
||||||
return
|
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):
|
def parse(self, template_statement):
|
||||||
"""Parse a template_statement string"""
|
"""Parse a template_statement string"""
|
||||||
@@ -370,7 +381,6 @@ class PhotoTemplate:
|
|||||||
self.filepath = options.filepath
|
self.filepath = options.filepath
|
||||||
self.quote = options.quote
|
self.quote = options.quote
|
||||||
self.dest_path = options.dest_path
|
self.dest_path = options.dest_path
|
||||||
self.exportdb = options.exportdb or ExportDBInMemory(None)
|
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
@@ -404,7 +414,6 @@ class PhotoTemplate:
|
|||||||
self.filepath = options.filepath
|
self.filepath = options.filepath
|
||||||
self.quote = options.quote
|
self.quote = options.quote
|
||||||
self.dest_path = options.dest_path
|
self.dest_path = options.dest_path
|
||||||
self.exportdb = options.exportdb or self.exportdb
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model = self.parser.parse(template)
|
model = self.parser.parse(template)
|
||||||
@@ -573,7 +582,7 @@ class PhotoTemplate:
|
|||||||
|
|
||||||
if self.expand_inplace or delim is not None:
|
if self.expand_inplace or delim is not None:
|
||||||
sep = delim if delim is not None else self.inplace_sep
|
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:
|
for filter_ in filters:
|
||||||
vals = self.get_template_value_filter(filter_, vals)
|
vals = self.get_template_value_filter(filter_, vals)
|
||||||
@@ -1002,6 +1011,9 @@ class PhotoTemplate:
|
|||||||
elif self.dirname:
|
elif self.dirname:
|
||||||
value = sanitize_dirname(value)
|
value = sanitize_dirname(value)
|
||||||
|
|
||||||
|
# ensure no empty strings in value (see #512)
|
||||||
|
value = None if value == "" else value
|
||||||
|
|
||||||
return [value]
|
return [value]
|
||||||
|
|
||||||
def get_template_value_pathlib(self, field):
|
def get_template_value_pathlib(self, field):
|
||||||
@@ -1111,6 +1123,11 @@ class PhotoTemplate:
|
|||||||
values = []
|
values = []
|
||||||
if field == "album":
|
if field == "album":
|
||||||
values = self.photo.burst_albums if self.photo.burst else self.photo.albums
|
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":
|
elif field == "keyword":
|
||||||
values = self.photo.keywords
|
values = self.photo.keywords
|
||||||
elif field == "person":
|
elif field == "person":
|
||||||
@@ -1121,13 +1138,15 @@ class PhotoTemplate:
|
|||||||
values = self.photo.labels
|
values = self.photo.labels
|
||||||
elif field == "label_normalized":
|
elif field == "label_normalized":
|
||||||
values = self.photo.labels_normalized
|
values = self.photo.labels_normalized
|
||||||
elif field == "folder_album":
|
elif field in ["folder_album", "folder_album_project"]:
|
||||||
values = []
|
values = []
|
||||||
# photos must be in an album to be in a folder
|
# photos must be in an album to be in a folder
|
||||||
if self.photo.burst:
|
if self.photo.burst:
|
||||||
album_info = self.photo.burst_album_info
|
album_info = self.photo.burst_album_info
|
||||||
else:
|
else:
|
||||||
album_info = self.photo.album_info
|
album_info = self.photo.album_info
|
||||||
|
if field == "folder_album_project":
|
||||||
|
album_info += self.photo.project_info
|
||||||
for album in album_info:
|
for album in album_info:
|
||||||
if album.folder_names:
|
if album.folder_names:
|
||||||
# album in folder
|
# album in folder
|
||||||
@@ -1161,6 +1180,8 @@ class PhotoTemplate:
|
|||||||
)
|
)
|
||||||
elif field == "shell_quote":
|
elif field == "shell_quote":
|
||||||
values = [shlex.quote(v) for v in default if v]
|
values = [shlex.quote(v) for v in default if v]
|
||||||
|
elif field == "strip":
|
||||||
|
values = [v.strip() for v in default]
|
||||||
elif field.startswith("photo"):
|
elif field.startswith("photo"):
|
||||||
# provide access to PhotoInfo object
|
# provide access to PhotoInfo object
|
||||||
properties = field.split(".")
|
properties = field.split(".")
|
||||||
@@ -1186,16 +1207,16 @@ class PhotoTemplate:
|
|||||||
elif isinstance(obj, (str, int, float)):
|
elif isinstance(obj, (str, int, float)):
|
||||||
values = [str(obj)]
|
values = [str(obj)]
|
||||||
else:
|
else:
|
||||||
values = [val for val in obj]
|
values = list(obj)
|
||||||
elif field == "detected_text":
|
elif field == "detected_text":
|
||||||
values = _get_detected_text(self.photo, self.exportdb, confidence=subfield)
|
values = _get_detected_text(self.photo, confidence=subfield)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unhandled template value: {field}")
|
raise ValueError(f"Unhandled template value: {field}")
|
||||||
|
|
||||||
# sanitize directory names if needed, folder_album handled differently above
|
# sanitize directory names if needed, folder_album handled differently above
|
||||||
if self.filename:
|
if self.filename:
|
||||||
values = [sanitize_pathpart(value) for value in values]
|
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
|
# skip folder_album because it would have been handled above
|
||||||
values = [sanitize_dirname(value) for value in values]
|
values = [sanitize_dirname(value) for value in values]
|
||||||
|
|
||||||
@@ -1431,7 +1452,7 @@ def _get_album_by_path(photo, folder_album_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_detected_text(photo, exportdb, confidence=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
|
def _get_detected_text(photo, confidence=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
|
||||||
"""Returns the detected text for a photo
|
"""Returns the detected text for a photo
|
||||||
{detected_text} uses this instead of PhotoInfo.detected_text() to cache the text for all confidence values
|
{detected_text} uses this instead of PhotoInfo.detected_text() to cache the text for all confidence values
|
||||||
"""
|
"""
|
||||||
@@ -1444,25 +1465,7 @@ def _get_detected_text(photo, exportdb, confidence=TEXT_DETECTION_CONFIDENCE_THR
|
|||||||
else TEXT_DETECTION_CONFIDENCE_THRESHOLD
|
else TEXT_DETECTION_CONFIDENCE_THRESHOLD
|
||||||
)
|
)
|
||||||
|
|
||||||
detected_text = exportdb.get_detected_text_for_uuid(photo.uuid)
|
# _detected_text caches the text detection results in an extended attribute
|
||||||
if detected_text is not None:
|
# so the first time this gets called is slow but repeated accesses are fast
|
||||||
detected_text = json.loads(detected_text)
|
detected_text = photo._detected_text()
|
||||||
else:
|
|
||||||
path = (
|
|
||||||
photo.path_edited
|
|
||||||
if photo.hasadjustments and photo.path_edited
|
|
||||||
else photo.path
|
|
||||||
)
|
|
||||||
path = path or photo.path_derivatives[0] if photo.path_derivatives else None
|
|
||||||
if not path:
|
|
||||||
detected_text = []
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
detected_text = detect_text(path)
|
|
||||||
except Exception as e:
|
|
||||||
logging.warning(
|
|
||||||
f"Error detecting text in image {photo.uuid} at {path}: {e}"
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
exportdb.set_detected_text_for_uuid(photo.uuid, json.dumps(detected_text))
|
|
||||||
return [text for text, conf in detected_text if conf >= confidence]
|
return [text for text, conf in detected_text if conf >= confidence]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// OSXPhotos Template Language (OTL)
|
// OSXPhotos Metadata Template Language (MTL)
|
||||||
// a TemplateString has format:
|
// a TemplateString has format:
|
||||||
// pre{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}post
|
// pre{delim+template_field:subfield|filter(path_sep)[find,replace] conditional?bool_value,default}post
|
||||||
// a TemplateStatement may contain zero or more TemplateStrings
|
// a TemplateStatement may contain zero or more TemplateStrings
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ from bpylist import archiver
|
|||||||
from ._constants import UNICODE_FORMAT
|
from ._constants import UNICODE_FORMAT
|
||||||
from .utils import normalize_unicode
|
from .utils import normalize_unicode
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"PLRevGeoLocationInfo",
|
||||||
|
"PLRevGeoMapItem",
|
||||||
|
"PLRevGeoMapItemAdditionalPlaceInfo",
|
||||||
|
"CNPostalAddress",
|
||||||
|
"PlaceInfo",
|
||||||
|
"PlaceInfo4",
|
||||||
|
"PlaceInfo5",
|
||||||
|
]
|
||||||
|
|
||||||
# postal address information, returned by PlaceInfo.address
|
# postal address information, returned by PlaceInfo.address
|
||||||
PostalAddress = namedtuple(
|
PostalAddress = namedtuple(
|
||||||
"PostalAddress",
|
"PostalAddress",
|
||||||
@@ -65,7 +75,7 @@ PlaceNames = namedtuple(
|
|||||||
# in ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA
|
# in ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA
|
||||||
# These classes are used by bpylist.archiver to unarchive the serialized objects
|
# These classes are used by bpylist.archiver to unarchive the serialized objects
|
||||||
class PLRevGeoLocationInfo:
|
class PLRevGeoLocationInfo:
|
||||||
""" The top level reverse geolocation object """
|
"""The top level reverse geolocation object"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -147,7 +157,7 @@ class PLRevGeoLocationInfo:
|
|||||||
|
|
||||||
|
|
||||||
class PLRevGeoMapItem:
|
class PLRevGeoMapItem:
|
||||||
""" Stores the list of place names, organized by area """
|
"""Stores the list of place names, organized by area"""
|
||||||
|
|
||||||
def __init__(self, sortedPlaceInfos, finalPlaceInfos):
|
def __init__(self, sortedPlaceInfos, finalPlaceInfos):
|
||||||
self.sortedPlaceInfos = sortedPlaceInfos
|
self.sortedPlaceInfos = sortedPlaceInfos
|
||||||
@@ -182,7 +192,7 @@ class PLRevGeoMapItem:
|
|||||||
|
|
||||||
|
|
||||||
class PLRevGeoMapItemAdditionalPlaceInfo:
|
class PLRevGeoMapItemAdditionalPlaceInfo:
|
||||||
""" Additional info about individual places """
|
"""Additional info about individual places"""
|
||||||
|
|
||||||
def __init__(self, area, name, placeType, dominantOrderType):
|
def __init__(self, area, name, placeType, dominantOrderType):
|
||||||
self.area = area
|
self.area = area
|
||||||
@@ -221,7 +231,7 @@ class PLRevGeoMapItemAdditionalPlaceInfo:
|
|||||||
|
|
||||||
|
|
||||||
class CNPostalAddress:
|
class CNPostalAddress:
|
||||||
""" postal address for the reverse geolocation info """
|
"""postal address for the reverse geolocation info"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -354,17 +364,17 @@ class PlaceInfo(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class PlaceInfo4(PlaceInfo):
|
class PlaceInfo4(PlaceInfo):
|
||||||
""" Reverse geolocation place info for a photo (Photos <= 4) """
|
"""Reverse geolocation place info for a photo (Photos <= 4)"""
|
||||||
|
|
||||||
def __init__(self, place_names, country_code):
|
def __init__(self, place_names, country_code):
|
||||||
""" place_names: list of place name tuples in ascending order by area
|
"""place_names: list of place name tuples in ascending order by area
|
||||||
tuple fields are: modelID, place name, place type, area, e.g.
|
tuple fields are: modelID, place name, place type, area, e.g.
|
||||||
[(5, "St James's Park", 45, 0),
|
[(5, "St James's Park", 45, 0),
|
||||||
(4, 'Westminster', 16, 22097376),
|
(4, 'Westminster', 16, 22097376),
|
||||||
(3, 'London', 4, 1596146816),
|
(3, 'London', 4, 1596146816),
|
||||||
(2, 'England', 2, 180406091776),
|
(2, 'England', 2, 180406091776),
|
||||||
(1, 'United Kingdom', 1, 414681432064)]
|
(1, 'United Kingdom', 1, 414681432064)]
|
||||||
country_code: two letter country code for the country
|
country_code: two letter country code for the country
|
||||||
"""
|
"""
|
||||||
self._place_names = place_names
|
self._place_names = place_names
|
||||||
self._country_code = country_code
|
self._country_code = country_code
|
||||||
@@ -404,7 +414,7 @@ class PlaceInfo4(PlaceInfo):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _process_place_info(self):
|
def _process_place_info(self):
|
||||||
""" Process place_names to set self._name and self._names """
|
"""Process place_names to set self._name and self._names"""
|
||||||
places = self._place_names
|
places = self._place_names
|
||||||
|
|
||||||
# build a dictionary where key is placetype
|
# build a dictionary where key is placetype
|
||||||
@@ -500,38 +510,38 @@ class PlaceInfo4(PlaceInfo):
|
|||||||
|
|
||||||
|
|
||||||
class PlaceInfo5(PlaceInfo):
|
class PlaceInfo5(PlaceInfo):
|
||||||
""" Reverse geolocation place info for a photo (Photos >= 5) """
|
"""Reverse geolocation place info for a photo (Photos >= 5)"""
|
||||||
|
|
||||||
def __init__(self, revgeoloc_bplist):
|
def __init__(self, revgeoloc_bplist):
|
||||||
""" revgeoloc_bplist: a binary plist blob containing
|
"""revgeoloc_bplist: a binary plist blob containing
|
||||||
a serialized PLRevGeoLocationInfo object """
|
a serialized PLRevGeoLocationInfo object"""
|
||||||
self._bplist = revgeoloc_bplist
|
self._bplist = revgeoloc_bplist
|
||||||
self._plrevgeoloc = archiver.unarchive(revgeoloc_bplist)
|
self._plrevgeoloc = archiver.unarchive(revgeoloc_bplist)
|
||||||
self._process_place_info()
|
self._process_place_info()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def address_str(self):
|
def address_str(self):
|
||||||
""" returns the postal address as a string """
|
"""returns the postal address as a string"""
|
||||||
return self._plrevgeoloc.addressString
|
return self._plrevgeoloc.addressString
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def country_code(self):
|
def country_code(self):
|
||||||
""" returns the country code """
|
"""returns the country code"""
|
||||||
return self._plrevgeoloc.countryCode
|
return self._plrevgeoloc.countryCode
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ishome(self):
|
def ishome(self):
|
||||||
""" returns True if place is user's home address """
|
"""returns True if place is user's home address"""
|
||||||
return self._plrevgeoloc.isHome
|
return self._plrevgeoloc.isHome
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
""" returns local place name """
|
"""returns local place name"""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def names(self):
|
def names(self):
|
||||||
""" returns PlaceNames tuple with detailed reverse geolocation place names """
|
"""returns PlaceNames tuple with detailed reverse geolocation place names"""
|
||||||
return self._names
|
return self._names
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -556,7 +566,7 @@ class PlaceInfo5(PlaceInfo):
|
|||||||
return postal_address
|
return postal_address
|
||||||
|
|
||||||
def _process_place_info(self):
|
def _process_place_info(self):
|
||||||
""" Process sortedPlaceInfos to set self._name and self._names """
|
"""Process sortedPlaceInfos to set self._name and self._names"""
|
||||||
places = self._plrevgeoloc.mapItem.sortedPlaceInfos
|
places = self._plrevgeoloc.mapItem.sortedPlaceInfos
|
||||||
|
|
||||||
# build a dictionary where key is placetype
|
# build a dictionary where key is placetype
|
||||||
|
|||||||
132
osxphotos/pyrepl.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
__all__ = ["PyReplQuitter", "embed_repl"]
|
||||||
|
""" 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()
|
||||||
5
osxphotos/queries/README.md
Normal 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)
|
||||||
4
osxphotos/queries/cloud_album_owner.sql.mako
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- Get owner name for shared iCloud album
|
||||||
|
SELECT ZGENERICALBUM.ZCLOUDOWNERFULLNAME AS OWNER_FULLNAME
|
||||||
|
FROM ZGENERICALBUM
|
||||||
|
WHERE ZGENERICALBUM.ZUUID = '${uuid}'
|
||||||
23
osxphotos/queries/shared_owner.sql.mako
Normal 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)"
|
||||||
6
osxphotos/queries/title.sql.mako
Normal 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}"
|
||||||
38
osxphotos/query_builder.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""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
|
||||||
|
|
||||||
|
__all__ = ["get_query"]
|
||||||
|
|
||||||
|
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
|
||||||
@@ -6,6 +6,8 @@ from typing import Iterable, List, Optional, Tuple
|
|||||||
|
|
||||||
import bitmath
|
import bitmath
|
||||||
|
|
||||||
|
__all__ = ["QueryOptions"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class QueryOptions:
|
class QueryOptions:
|
||||||
@@ -84,6 +86,7 @@ class QueryOptions:
|
|||||||
no_location: Optional[bool] = None
|
no_location: Optional[bool] = None
|
||||||
function: Optional[List[Tuple[callable, str]]] = None
|
function: Optional[List[Tuple[callable, str]]] = None
|
||||||
selected: Optional[bool] = None
|
selected: Optional[bool] = None
|
||||||
|
exif: Optional[Iterable[Tuple[str, str]]] = None
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|||||||
40
osxphotos/scoreinfo.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
""" ScoreInfo class to expose computed score info from the library """
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from ._constants import _PHOTOS_4_VERSION
|
||||||
|
|
||||||
|
__all__ = ["ScoreInfo"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ScoreInfo:
|
||||||
|
"""Computed photo score info associated with a photo from the Photos library"""
|
||||||
|
|
||||||
|
overall: float
|
||||||
|
curation: float
|
||||||
|
promotion: float
|
||||||
|
highlight_visibility: float
|
||||||
|
behavioral: float
|
||||||
|
failure: float
|
||||||
|
harmonious_color: float
|
||||||
|
immersiveness: float
|
||||||
|
interaction: float
|
||||||
|
interesting_subject: float
|
||||||
|
intrusive_object_presence: float
|
||||||
|
lively_color: float
|
||||||
|
low_light: float
|
||||||
|
noise: float
|
||||||
|
pleasant_camera_tilt: float
|
||||||
|
pleasant_composition: float
|
||||||
|
pleasant_lighting: float
|
||||||
|
pleasant_pattern: float
|
||||||
|
pleasant_perspective: float
|
||||||
|
pleasant_post_processing: float
|
||||||
|
pleasant_reflection: float
|
||||||
|
pleasant_symmetry: float
|
||||||
|
sharply_focused_subject: float
|
||||||
|
tastefully_blurred: float
|
||||||
|
well_chosen_subject: float
|
||||||
|
well_framed_subject: float
|
||||||
|
well_timed_shot: float
|
||||||
@@ -1,98 +1,41 @@
|
|||||||
""" Methods and class for PhotoInfo exposing SearchInfo data such as labels
|
""" class for PhotoInfo exposing SearchInfo data such as labels
|
||||||
Adds the following properties to PhotoInfo (valid only for Photos 5):
|
|
||||||
search_info: returns a SearchInfo object
|
|
||||||
search_info_normalized: returns a SearchInfo object with properties that produce normalized results
|
|
||||||
labels: returns list of labels
|
|
||||||
labels_normalized: returns list of normalized labels
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .._constants import (
|
from ._constants import (
|
||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
|
SEARCH_CATEGORY_ACTIVITY,
|
||||||
|
SEARCH_CATEGORY_ALL_LOCALITY,
|
||||||
|
SEARCH_CATEGORY_BODY_OF_WATER,
|
||||||
SEARCH_CATEGORY_CITY,
|
SEARCH_CATEGORY_CITY,
|
||||||
|
SEARCH_CATEGORY_COUNTRY,
|
||||||
|
SEARCH_CATEGORY_HOLIDAY,
|
||||||
SEARCH_CATEGORY_LABEL,
|
SEARCH_CATEGORY_LABEL,
|
||||||
|
SEARCH_CATEGORY_MEDIA_TYPES,
|
||||||
|
SEARCH_CATEGORY_MONTH,
|
||||||
SEARCH_CATEGORY_NEIGHBORHOOD,
|
SEARCH_CATEGORY_NEIGHBORHOOD,
|
||||||
SEARCH_CATEGORY_PLACE_NAME,
|
SEARCH_CATEGORY_PLACE_NAME,
|
||||||
SEARCH_CATEGORY_STREET,
|
SEARCH_CATEGORY_SEASON,
|
||||||
SEARCH_CATEGORY_ALL_LOCALITY,
|
|
||||||
SEARCH_CATEGORY_COUNTRY,
|
|
||||||
SEARCH_CATEGORY_STATE,
|
SEARCH_CATEGORY_STATE,
|
||||||
SEARCH_CATEGORY_STATE_ABBREVIATION,
|
SEARCH_CATEGORY_STATE_ABBREVIATION,
|
||||||
SEARCH_CATEGORY_BODY_OF_WATER,
|
SEARCH_CATEGORY_STREET,
|
||||||
SEARCH_CATEGORY_MONTH,
|
|
||||||
SEARCH_CATEGORY_YEAR,
|
|
||||||
SEARCH_CATEGORY_HOLIDAY,
|
|
||||||
SEARCH_CATEGORY_ACTIVITY,
|
|
||||||
SEARCH_CATEGORY_SEASON,
|
|
||||||
SEARCH_CATEGORY_VENUE,
|
SEARCH_CATEGORY_VENUE,
|
||||||
SEARCH_CATEGORY_VENUE_TYPE,
|
SEARCH_CATEGORY_VENUE_TYPE,
|
||||||
SEARCH_CATEGORY_MEDIA_TYPES,
|
SEARCH_CATEGORY_YEAR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__all__ = ["SearchInfo"]
|
||||||
@property
|
|
||||||
def search_info(self):
|
|
||||||
""" returns SearchInfo object for photo
|
|
||||||
only valid on Photos 5, on older libraries, returns None
|
|
||||||
"""
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# memoize SearchInfo object
|
|
||||||
try:
|
|
||||||
return self._search_info
|
|
||||||
except AttributeError:
|
|
||||||
self._search_info = SearchInfo(self)
|
|
||||||
return self._search_info
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def search_info_normalized(self):
|
|
||||||
""" returns SearchInfo object for photo that produces normalized results
|
|
||||||
only valid on Photos 5, on older libraries, returns None
|
|
||||||
"""
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# memoize SearchInfo object
|
|
||||||
try:
|
|
||||||
return self._search_info_normalized
|
|
||||||
except AttributeError:
|
|
||||||
self._search_info_normalized = SearchInfo(self, normalized=True)
|
|
||||||
return self._search_info_normalized
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def labels(self):
|
|
||||||
""" returns list of labels applied to photo by Photos image categorization
|
|
||||||
only valid on Photos 5, on older libraries returns empty list
|
|
||||||
"""
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
return []
|
|
||||||
|
|
||||||
return self.search_info.labels
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def labels_normalized(self):
|
|
||||||
""" returns normalized list of labels applied to photo by Photos image categorization
|
|
||||||
only valid on Photos 5, on older libraries returns empty list
|
|
||||||
"""
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
return []
|
|
||||||
|
|
||||||
return self.search_info_normalized.labels
|
|
||||||
|
|
||||||
|
|
||||||
class SearchInfo:
|
class SearchInfo:
|
||||||
""" Info about search terms such as machine learning labels that Photos knows about a photo """
|
"""Info about search terms such as machine learning labels that Photos knows about a photo"""
|
||||||
|
|
||||||
def __init__(self, photo, normalized=False):
|
def __init__(self, photo, normalized=False):
|
||||||
""" photo: PhotoInfo object
|
"""photo: PhotoInfo object
|
||||||
normalized: if True, all properties return normalized (lower case) results """
|
normalized: if True, all properties return normalized (lower case) results"""
|
||||||
|
|
||||||
if photo._db._db_version <= _PHOTOS_4_VERSION:
|
if photo._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f"search info not implemented for this database version"
|
"search info not implemented for this database version"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._photo = photo
|
self._photo = photo
|
||||||
@@ -107,27 +50,27 @@ class SearchInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def labels(self):
|
def labels(self):
|
||||||
""" return list of labels associated with Photo """
|
"""return list of labels associated with Photo"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_LABEL)
|
return self._get_text_for_category(SEARCH_CATEGORY_LABEL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def place_names(self):
|
def place_names(self):
|
||||||
""" returns list of place names """
|
"""returns list of place names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_PLACE_NAME)
|
return self._get_text_for_category(SEARCH_CATEGORY_PLACE_NAME)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def streets(self):
|
def streets(self):
|
||||||
""" returns list of street names """
|
"""returns list of street names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_STREET)
|
return self._get_text_for_category(SEARCH_CATEGORY_STREET)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def neighborhoods(self):
|
def neighborhoods(self):
|
||||||
""" returns list of neighborhoods """
|
"""returns list of neighborhoods"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_NEIGHBORHOOD)
|
return self._get_text_for_category(SEARCH_CATEGORY_NEIGHBORHOOD)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locality_names(self):
|
def locality_names(self):
|
||||||
""" returns list of other locality names """
|
"""returns list of other locality names"""
|
||||||
locality = []
|
locality = []
|
||||||
for category in SEARCH_CATEGORY_ALL_LOCALITY:
|
for category in SEARCH_CATEGORY_ALL_LOCALITY:
|
||||||
locality += self._get_text_for_category(category)
|
locality += self._get_text_for_category(category)
|
||||||
@@ -135,74 +78,74 @@ class SearchInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def city(self):
|
def city(self):
|
||||||
""" returns city/town """
|
"""returns city/town"""
|
||||||
city = self._get_text_for_category(SEARCH_CATEGORY_CITY)
|
city = self._get_text_for_category(SEARCH_CATEGORY_CITY)
|
||||||
return city[0] if city else ""
|
return city[0] if city else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
""" returns state name """
|
"""returns state name"""
|
||||||
state = self._get_text_for_category(SEARCH_CATEGORY_STATE)
|
state = self._get_text_for_category(SEARCH_CATEGORY_STATE)
|
||||||
return state[0] if state else ""
|
return state[0] if state else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_abbreviation(self):
|
def state_abbreviation(self):
|
||||||
""" returns state abbreviation """
|
"""returns state abbreviation"""
|
||||||
abbrev = self._get_text_for_category(SEARCH_CATEGORY_STATE_ABBREVIATION)
|
abbrev = self._get_text_for_category(SEARCH_CATEGORY_STATE_ABBREVIATION)
|
||||||
return abbrev[0] if abbrev else ""
|
return abbrev[0] if abbrev else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def country(self):
|
def country(self):
|
||||||
""" returns country name """
|
"""returns country name"""
|
||||||
country = self._get_text_for_category(SEARCH_CATEGORY_COUNTRY)
|
country = self._get_text_for_category(SEARCH_CATEGORY_COUNTRY)
|
||||||
return country[0] if country else ""
|
return country[0] if country else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def month(self):
|
def month(self):
|
||||||
""" returns month name """
|
"""returns month name"""
|
||||||
month = self._get_text_for_category(SEARCH_CATEGORY_MONTH)
|
month = self._get_text_for_category(SEARCH_CATEGORY_MONTH)
|
||||||
return month[0] if month else ""
|
return month[0] if month else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def year(self):
|
def year(self):
|
||||||
""" returns year """
|
"""returns year"""
|
||||||
year = self._get_text_for_category(SEARCH_CATEGORY_YEAR)
|
year = self._get_text_for_category(SEARCH_CATEGORY_YEAR)
|
||||||
return year[0] if year else ""
|
return year[0] if year else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bodies_of_water(self):
|
def bodies_of_water(self):
|
||||||
""" returns list of body of water names """
|
"""returns list of body of water names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_BODY_OF_WATER)
|
return self._get_text_for_category(SEARCH_CATEGORY_BODY_OF_WATER)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def holidays(self):
|
def holidays(self):
|
||||||
""" returns list of holiday names """
|
"""returns list of holiday names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_HOLIDAY)
|
return self._get_text_for_category(SEARCH_CATEGORY_HOLIDAY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def activities(self):
|
def activities(self):
|
||||||
""" returns list of activity names """
|
"""returns list of activity names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_ACTIVITY)
|
return self._get_text_for_category(SEARCH_CATEGORY_ACTIVITY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def season(self):
|
def season(self):
|
||||||
""" returns season name """
|
"""returns season name"""
|
||||||
season = self._get_text_for_category(SEARCH_CATEGORY_SEASON)
|
season = self._get_text_for_category(SEARCH_CATEGORY_SEASON)
|
||||||
return season[0] if season else ""
|
return season[0] if season else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def venues(self):
|
def venues(self):
|
||||||
""" returns list of venue names """
|
"""returns list of venue names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_VENUE)
|
return self._get_text_for_category(SEARCH_CATEGORY_VENUE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def venue_types(self):
|
def venue_types(self):
|
||||||
""" returns list of venue types """
|
"""returns list of venue types"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_VENUE_TYPE)
|
return self._get_text_for_category(SEARCH_CATEGORY_VENUE_TYPE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_types(self):
|
def media_types(self):
|
||||||
""" returns list of media types (photo, video, panorama, etc) """
|
"""returns list of media types (photo, video, panorama, etc)"""
|
||||||
types = []
|
types = []
|
||||||
for category in SEARCH_CATEGORY_MEDIA_TYPES:
|
for category in SEARCH_CATEGORY_MEDIA_TYPES:
|
||||||
types += self._get_text_for_category(category)
|
types += self._get_text_for_category(category)
|
||||||
@@ -210,7 +153,7 @@ class SearchInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def all(self):
|
def all(self):
|
||||||
""" return all search info properties in a single list """
|
"""return all search info properties in a single list"""
|
||||||
all = (
|
all = (
|
||||||
self.labels
|
self.labels
|
||||||
+ self.place_names
|
+ self.place_names
|
||||||
@@ -242,7 +185,7 @@ class SearchInfo:
|
|||||||
return all
|
return all
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
""" return dict of search info """
|
"""return dict of search info"""
|
||||||
return {
|
return {
|
||||||
"labels": self.labels,
|
"labels": self.labels,
|
||||||
"place_names": self.place_names,
|
"place_names": self.place_names,
|
||||||
@@ -265,7 +208,7 @@ class SearchInfo:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_text_for_category(self, category):
|
def _get_text_for_category(self, category):
|
||||||
""" return list of text for a specified category ID """
|
"""return list of text for a specified category ID"""
|
||||||
if self._db_searchinfo:
|
if self._db_searchinfo:
|
||||||
content = "normalized_string" if self._normalized else "content_string"
|
content = "normalized_string" if self._normalized else "content_string"
|
||||||
return [
|
return [
|
||||||
57
osxphotos/sqlgrep.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""Search through a sqlite database file for a given string"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
from typing import Generator, List
|
||||||
|
|
||||||
|
__all__ = ["sqlgrep"]
|
||||||
|
|
||||||
|
|
||||||
|
def sqlgrep(
|
||||||
|
filename: str,
|
||||||
|
pattern: str,
|
||||||
|
ignore_case: bool = False,
|
||||||
|
print_filename: bool = True,
|
||||||
|
rich_markup: bool = False,
|
||||||
|
) -> Generator[List[str], None, None]:
|
||||||
|
"""grep through a sqlite database file for a given string
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (str): The filename of the sqlite database file
|
||||||
|
pattern (str): The pattern to search for
|
||||||
|
ignore_case (bool, optional): Ignore case when searching. Defaults to False.
|
||||||
|
print_filename (bool, optional): include the filename of the file with table name. Defaults to True.
|
||||||
|
rich_markup (bool, optional): Add rich markup to mark found text in bold. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Generator which yields list of [table, column, row_id, value]
|
||||||
|
"""
|
||||||
|
flags = re.IGNORECASE if ignore_case else 0
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(f"file:{filename}?mode=ro", uri=True) as conn:
|
||||||
|
regex = re.compile(r"(" + pattern + r")", flags=flags)
|
||||||
|
filename_header = f"{filename}: " if print_filename else ""
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||||
|
for tablerow in cursor.fetchall():
|
||||||
|
table = tablerow[0]
|
||||||
|
cursor.execute("SELECT * FROM {t}".format(t=table))
|
||||||
|
for row_num, row in enumerate(cursor):
|
||||||
|
for field in row.keys():
|
||||||
|
field_value = row[field]
|
||||||
|
if not field_value or type(field_value) == bytes:
|
||||||
|
# don't search binary blobs
|
||||||
|
next
|
||||||
|
field_value = str(field_value)
|
||||||
|
if re.search(pattern, field_value, flags=flags):
|
||||||
|
if rich_markup:
|
||||||
|
field_value = regex.sub(r"[bold]\1[/bold]", field_value)
|
||||||
|
yield [
|
||||||
|
f"{filename_header}{table}",
|
||||||
|
field,
|
||||||
|
str(row_num),
|
||||||
|
field_value,
|
||||||
|
]
|
||||||
|
except sqlite3.DatabaseError as e:
|
||||||
|
raise sqlite3.DatabaseError(f"{filename}: {e}")
|
||||||
@@ -1,19 +1,40 @@
|
|||||||
""" Use Apple's Vision Framework via PyObjC to perform text detection on images """
|
""" 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 objc
|
||||||
import Quartz
|
import Quartz
|
||||||
import Vision
|
|
||||||
from Cocoa import NSURL
|
from Cocoa import NSURL
|
||||||
from Foundation import NSDictionary
|
from Foundation import NSDictionary
|
||||||
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
# needed to capture system-level stderr
|
# needed to capture system-level stderr
|
||||||
from wurlitzer import pipes
|
from wurlitzer import pipes
|
||||||
|
|
||||||
|
from .utils import _get_os_version
|
||||||
|
|
||||||
|
__all__ = ["detect_text", "make_request_handler"]
|
||||||
|
|
||||||
|
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 []
|
||||||
|
|
||||||
def detect_text(img_path: str) -> List:
|
|
||||||
"""process image at img_path with VNRecognizeTextRequest and return list of results"""
|
|
||||||
with objc.autorelease_pool():
|
with objc.autorelease_pool():
|
||||||
input_url = NSURL.fileURLWithPath_(img_path)
|
input_url = NSURL.fileURLWithPath_(img_path)
|
||||||
|
|
||||||
@@ -26,9 +47,18 @@ def detect_text(img_path: str) -> List:
|
|||||||
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
||||||
|
|
||||||
vision_options = NSDictionary.dictionaryWithDictionary_({})
|
vision_options = NSDictionary.dictionaryWithDictionary_({})
|
||||||
vision_handler = Vision.VNImageRequestHandler.alloc().initWithCIImage_options_(
|
if orientation is not None:
|
||||||
input_image, vision_options
|
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 = []
|
results = []
|
||||||
handler = make_request_handler(results)
|
handler = make_request_handler(results)
|
||||||
vision_request = (
|
vision_request = (
|
||||||
@@ -38,6 +68,9 @@ def detect_text(img_path: str) -> List:
|
|||||||
vision_request.dealloc()
|
vision_request.dealloc()
|
||||||
vision_handler.dealloc()
|
vision_handler.dealloc()
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
result[0] = str(result[0])
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
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:
|
Explanation of the template string:
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
{title}{title?{descr?{newline},},}{descr}
|
{title,}{title?{descr?{newline},},}{descr,}
|
||||||
│ │ │ │ │ │ │
|
│ │ │ │ │ │ │
|
||||||
│ │ │ │ │ │ │
|
│ │ │ │ │ │ │
|
||||||
└──> insert title │ │ │ │ │
|
└──> insert title (or nothing if no title)
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │
|
||||||
└───> is there a title?
|
└───> is there a title?
|
||||||
│ │ │ │ │
|
│ │ │ │ │
|
||||||
@@ -299,6 +299,7 @@ Explanation of the template string:
|
|||||||
└───> if title is blank, insert nothing
|
└───> 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.
|
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.
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
__all__ = ["get_preferred_uti_extension", "get_uti_for_extension"]
|
||||||
""" get UTI for a given file extension and the preferred extension for a given UTI """
|
""" get UTI for a given file extension and the preferred extension for a given UTI """
|
||||||
|
|
||||||
""" Implementation note: runs only on macOS
|
""" Implementation note: runs only on macOS
|
||||||
@@ -528,35 +529,38 @@ def _get_uti_from_mdls(extension):
|
|||||||
# mdls -name kMDItemContentType foo.3fr
|
# mdls -name kMDItemContentType foo.3fr
|
||||||
# kMDItemContentType = "com.hasselblad.3fr-raw-image"
|
# kMDItemContentType = "com.hasselblad.3fr-raw-image"
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
|
try:
|
||||||
output = subprocess.check_output(
|
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
|
||||||
[
|
output = subprocess.check_output(
|
||||||
"/usr/bin/mdls",
|
[
|
||||||
"-name",
|
"/usr/bin/mdls",
|
||||||
"kMDItemContentType",
|
"-name",
|
||||||
temp.name,
|
"kMDItemContentType",
|
||||||
]
|
temp.name,
|
||||||
).splitlines()
|
]
|
||||||
output = output[0].decode("UTF-8") if output else None
|
).splitlines()
|
||||||
if not output:
|
output = output[0].decode("UTF-8") if output else None
|
||||||
return None
|
if not output:
|
||||||
|
return None
|
||||||
|
|
||||||
match = re.match(r'kMDItemContentType\s+\=\s+"(.*)"', output)
|
match = re.match(r'kMDItemContentType\s+\=\s+"(.*)"', output)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1)
|
return match.group(1)
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_uti_from_ext_dict(ext):
|
def _get_uti_from_ext_dict(ext):
|
||||||
try:
|
try:
|
||||||
return EXT_UTI_DICT[ext]
|
return EXT_UTI_DICT[ext.lower()]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_ext_from_uti_dict(uti):
|
def _get_ext_from_uti_dict(uti):
|
||||||
try:
|
try:
|
||||||
return UTI_EXT_DICT[uti]
|
return UTI_EXT_DICT[uti.lower()]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -588,6 +592,9 @@ def get_preferred_uti_extension(uti):
|
|||||||
def get_uti_for_extension(extension):
|
def get_uti_for_extension(extension):
|
||||||
"""get UTI for a given file extension"""
|
"""get UTI for a given file extension"""
|
||||||
|
|
||||||
|
if not extension:
|
||||||
|
return None
|
||||||
|
|
||||||
# accepts extension with or without leading 0
|
# accepts extension with or without leading 0
|
||||||
if extension[0] == ".":
|
if extension[0] == ".":
|
||||||
extension = extension[1:]
|
extension = extension[1:]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
""" Utility functions used in osxphotos """
|
""" Utility functions used in osxphotos """
|
||||||
|
|
||||||
|
import datetime
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import glob
|
import glob
|
||||||
import importlib
|
import importlib
|
||||||
@@ -16,11 +17,30 @@ import sys
|
|||||||
import unicodedata
|
import unicodedata
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
from plistlib import load as plistload
|
from plistlib import load as plistload
|
||||||
from typing import Callable
|
from typing import Callable, List, Optional, Union
|
||||||
|
|
||||||
import CoreFoundation
|
import CoreFoundation
|
||||||
|
import objc
|
||||||
|
from Foundation import NSFileManager, NSPredicate, NSString
|
||||||
|
|
||||||
from ._constants import UNICODE_FORMAT
|
from ._constants import UNICODE_FORMAT
|
||||||
|
from .path_utils import sanitize_filestem_with_count
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"dd_to_dms_str",
|
||||||
|
"expand_and_validate_filepath",
|
||||||
|
"get_last_library_path",
|
||||||
|
"get_system_library_path",
|
||||||
|
"increment_filename_with_count",
|
||||||
|
"increment_filename",
|
||||||
|
"lineno",
|
||||||
|
"list_directory",
|
||||||
|
"list_photo_libraries",
|
||||||
|
"load_function",
|
||||||
|
"noop",
|
||||||
|
"normalize_fs_path",
|
||||||
|
"normalize_unicode",
|
||||||
|
]
|
||||||
|
|
||||||
_DEBUG = False
|
_DEBUG = False
|
||||||
|
|
||||||
@@ -246,7 +266,9 @@ def list_photo_libraries():
|
|||||||
# On older MacOS versions, mdfind appears to ignore some libraries
|
# On older MacOS versions, mdfind appears to ignore some libraries
|
||||||
# glob to find libraries in ~/Pictures then mdfind to find all the others
|
# glob to find libraries in ~/Pictures then mdfind to find all the others
|
||||||
# TODO: make this more robust
|
# TODO: make this more robust
|
||||||
lib_list = glob.glob(f"{str(pathlib.Path.home())}/Pictures/*.photoslibrary")
|
lib_list = list_directory(
|
||||||
|
f"{pathlib.Path.home()}/Pictures/", glob="*.photoslibrary"
|
||||||
|
)
|
||||||
|
|
||||||
# On older OS, may not get all libraries so make sure we get the last one
|
# On older OS, may not get all libraries so make sure we get the last one
|
||||||
last_lib = get_last_library_path()
|
last_lib = get_last_library_path()
|
||||||
@@ -263,16 +285,97 @@ def list_photo_libraries():
|
|||||||
return lib_list
|
return lib_list
|
||||||
|
|
||||||
|
|
||||||
def findfiles(pattern, path_):
|
def normalize_fs_path(path: str) -> str:
|
||||||
"""Returns list of filenames from path_ matched by pattern
|
"""Normalize filesystem paths with unicode in them"""
|
||||||
shell pattern. Matching is case-insensitive.
|
# macOS HFS+ uses NFD, APFS doesn't normalize but stick with NFD
|
||||||
If 'path_' is invalid/doesn't exist, returns []."""
|
# ref: https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/
|
||||||
if not os.path.isdir(path_):
|
return unicodedata.normalize("NFD", path)
|
||||||
return []
|
|
||||||
# See: https://gist.github.com/techtonik/5694830
|
|
||||||
|
|
||||||
rule = re.compile(fnmatch.translate(pattern), re.IGNORECASE)
|
|
||||||
return [name for name in os.listdir(path_) if rule.match(name)]
|
# def findfiles(pattern, path):
|
||||||
|
# """Returns list of filenames from path matched by pattern
|
||||||
|
# shell pattern. Matching is case-insensitive.
|
||||||
|
# If 'path_' is invalid/doesn't exist, returns []."""
|
||||||
|
# if not os.path.isdir(path):
|
||||||
|
# return []
|
||||||
|
|
||||||
|
# # 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)
|
||||||
|
# files = os.listdir(path)
|
||||||
|
# return [name for name in files if rule.match(name)]
|
||||||
|
|
||||||
|
|
||||||
|
def list_directory(
|
||||||
|
directory: Union[str, pathlib.Path],
|
||||||
|
startswith: Optional[str] = None,
|
||||||
|
endswith: Optional[str] = None,
|
||||||
|
contains: Optional[str] = None,
|
||||||
|
glob: Optional[str] = None,
|
||||||
|
include_path: bool = False,
|
||||||
|
case_sensitive: bool = False,
|
||||||
|
) -> List[Union[str, pathlib.Path]]:
|
||||||
|
"""List directory contents and return list of files or directories matching search criteria.
|
||||||
|
Accounts for case-insensitive filesystems, unicode filenames. directory can be a str or a pathlib.Path object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory: directory to search
|
||||||
|
startswith: string to match at start of filename
|
||||||
|
endswith: string to match at end of filename
|
||||||
|
contains: string to match anywhere in filename
|
||||||
|
glob: shell-style glob pattern to match filename
|
||||||
|
include_path: if True, return full path to file
|
||||||
|
case_sensitive: if True, match case-sensitively
|
||||||
|
|
||||||
|
Returns: List of files or directories matching search criteria as either str or pathlib.Path objects depending on the input type;
|
||||||
|
returns empty list if directory is invalid or doesn't exist.
|
||||||
|
|
||||||
|
"""
|
||||||
|
is_pathlib = isinstance(directory, pathlib.Path)
|
||||||
|
if is_pathlib:
|
||||||
|
directory = str(directory)
|
||||||
|
|
||||||
|
if not os.path.isdir(directory):
|
||||||
|
return []
|
||||||
|
|
||||||
|
startswith = normalize_fs_path(startswith) if startswith else None
|
||||||
|
endswith = normalize_fs_path(endswith) if endswith else None
|
||||||
|
contains = normalize_fs_path(contains) if contains else None
|
||||||
|
glob = normalize_fs_path(glob) if glob else None
|
||||||
|
|
||||||
|
files = [normalize_fs_path(f) for f in os.listdir(directory)]
|
||||||
|
if not case_sensitive:
|
||||||
|
files_normalized = {f.lower(): f for f in files}
|
||||||
|
files = [f.lower() for f in files]
|
||||||
|
startswith = startswith.lower() if startswith else None
|
||||||
|
endswith = endswith.lower() if endswith else None
|
||||||
|
contains = contains.lower() if contains else None
|
||||||
|
glob = glob.lower() if glob else None
|
||||||
|
else:
|
||||||
|
files_normalized = {f: f for f in files}
|
||||||
|
|
||||||
|
if startswith:
|
||||||
|
files = [f for f in files if f.startswith(startswith)]
|
||||||
|
if endswith:
|
||||||
|
endswith = normalize_fs_path(endswith)
|
||||||
|
files = [f for f in files if f.endswith(endswith)]
|
||||||
|
if contains:
|
||||||
|
contains = normalize_fs_path(contains)
|
||||||
|
files = [f for f in files if contains in f]
|
||||||
|
if glob:
|
||||||
|
glob = normalize_fs_path(glob)
|
||||||
|
flags = re.IGNORECASE if not case_sensitive else 0
|
||||||
|
rule = re.compile(fnmatch.translate(glob), flags)
|
||||||
|
files = [f for f in files if rule.match(f)]
|
||||||
|
|
||||||
|
files = [files_normalized[f] for f in files]
|
||||||
|
|
||||||
|
if include_path:
|
||||||
|
files = [os.path.join(directory, f) for f in files]
|
||||||
|
if is_pathlib:
|
||||||
|
files = [pathlib.Path(f) for f in files]
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
def _open_sql_file(dbname):
|
def _open_sql_file(dbname):
|
||||||
@@ -313,70 +416,90 @@ def _db_is_locked(dbname):
|
|||||||
return locked
|
return locked
|
||||||
|
|
||||||
|
|
||||||
# OSXPHOTOS_XATTR_UUID = "com.osxphotos.uuid"
|
|
||||||
|
|
||||||
# def get_uuid_for_file(filepath):
|
|
||||||
# """ returns UUID associated with an exported file
|
|
||||||
# filepath: path to exported photo
|
|
||||||
# """
|
|
||||||
# attr = xattr.xattr(filepath)
|
|
||||||
# try:
|
|
||||||
# uuid_bytes = attr[OSXPHOTOS_XATTR_UUID]
|
|
||||||
# uuid_str = uuid_bytes.decode('utf-8')
|
|
||||||
# except KeyError:
|
|
||||||
# uuid_str = None
|
|
||||||
# return uuid_str
|
|
||||||
|
|
||||||
# def set_uuid_for_file(filepath, uuid):
|
|
||||||
# """ sets the UUID associated with an exported file
|
|
||||||
# filepath: path to exported photo
|
|
||||||
# uuid: uuid string for photo
|
|
||||||
# """
|
|
||||||
# if not os.path.exists(filepath):
|
|
||||||
# raise FileNotFoundError(f"Missing file: {filepath}")
|
|
||||||
|
|
||||||
# attr = xattr.xattr(filepath)
|
|
||||||
# uuid_bytes = bytes(uuid, 'utf-8')
|
|
||||||
# attr.set(OSXPHOTOS_XATTR_UUID, uuid_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_unicode(value):
|
def normalize_unicode(value):
|
||||||
"""normalize unicode data"""
|
"""normalize unicode data"""
|
||||||
if value is not None:
|
if value is None:
|
||||||
if isinstance(value, (tuple, list)):
|
|
||||||
return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value)
|
|
||||||
elif isinstance(value, str):
|
|
||||||
return unicodedata.normalize(UNICODE_FORMAT, value)
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
else:
|
|
||||||
return None
|
return None
|
||||||
|
if isinstance(value, (tuple, list)):
|
||||||
|
return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value)
|
||||||
|
elif isinstance(value, str):
|
||||||
|
return unicodedata.normalize(UNICODE_FORMAT, value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def increment_filename(filepath):
|
def increment_filename_with_count(
|
||||||
|
filepath: Union[str, pathlib.Path],
|
||||||
|
count: int = 0,
|
||||||
|
lock: bool = False,
|
||||||
|
dry_run: bool = False,
|
||||||
|
) -> str:
|
||||||
"""Return filename (1).ext, etc if filename.ext exists
|
"""Return filename (1).ext, etc if filename.ext exists
|
||||||
|
|
||||||
If file exists in filename's parent folder with same stem as filename,
|
If file exists in filename's parent folder with same stem as filename,
|
||||||
add (1), (2), etc. until a non-existing filename is found.
|
add (1), (2), etc. until a non-existing filename is found.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filepath: str; full path, including file name
|
filepath: str or pathlib.Path; full path, including file name
|
||||||
|
count: int; starting increment value
|
||||||
|
lock: bool; if True, create a lock file in form .filename.lock to prevent other processes from using the same filename
|
||||||
|
dry_run: bool; if True, don't actually create lock file
|
||||||
|
|
||||||
|
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 = list_directory(dest.parent, startswith=dest.stem)
|
||||||
|
dest_files = [f.stem.lower() for f in dest_files]
|
||||||
|
dest_new = f"{dest.stem} ({count})" if count else dest.stem
|
||||||
|
dest_new = normalize_fs_path(dest_new)
|
||||||
|
dest_new = sanitize_filestem_with_count(dest_new, dest.suffix)
|
||||||
|
if lock and not dry_run:
|
||||||
|
dest_lock = "." + dest_new + dest.suffix + ".lock"
|
||||||
|
dest_lock = dest.parent / dest_lock
|
||||||
|
else:
|
||||||
|
dest_lock = pathlib.Path("")
|
||||||
|
|
||||||
|
while dest_new.lower() in dest_files or (
|
||||||
|
lock and not dry_run and dest_lock.exists()
|
||||||
|
):
|
||||||
|
count += 1
|
||||||
|
dest_new = normalize_fs_path(f"{dest.stem} ({count})")
|
||||||
|
dest_new = sanitize_filestem_with_count(dest_new, dest.suffix)
|
||||||
|
if lock:
|
||||||
|
dest_lock = "." + dest_new + dest.suffix + ".lock"
|
||||||
|
dest_lock = dest.parent / dest_lock
|
||||||
|
if lock and not dry_run:
|
||||||
|
dest_lock.touch()
|
||||||
|
dest = dest.parent / f"{dest_new}{dest.suffix}"
|
||||||
|
|
||||||
|
return normalize_fs_path(str(dest)), count
|
||||||
|
|
||||||
|
|
||||||
|
def increment_filename(
|
||||||
|
filepath: Union[str, pathlib.Path], lock: bool = False, dry_run: bool = False
|
||||||
|
) -> 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
|
||||||
|
lock: bool; if True, creates a lock file in form .filename.lock to prevent other processes from using the same filename
|
||||||
|
dry_run: bool; if True, don't actually create lock file
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
new filepath (or same if not incremented)
|
new filepath (or same if not incremented)
|
||||||
|
|
||||||
Note: This obviously is subject to race condition so using with caution.
|
Note: This obviously is subject to race condition so using with caution but using lock=True reduces the risk of race condition (but lock files must be cleaned up)
|
||||||
"""
|
"""
|
||||||
dest = pathlib.Path(str(filepath))
|
new_filepath, _ = increment_filename_with_count(
|
||||||
count = 1
|
filepath, lock=lock, dry_run=dry_run
|
||||||
dest_files = findfiles(f"{dest.stem}*", str(dest.parent))
|
)
|
||||||
dest_files = [pathlib.Path(f).stem.lower() for f in dest_files]
|
return new_filepath
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def expand_and_validate_filepath(path: str) -> str:
|
def expand_and_validate_filepath(path: str) -> str:
|
||||||
@@ -416,3 +539,9 @@ def load_function(pyfile: str, function_name: str) -> Callable:
|
|||||||
sys.path = syspath
|
sys.path = syspath
|
||||||
|
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
def format_sec_to_hhmmss(sec: float) -> str:
|
||||||
|
"""Format seconds to hh:mm:ss"""
|
||||||
|
delta = datetime.timedelta(seconds=sec)
|
||||||
|
return str(delta).split(".")[0]
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
pyobjc-core>=7.2
|
Click>=8.0.1,<9.0
|
||||||
pyobjc-framework-AppleScriptKit>=7.2
|
Mako>=1.1.4,<1.2.0
|
||||||
pyobjc-framework-AppleScriptObjC>=7.2
|
PyYAML>=5.4.1,<6.0.0
|
||||||
pyobjc-framework-Photos>=7.2
|
bitmath>=1.3.3.1,<1.4.0.0
|
||||||
pyobjc-framework-Quartz>=7.2
|
|
||||||
pyobjc-framework-AVFoundation>=7.2
|
|
||||||
pyobjc-framework-CoreServices>=7.2
|
|
||||||
pyobjc-framework-Metal>=7.2
|
|
||||||
pyobjc-framework-Vision>=7.2
|
|
||||||
Click==8.0.1
|
|
||||||
PyYAML==5.4.1
|
|
||||||
Mako==1.1.4
|
|
||||||
bpylist2==3.0.2
|
bpylist2==3.0.2
|
||||||
pathvalidate==2.4.1
|
|
||||||
dataclasses==0.7;python_version<'3.7'
|
dataclasses==0.7;python_version<'3.7'
|
||||||
wurlitzer==2.1.0
|
more-itertools>=8.8.0,<9.0.0
|
||||||
photoscript==0.1.4
|
objexplore>=1.5.5,<1.6.0
|
||||||
toml==0.10.2
|
osxmetadata>=0.99.34,<1.0.0
|
||||||
osxmetadata==0.99.26
|
pathvalidate>=2.4.1,<2.5.0
|
||||||
textx==2.3.0
|
photoscript>=0.1.4,<0.2.0
|
||||||
rich==10.6.0
|
ptpython>=3.0.20,<3.1.0
|
||||||
bitmath==1.3.3.1
|
pyobjc-core>=7.3,<9.0
|
||||||
more-itertools==8.8.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
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
sphinx_click
|
|
||||||
pytest==6.2.4
|
|
||||||
pytest-mock
|
|
||||||
m2r2
|
|
||||||
pyinstaller==4.4
|
|
||||||
|
|
||||||
46
setup.py
@@ -70,32 +70,36 @@ setup(
|
|||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"pyobjc-core",
|
"bitmath>=1.3.3.1,<1.4.0.0",
|
||||||
"pyobjc-framework-AppleScriptKit",
|
|
||||||
"pyobjc-framework-AppleScriptObjC",
|
|
||||||
"pyobjc-framework-Photos",
|
|
||||||
"pyobjc-framework-Quartz",
|
|
||||||
"pyobjc-framework-AVFoundation",
|
|
||||||
"pyobjc-framework-CoreServices",
|
|
||||||
"pyobjc-framework-Metal",
|
|
||||||
"pyobjc-framework-Vision",
|
|
||||||
"Click==8.0.1",
|
|
||||||
"PyYAML==5.4.1",
|
|
||||||
"Mako==1.1.4",
|
|
||||||
"bpylist2==3.0.2",
|
"bpylist2==3.0.2",
|
||||||
"pathvalidate==2.4.1",
|
"Click>=8.0.1,<9.0",
|
||||||
"dataclasses==0.7;python_version<'3.7'",
|
"dataclasses==0.7;python_version<'3.7'",
|
||||||
"wurlitzer==2.1.0",
|
"Mako>=1.1.4,<1.2.0",
|
||||||
"photoscript==0.1.4",
|
"more-itertools>=8.8.0,<9.0.0",
|
||||||
"toml==0.10.2",
|
"objexplore>=1.5.5,<1.6.0",
|
||||||
"osxmetadata==0.99.26",
|
"osxmetadata>=0.99.34,<1.0.0",
|
||||||
"textx==2.3.0",
|
"pathvalidate>=2.4.1,<3.0.0",
|
||||||
"rich==10.6.0",
|
"photoscript>=0.1.4,<0.2.0",
|
||||||
"bitmath==1.3.3.1",
|
"ptpython>=3.0.20,<4.0.0",
|
||||||
"more-itertools==8.8.0",
|
"pyobjc-core>=7.3,<9.0",
|
||||||
|
"pyobjc-framework-AppleScriptKit>=7.3,<9.0",
|
||||||
|
"pyobjc-framework-AppleScriptObjC>=7.3,<9.0",
|
||||||
|
"pyobjc-framework-AVFoundation>=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",
|
||||||
|
"PyYAML>=5.4.1,<5.5.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"]},
|
entry_points={"console_scripts": ["osxphotos=osxphotos.__main__:cli"]},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<key>hostuuid</key>
|
<key>hostuuid</key>
|
||||||
<string>585B80BF-8D1F-55EF-A9E8-6CF4E5523959</string>
|
<string>585B80BF-8D1F-55EF-A9E8-6CF4E5523959</string>
|
||||||
<key>pid</key>
|
<key>pid</key>
|
||||||
<integer>1961</integer>
|
<integer>14817</integer>
|
||||||
<key>processname</key>
|
<key>processname</key>
|
||||||
<string>photolibraryd</string>
|
<string>photolibraryd</string>
|
||||||
<key>uid</key>
|
<key>uid</key>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 10 MiB |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 10 MiB |
|
After Width: | Height: | Size: 75 KiB |